diff options
author | Mike Crute <mcrute@gmail.com> | 2017-03-04 09:23:07 +0800 |
---|---|---|
committer | Mike Crute <mcrute@gmail.com> | 2017-03-04 09:23:07 +0800 |
commit | 3f8eccef7a91950494199bce2063d6fb7c9a712b (patch) | |
tree | dab1f51403290f10c4d32c5d2d4667a3d7187ded | |
download | mmds-3f8eccef7a91950494199bce2063d6fb7c9a712b.tar.bz2 mmds-3f8eccef7a91950494199bce2063d6fb7c9a712b.tar.xz mmds-3f8eccef7a91950494199bce2063d6fb7c9a712b.zip |
Initial Import
-rw-r--r-- | README.md | 115 | ||||
-rw-r--r-- | credentials.go | 153 | ||||
-rw-r--r-- | http.go | 124 | ||||
-rw-r--r-- | main.go | 372 | ||||
-rw-r--r-- | metadata-models.go | 98 | ||||
-rw-r--r-- | network.go | 94 |
6 files changed, 956 insertions, 0 deletions
diff --git a/README.md b/README.md new file mode 100644 index 0000000..817e194 --- /dev/null +++ b/README.md | |||
@@ -0,0 +1,115 @@ | |||
1 | # Mock Metadata Service | ||
2 | |||
3 | This software is still heavily a work-in-progress. The IAM functionality should | ||
4 | work but other stuff may not. Bug reports and pull requests welcome. | ||
5 | |||
6 | This package provides a mock metadata service that returns plausible | ||
7 | responses for most of the metadata service endpoints. It also provides a | ||
8 | full IAM temporary credential endpoint that will assume an IAM role and | ||
9 | continually refresh the credentials as time passes. All AWS SDKs and most AWS | ||
10 | agents are able to work with this interface provided that it is bound to | ||
11 | 169.254.169.254 port 80. | ||
12 | |||
13 | The daemon will attempt to bind two ports, port 80 on IP 169.245.169.254 | ||
14 | provides the mock metadata service that is only available on the instance. | ||
15 | Additionally port 8998 will be bound on all interfaces for the administrative | ||
16 | service. The administrative service is used to boostrap the daemon and provide | ||
17 | health-checking. | ||
18 | |||
19 | ## Setting up Interfaces | ||
20 | A loopback interface with IP address 169.254.169.254 is required by the daemon. | ||
21 | This can be accomplished on Linux with the following command: | ||
22 | |||
23 | ``` | ||
24 | sudo ip addr add 169.254.169.254/24 broadcast 169.254.169.255 dev lo:metadata | ||
25 | sudo ip link set dev lo:metadata up | ||
26 | sudo iptables -I INPUT 1 -d 169.254.0.0/16 ! -i lo -j DROP | ||
27 | ``` | ||
28 | |||
29 | *Note*: Do not bind this address to a publicly accessible interface or anyone | ||
30 | on the network will be able to use your AWS credentials. | ||
31 | |||
32 | ## Startup | ||
33 | On startup the daemon will bind the ports described above and will wait for a | ||
34 | bootstrap credential. At this time it will accept requests for all endpoints | ||
35 | but will return an IAM failure response for the assumed role. Once bootstrapped | ||
36 | it will assume the requested IAM role and begin serving credentials. | ||
37 | |||
38 | ## Health Checking | ||
39 | The daemon provides an HTTP endpoint on the administrative service to provide a | ||
40 | health status. The endpoint is `/status` and will return a JSON boolean (`true` | ||
41 | or `false`) to indicate that the daemon is running with a valid set of assumed | ||
42 | credentials. | ||
43 | |||
44 | ## Bootstrapping | ||
45 | Once the daemon has assumed a role it will continue to re-assume that role | ||
46 | using the credentials provided by the AssumeRole API call. However, initial | ||
47 | credentials are required to bootstrap the role. These credentials only need | ||
48 | permissions to assume the role, all other permissions should be granted to the | ||
49 | role itself. These credentials should be provided to the administrative service | ||
50 | using a POST request with a JSON body. | ||
51 | |||
52 | The POST endpoint is `/bootstrap/creds` and is write-only. The JSON formatted | ||
53 | message should contain an access key ID, a secret access key and optionally, a | ||
54 | session token. The format is: | ||
55 | |||
56 | ``` | ||
57 | { | ||
58 | "AccessKeyId": "AK...", | ||
59 | "SecretAccessKey": "...", | ||
60 | "Token": "..." | ||
61 | } | ||
62 | ``` | ||
63 | |||
64 | It is required to omit the token key or set the value to an empty string if no | ||
65 | token is available. | ||
66 | |||
67 | As soon as the bootstrap token is submitted the daemon will attempt to assume | ||
68 | the role it was started with and will begin allowing clients to reqeuest | ||
69 | credentials. | ||
70 | |||
71 | ## Known Missing Features | ||
72 | Many of these feature either don't make sense outside of AWS or are not | ||
73 | possible to emulate. | ||
74 | |||
75 | Instance identity document signing. This can not be implemented because only | ||
76 | AWS has the private key. | ||
77 | |||
78 | ``` | ||
79 | /latest/dynamic/instance-identity/signature | ||
80 | /latest/dynamic/instance-identity/pkcs7 | ||
81 | /latest/dynamic/instance-identity/rsa2048 | ||
82 | ``` | ||
83 | |||
84 | Block device mappings. May be available in the future. | ||
85 | |||
86 | ``` | ||
87 | /latest/meta-data/block-device-mapping/ami | ||
88 | /latest/meta-data/block-device-mapping/root | ||
89 | ``` | ||
90 | |||
91 | SSH keys. Will be available in a future release. | ||
92 | |||
93 | ``` | ||
94 | /latest/meta-data/public-keys/ | ||
95 | /latest/meta-data/public-keys/0/openssh-key | ||
96 | ``` | ||
97 | |||
98 | Network interface mapping. May be available in the future. | ||
99 | ``` | ||
100 | /latest/meta-data/network/interfaces/macs/ | ||
101 | /latest/meta-data/network/interfaces/macs/{mac}/device-number | ||
102 | /latest/meta-data/network/interfaces/macs/{mac}/interface-id | ||
103 | /latest/meta-data/network/interfaces/macs/{mac}/local-hostname | ||
104 | /latest/meta-data/network/interfaces/macs/{mac}/local-ipv4s | ||
105 | /latest/meta-data/network/interfaces/macs/{mac}/mac | ||
106 | /latest/meta-data/network/interfaces/macs/{mac}/owner-id | ||
107 | /latest/meta-data/network/interfaces/macs/{mac}/security-group-ids | ||
108 | /latest/meta-data/network/interfaces/macs/{mac}/security-groups | ||
109 | /latest/meta-data/network/interfaces/macs/{mac}/subnet-id | ||
110 | /latest/meta-data/network/interfaces/macs/{mac}/subnet-ipv4-cidr-block | ||
111 | /latest/meta-data/network/interfaces/macs/{mac}/vpc-id | ||
112 | /latest/meta-data/network/interfaces/macs/{mac}/vpc-ipv4-cidr-block | ||
113 | /latest/meta-data/network/interfaces/macs/{mac}/vpc-ipv4-cidr-blocks | ||
114 | /latest/meta-data/network/interfaces/macs/{mac}/vpc-ipv6-cidr-blocks | ||
115 | ``` | ||
diff --git a/credentials.go b/credentials.go new file mode 100644 index 0000000..a9412da --- /dev/null +++ b/credentials.go | |||
@@ -0,0 +1,153 @@ | |||
1 | package main | ||
2 | |||
3 | import ( | ||
4 | "time" | ||
5 | |||
6 | "github.com/aws/aws-sdk-go/aws" | ||
7 | "github.com/aws/aws-sdk-go/aws/credentials" | ||
8 | "github.com/aws/aws-sdk-go/aws/session" | ||
9 | "github.com/aws/aws-sdk-go/service/sts" | ||
10 | jww "github.com/spf13/jwalterweatherman" | ||
11 | ) | ||
12 | |||
13 | // Try to refresh credentials 3 times an hour but in the worst case if the | ||
14 | // credential refresh fails twice try to get one last refresh in before the end | ||
15 | // of the hour when the credential expires. | ||
16 | const REFRESH_INTERVAL = time.Duration(19) * time.Minute | ||
17 | |||
18 | type CredentialHandler interface { | ||
19 | Start() | ||
20 | InGoodState() bool | ||
21 | SetBootstrapCredential(*credentials.Credentials) | ||
22 | Output() chan *IAMCredentials | ||
23 | } | ||
24 | |||
25 | type credentialHandler struct { | ||
26 | region *string | ||
27 | roleARN *string | ||
28 | sessionName *string | ||
29 | bootstrapCreds *credentials.Credentials | ||
30 | output chan *IAMCredentials | ||
31 | input chan *credentials.Credentials | ||
32 | } | ||
33 | |||
34 | func NewCredentialHandler(region, arn, name *string) CredentialHandler { | ||
35 | return &credentialHandler{ | ||
36 | region: region, | ||
37 | roleARN: arn, | ||
38 | sessionName: name, | ||
39 | output: make(chan *IAMCredentials), | ||
40 | input: make(chan *credentials.Credentials, 1), // 1-item buffer to allow pre-start bootstrapping | ||
41 | } | ||
42 | } | ||
43 | |||
44 | func (h *credentialHandler) Output() chan *IAMCredentials { | ||
45 | return h.output | ||
46 | } | ||
47 | |||
48 | func (h *credentialHandler) InGoodState() bool { | ||
49 | c := <-h.Output() | ||
50 | return c.Code == "Success" | ||
51 | } | ||
52 | |||
53 | func (h *credentialHandler) SetBootstrapCredential(bc *credentials.Credentials) { | ||
54 | h.input <- bc | ||
55 | } | ||
56 | |||
57 | func (h *credentialHandler) Start() { | ||
58 | c := &IAMCredentials{Code: "Failure"} | ||
59 | updateChan := make(chan *IAMCredentials) | ||
60 | |||
61 | ticker := time.NewTicker(REFRESH_INTERVAL) | ||
62 | defer ticker.Stop() | ||
63 | |||
64 | jww.INFO.Printf("Starting credential handler, awaiting bootstrap") | ||
65 | |||
66 | for { | ||
67 | select { | ||
68 | // Read and update bootstrap credentials | ||
69 | case h.bootstrapCreds = <-h.input: | ||
70 | go h.refreshCredential(nil, updateChan) | ||
71 | // HTTP handler requests credential | ||
72 | case h.output <- c: | ||
73 | // Time to refresh credentials | ||
74 | case <-ticker.C: | ||
75 | go h.refreshCredential(c.rawCredentials, updateChan) | ||
76 | // Updated credentials arrive | ||
77 | case up := <-updateChan: | ||
78 | if up == nil && c.Expiration.After(time.Now()) { | ||
79 | c = &IAMCredentials{Code: "Failure"} | ||
80 | } else { | ||
81 | c = up | ||
82 | } | ||
83 | } | ||
84 | } | ||
85 | } | ||
86 | |||
87 | func (h *credentialHandler) refreshCredential(creds *credentials.Credentials, out chan *IAMCredentials) { | ||
88 | jww.INFO.Printf("Attempting to obtain credentials") | ||
89 | |||
90 | if creds == nil && h.bootstrapCreds == nil { | ||
91 | jww.WARN.Printf("No session or bootstrap credentials available") | ||
92 | return | ||
93 | } | ||
94 | |||
95 | if creds != nil { | ||
96 | jww.DEBUG.Printf("Attempting to use session credentials") | ||
97 | |||
98 | c, err := h.assumeRole(creds) | ||
99 | if err != nil { | ||
100 | jww.WARN.Printf("Failed to obtain with session credentials: %s", err) | ||
101 | } else { | ||
102 | jww.INFO.Printf("Successfully obtained credentials") | ||
103 | out <- c | ||
104 | return | ||
105 | } | ||
106 | } | ||
107 | |||
108 | if h.bootstrapCreds != nil { | ||
109 | jww.DEBUG.Printf("Attempting to use bootstrap credentials") | ||
110 | |||
111 | c, err := h.assumeRole(h.bootstrapCreds) | ||
112 | if err != nil { | ||
113 | jww.WARN.Printf("Failed to obtain with bootstrap credentials: %s", err) | ||
114 | } else { | ||
115 | jww.INFO.Printf("Successfully obtained credentials") | ||
116 | out <- c | ||
117 | return | ||
118 | } | ||
119 | } | ||
120 | |||
121 | jww.ERROR.Printf("Failed to obtain credentials") | ||
122 | out <- nil | ||
123 | } | ||
124 | |||
125 | func (h *credentialHandler) assumeRole(creds *credentials.Credentials) (*IAMCredentials, error) { | ||
126 | ses := session.New(&aws.Config{ | ||
127 | Region: h.region, | ||
128 | Credentials: creds, | ||
129 | }) | ||
130 | |||
131 | assumed, err := sts.New(ses).AssumeRole(&sts.AssumeRoleInput{ | ||
132 | RoleArn: h.roleARN, | ||
133 | RoleSessionName: h.sessionName, | ||
134 | }) | ||
135 | if err != nil { | ||
136 | return nil, err | ||
137 | } | ||
138 | |||
139 | return &IAMCredentials{ | ||
140 | Code: "Success", | ||
141 | Type: "AWS-HMAC", | ||
142 | AccessKeyId: *assumed.Credentials.AccessKeyId, | ||
143 | SecretAccessKey: *assumed.Credentials.SecretAccessKey, | ||
144 | Token: *assumed.Credentials.SessionToken, | ||
145 | LastUpdated: time.Now().UTC().Round(time.Second), | ||
146 | Expiration: *assumed.Credentials.Expiration, | ||
147 | rawCredentials: credentials.NewStaticCredentials( | ||
148 | *assumed.Credentials.AccessKeyId, | ||
149 | *assumed.Credentials.SecretAccessKey, | ||
150 | *assumed.Credentials.SessionToken, | ||
151 | ), | ||
152 | }, nil | ||
153 | } | ||
@@ -0,0 +1,124 @@ | |||
1 | package main | ||
2 | |||
3 | import ( | ||
4 | "context" | ||
5 | "encoding/json" | ||
6 | "fmt" | ||
7 | "net" | ||
8 | "net/http" | ||
9 | "reflect" | ||
10 | "strings" | ||
11 | "time" | ||
12 | |||
13 | jww "github.com/spf13/jwalterweatherman" | ||
14 | ) | ||
15 | |||
16 | const ( | ||
17 | APP_CTX_KEY = "app" | ||
18 | ) | ||
19 | |||
20 | // tcpKeepAliveListener sets TCP keep-alive timeouts on accepted | ||
21 | // connections. It's used by ListenAndServe and ListenAndServeTLS so | ||
22 | // dead TCP connections (e.g. closing laptop mid-download) eventually | ||
23 | // go away. | ||
24 | type tcpKeepAliveListener struct { | ||
25 | *net.TCPListener | ||
26 | } | ||
27 | |||
28 | func (ln tcpKeepAliveListener) Accept() (c net.Conn, err error) { | ||
29 | tc, err := ln.AcceptTCP() | ||
30 | if err != nil { | ||
31 | return | ||
32 | } | ||
33 | tc.SetKeepAlive(true) | ||
34 | tc.SetKeepAlivePeriod(3 * time.Minute) | ||
35 | return tc, nil | ||
36 | } | ||
37 | |||
38 | // Do the same thing ListenAndServe does but allow passing in the listener | ||
39 | // instead of the address so that we can bind privileged ports before dropping | ||
40 | // permissions to start the server itself. | ||
41 | func ListenAndServeRaw(ln net.Listener, handler http.Handler) error { | ||
42 | server := &http.Server{Addr: ln.Addr().String(), Handler: handler} | ||
43 | return server.Serve(tcpKeepAliveListener{ln.(*net.TCPListener)}) | ||
44 | } | ||
45 | |||
46 | // Handler that pushes the application context onto the request context stack | ||
47 | // so that it's available to all other handlers in the stack. | ||
48 | type ContextAwareHandler struct { | ||
49 | ctx *appContext | ||
50 | handler http.HandlerFunc | ||
51 | } | ||
52 | |||
53 | func (h ContextAwareHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { | ||
54 | defaultHeaders(w) | ||
55 | h.handler(w, r.WithContext(context.WithValue(r.Context(), APP_CTX_KEY, h.ctx))) | ||
56 | } | ||
57 | |||
58 | // Handler that is able to inspect the application context and print out the | ||
59 | // value of specific fields. | ||
60 | type ContextPrintingHandler struct { | ||
61 | ctx *appContext | ||
62 | field string | ||
63 | } | ||
64 | |||
65 | func (h ContextPrintingHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { | ||
66 | defaultHeaders(w) | ||
67 | |||
68 | c := reflect.ValueOf(h.ctx) | ||
69 | f := reflect.Indirect(c).FieldByName(h.field) | ||
70 | |||
71 | switch i := f.Interface().(type) { | ||
72 | case fmt.Stringer: | ||
73 | fmt.Fprintln(w, i.String()) | ||
74 | case *string: | ||
75 | if i == nil { | ||
76 | fmt.Fprintln(w, "") | ||
77 | } else { | ||
78 | fmt.Fprintln(w, *i) | ||
79 | } | ||
80 | default: | ||
81 | fmt.Fprintln(w, i) | ||
82 | } | ||
83 | } | ||
84 | |||
85 | // Print default headers for JSON data | ||
86 | func defaultHeaders(w http.ResponseWriter) { | ||
87 | w.Header().Set("Content-Type", "text/plain") | ||
88 | w.Header().Set("Server", "EC2ws") | ||
89 | w.Header().Set("Connection", "close") | ||
90 | } | ||
91 | |||
92 | // Write out JSON data in formatted form or an error | ||
93 | func writeHTTPJson(w http.ResponseWriter, data interface{}, name string) { | ||
94 | jd, err := json.MarshalIndent(data, "", " ") | ||
95 | if err != nil { | ||
96 | jww.ERROR.Printf("Error marshaling json in %s: %s", name, err) | ||
97 | http.Error(w, err.Error(), http.StatusInternalServerError) | ||
98 | } | ||
99 | fmt.Fprintf(w, string(jd)) | ||
100 | } | ||
101 | |||
102 | // Get the application context from the request context | ||
103 | func getAppCtx(r *http.Request) *appContext { | ||
104 | return r.Context().Value(APP_CTX_KEY).(*appContext) | ||
105 | } | ||
106 | |||
107 | // Handler that will reject non-local requests in case the daemon gets bound | ||
108 | // incorrectly to a public interface | ||
109 | type SecurityHandler struct { | ||
110 | handler http.Handler | ||
111 | } | ||
112 | |||
113 | func (h SecurityHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { | ||
114 | // String is ip:port formatted | ||
115 | ip := strings.Split(r.RemoteAddr, ":")[0] | ||
116 | |||
117 | if ip != "169.254.169.254" && ip != "127.0.0.1" { | ||
118 | jww.ERROR.Printf("Non-local metadata request from %s!", ip) | ||
119 | http.NotFound(w, r) | ||
120 | return | ||
121 | } else { | ||
122 | h.handler.ServeHTTP(w, r) | ||
123 | } | ||
124 | } | ||
@@ -0,0 +1,372 @@ | |||
1 | package main | ||
2 | |||
3 | import ( | ||
4 | "bytes" | ||
5 | "crypto/hmac" | ||
6 | "crypto/sha256" | ||
7 | "encoding/hex" | ||
8 | "encoding/json" | ||
9 | "errors" | ||
10 | "flag" | ||
11 | "fmt" | ||
12 | "io/ioutil" | ||
13 | "net" | ||
14 | "net/http" | ||
15 | "os" | ||
16 | "os/user" | ||
17 | "strconv" | ||
18 | "time" | ||
19 | |||
20 | "github.com/aws/aws-sdk-go/aws/credentials" | ||
21 | "github.com/aws/aws-sdk-go/aws/ec2metadata" | ||
22 | "github.com/gorilla/handlers" | ||
23 | "github.com/gorilla/mux" | ||
24 | "github.com/sid77/drop" | ||
25 | jww "github.com/spf13/jwalterweatherman" | ||
26 | ) | ||
27 | |||
28 | var ( | ||
29 | DEFAULT_VALUES map[string]*string | ||
30 | ) | ||
31 | |||
32 | func init() { | ||
33 | DEFAULT_VALUES = map[string]*string{ | ||
34 | "domain": StringPtr("amazonaws.com"), | ||
35 | "partition": StringPtr("aws"), | ||
36 | "ami-launch-index": StringPtr("0"), | ||
37 | "ami-manifest-path": StringPtr("(unknown)"), | ||
38 | "instance-action": StringPtr("none"), | ||
39 | "profile": StringPtr("default-hvm"), | ||
40 | "security-groups": StringPtr("default"), | ||
41 | } | ||
42 | } | ||
43 | |||
44 | func StringPtr(v string) *string { | ||
45 | return &v | ||
46 | } | ||
47 | |||
48 | type appContext struct { | ||
49 | PrivateIP *net.IP | ||
50 | Region *string | ||
51 | MacAddr *net.HardwareAddr | ||
52 | AvailabilityZone *string | ||
53 | Hostname *string | ||
54 | InstanceId *string | ||
55 | InstanceType *string | ||
56 | AccountId *int64 | ||
57 | ImageId *string | ||
58 | ReservationId *string | ||
59 | InstanceProfileId *string | ||
60 | RoleARN *string | ||
61 | BootstrapSecret *string | ||
62 | CredentialHandler CredentialHandler | ||
63 | } | ||
64 | |||
65 | func (c *appContext) FormatAZ() string { | ||
66 | return fmt.Sprintf("%s%s", *c.Region, *c.AvailabilityZone) | ||
67 | } | ||
68 | |||
69 | func GetKeyHandler(content map[string]*string, key string) http.Handler { | ||
70 | return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { | ||
71 | w.Write([]byte(*content[key])) | ||
72 | }) | ||
73 | } | ||
74 | |||
75 | func AvailabilityZoneHandler(w http.ResponseWriter, r *http.Request) { | ||
76 | ctx := getAppCtx(r) | ||
77 | fmt.Fprintf(w, ctx.FormatAZ()) | ||
78 | } | ||
79 | |||
80 | func IAMCredentialHandler(w http.ResponseWriter, r *http.Request) { | ||
81 | ctx := getAppCtx(r) | ||
82 | vars := mux.Vars(r) | ||
83 | |||
84 | name, err := parseRoleName(*ctx.RoleARN) | ||
85 | if err != nil { | ||
86 | jww.ERROR.Printf("Error parsing role name in IAMCredentialHandler: %s", err) | ||
87 | http.Error(w, err.Error(), http.StatusInternalServerError) | ||
88 | } | ||
89 | |||
90 | if vars["profile"] != name { | ||
91 | http.NotFound(w, r) | ||
92 | return | ||
93 | } | ||
94 | |||
95 | writeHTTPJson(w, <-ctx.CredentialHandler.Output(), "IAMCredentialHandler") | ||
96 | } | ||
97 | |||
98 | func StatusHandler(w http.ResponseWriter, r *http.Request) { | ||
99 | ctx := getAppCtx(r) | ||
100 | writeHTTPJson(w, ctx.CredentialHandler.InGoodState(), "IAMCredentialHandler") | ||
101 | } | ||
102 | |||
103 | type bootstrapInput struct { | ||
104 | AccessKeyId string | ||
105 | SecretAccessKey string | ||
106 | Token string | ||
107 | Signature string | ||
108 | } | ||
109 | |||
110 | func validateSignature(r *bootstrapInput, key *string) bool { | ||
111 | buf := bytes.Buffer{} | ||
112 | // Alphabetical order matters here | ||
113 | buf.WriteString(fmt.Sprintf("AccessKeyId%s", r.AccessKeyId)) | ||
114 | buf.WriteString(fmt.Sprintf("SecretAccessKey%s", r.SecretAccessKey)) | ||
115 | |||
116 | // Only hash token if it was presented | ||
117 | if r.Token != "" { | ||
118 | buf.WriteString(fmt.Sprintf("Token%s", r.Token)) | ||
119 | } | ||
120 | |||
121 | mac := hmac.New(sha256.New, []byte(*key)) | ||
122 | mac.Write(buf.Bytes()) | ||
123 | expected := mac.Sum(nil) | ||
124 | |||
125 | sig, err := hex.DecodeString(r.Signature) | ||
126 | if err != nil { | ||
127 | return false | ||
128 | } | ||
129 | |||
130 | return hmac.Equal(expected, sig) | ||
131 | } | ||
132 | |||
133 | func BootstrapCredentialHandler(w http.ResponseWriter, r *http.Request) { | ||
134 | ctx := getAppCtx(r) | ||
135 | d := json.NewDecoder(r.Body) | ||
136 | var cred bootstrapInput | ||
137 | |||
138 | err := d.Decode(&cred) | ||
139 | if err != nil { | ||
140 | jww.ERROR.Printf("Error decoding bootstrap JSON: %s", err) | ||
141 | http.Error(w, err.Error(), http.StatusInternalServerError) | ||
142 | } | ||
143 | |||
144 | if !validateSignature(&cred, ctx.BootstrapSecret) { | ||
145 | jww.ERROR.Printf("Invalid signature for bootstrapping credentials") | ||
146 | http.Error(w, "Not allowed", http.StatusForbidden) | ||
147 | return | ||
148 | } | ||
149 | |||
150 | creds := credentials.NewStaticCredentials(cred.AccessKeyId, cred.SecretAccessKey, cred.Token) | ||
151 | ctx.CredentialHandler.SetBootstrapCredential(creds) | ||
152 | } | ||
153 | |||
154 | func IAMInfoHandler(w http.ResponseWriter, r *http.Request) { | ||
155 | ctx := getAppCtx(r) | ||
156 | |||
157 | p := &ec2metadata.EC2IAMInfo{ | ||
158 | Code: "Success", | ||
159 | LastUpdated: time.Now().UTC().Round(time.Second), | ||
160 | InstanceProfileArn: *ctx.RoleARN, | ||
161 | InstanceProfileID: *ctx.InstanceProfileId, | ||
162 | } | ||
163 | |||
164 | writeHTTPJson(w, p, "IAMInfoHandler") | ||
165 | } | ||
166 | |||
167 | func InstanceProfileListHandler(w http.ResponseWriter, r *http.Request) { | ||
168 | ctx := getAppCtx(r) | ||
169 | |||
170 | if !ctx.CredentialHandler.InGoodState() { | ||
171 | jww.ERROR.Printf("Credential handler in a bad state") | ||
172 | fmt.Fprintf(w, "") | ||
173 | return | ||
174 | } | ||
175 | |||
176 | name, err := parseRoleName(*ctx.RoleARN) | ||
177 | if err != nil { | ||
178 | jww.ERROR.Printf("Error parsing role name in InstanceProfileListHandler: %s", err) | ||
179 | http.Error(w, err.Error(), http.StatusInternalServerError) | ||
180 | } | ||
181 | |||
182 | fmt.Fprintf(w, name) | ||
183 | } | ||
184 | |||
185 | func IdentityDocumentHandler(w http.ResponseWriter, r *http.Request) { | ||
186 | ctx := getAppCtx(r) | ||
187 | |||
188 | id := &ec2metadata.EC2InstanceIdentityDocument{ | ||
189 | PrivateIP: ctx.PrivateIP.String(), | ||
190 | DevpayProductCodes: nil, | ||
191 | AvailabilityZone: ctx.FormatAZ(), | ||
192 | Version: "2010-08-31", | ||
193 | InstanceID: *ctx.InstanceId, | ||
194 | BillingProducts: nil, | ||
195 | InstanceType: *ctx.InstanceType, | ||
196 | ImageID: *ctx.ImageId, | ||
197 | AccountID: strconv.FormatInt(*ctx.AccountId, 10), | ||
198 | Architecture: "x86_64", | ||
199 | KernelID: "", | ||
200 | RamdiskID: "", | ||
201 | PendingTime: time.Now().Round(time.Second), | ||
202 | Region: *ctx.Region, | ||
203 | } | ||
204 | |||
205 | writeHTTPJson(w, id, "IdentityDocumentHandler") | ||
206 | } | ||
207 | |||
208 | func buildMetadataHandler(ctx *appContext, defaults map[string]*string) http.Handler { | ||
209 | r := mux.NewRouter() | ||
210 | |||
211 | // Static Data Handlers | ||
212 | r.Handle("/latest/meta-data/services/domain", GetKeyHandler(defaults, "domain")) | ||
213 | r.Handle("/latest/meta-data/services/partition", GetKeyHandler(defaults, "partition")) | ||
214 | r.Handle("/latest/meta-data/ami-launch-index", GetKeyHandler(defaults, "ami-launch-index")) | ||
215 | r.Handle("/latest/meta-data/ami-manifest-path", GetKeyHandler(defaults, "ami-manifest-path")) | ||
216 | r.Handle("/latest/meta-data/instance-action", GetKeyHandler(defaults, "instance-action")) | ||
217 | r.Handle("/latest/meta-data/profile", GetKeyHandler(defaults, "profile")) | ||
218 | r.Handle("/latest/meta-data/security-groups", GetKeyHandler(defaults, "security-groups")) | ||
219 | |||
220 | // Machine-specific Pseudo-static Handlers | ||
221 | r.Handle("/latest/meta-data/mac", ContextPrintingHandler{ctx, "MacAddr"}) | ||
222 | r.Handle("/latest/meta-data/hostname", ContextPrintingHandler{ctx, "Hostname"}) | ||
223 | r.Handle("/latest/meta-data/local-hostname", ContextPrintingHandler{ctx, "Hostname"}) | ||
224 | r.Handle("/latest/meta-data/local-ipv4", ContextPrintingHandler{ctx, "PrivateIP"}) | ||
225 | r.Handle("/latest/meta-data/instance-id", ContextPrintingHandler{ctx, "InstanceId"}) | ||
226 | r.Handle("/latest/meta-data/instance-type", ContextPrintingHandler{ctx, "InstanceType"}) | ||
227 | r.Handle("/latest/meta-data/ami-id", ContextPrintingHandler{ctx, "ImageId"}) | ||
228 | r.Handle("/latest/meta-data/reservation-id", ContextPrintingHandler{ctx, "ReservationId"}) | ||
229 | |||
230 | // Context-specific Handlers | ||
231 | r.Handle("/latest/meta-data/placement/availability-zone", ContextAwareHandler{ctx, AvailabilityZoneHandler}) | ||
232 | r.Handle("/latest/dynamic/instance-identity/document", ContextAwareHandler{ctx, IdentityDocumentHandler}) | ||
233 | |||
234 | // IAM Credential Handlers | ||
235 | r.Handle("/latest/meta-data/iam/info", ContextAwareHandler{ctx, IAMInfoHandler}) | ||
236 | r.Handle("/latest/meta-data/iam/security-credentials/", ContextAwareHandler{ctx, InstanceProfileListHandler}) | ||
237 | r.Handle("/latest/meta-data/iam/security-credentials/{profile}", ContextAwareHandler{ctx, IAMCredentialHandler}) | ||
238 | |||
239 | return handlers.LoggingHandler(os.Stdout, SecurityHandler{r}) | ||
240 | } | ||
241 | |||
242 | func buildAdminHandler(ctx *appContext) http.Handler { | ||
243 | r := mux.NewRouter() | ||
244 | |||
245 | r.Handle("/bootstrap/creds", ContextAwareHandler{ctx, BootstrapCredentialHandler}).Methods("POST") | ||
246 | r.Handle("/status", ContextAwareHandler{ctx, StatusHandler}).Methods("GET") | ||
247 | |||
248 | return handlers.LoggingHandler(os.Stdout, r) | ||
249 | } | ||
250 | |||
251 | type UserArgs struct { | ||
252 | regionAZ string | ||
253 | InstanceType string | ||
254 | Region string | ||
255 | AvailabilityZone string | ||
256 | RoleARN string | ||
257 | User string | ||
258 | AccountNumber int64 | ||
259 | BootstrapSecret string | ||
260 | } | ||
261 | |||
262 | func parseArgs() (*UserArgs, error) { | ||
263 | a := &UserArgs{} | ||
264 | |||
265 | flag.StringVar(&a.regionAZ, "region", "us-west-2a", "region and availability zone") | ||
266 | flag.StringVar(&a.InstanceType, "instance", "t2.micro", "instance type") | ||
267 | flag.StringVar(&a.RoleARN, "arn", "", "ARN of role to assume") | ||
268 | flag.StringVar(&a.User, "user", "nobody", "run-as user after port binding") | ||
269 | flag.StringVar(&a.BootstrapSecret, "secret", "", "bootstrap signing secret") | ||
270 | |||
271 | flag.Parse() | ||
272 | |||
273 | _, err := user.Lookup(a.User) | ||
274 | if err != nil { | ||
275 | return nil, err | ||
276 | } | ||
277 | |||
278 | region, az, err := parseRegionAZ(a.regionAZ) | ||
279 | if err != nil { | ||
280 | return nil, err | ||
281 | } | ||
282 | a.Region = region | ||
283 | a.AvailabilityZone = az | ||
284 | |||
285 | act, err := parseAccountFromARN(a.RoleARN) | ||
286 | if err != nil { | ||
287 | return nil, err | ||
288 | } | ||
289 | a.AccountNumber = act | ||
290 | |||
291 | if a.BootstrapSecret == "" { | ||
292 | return nil, errors.New("Bootstrap secret must be set") | ||
293 | } | ||
294 | |||
295 | return a, nil | ||
296 | } | ||
297 | |||
298 | func initialBootstrap(file string, handler CredentialHandler) { | ||
299 | bd, err := ioutil.ReadFile(file) | ||
300 | if err != nil { | ||
301 | jww.ERROR.Printf("Error reading bootstrap file: %s", err.Error()) | ||
302 | return | ||
303 | } | ||
304 | |||
305 | var cred bootstrapInput | ||
306 | err = json.Unmarshal(bd, &cred) | ||
307 | if err != nil { | ||
308 | jww.ERROR.Printf("Error decoding bootstrap JSON: %s", err) | ||
309 | return | ||
310 | } | ||
311 | |||
312 | creds := credentials.NewStaticCredentials(cred.AccessKeyId, cred.SecretAccessKey, cred.Token) | ||
313 | handler.SetBootstrapCredential(creds) | ||
314 | } | ||
315 | |||
316 | func main() { | ||
317 | jww.SetStdoutThreshold(jww.LevelInfo) | ||
318 | |||
319 | args, err := parseArgs() | ||
320 | if err != nil { | ||
321 | fmt.Println(err.Error()) | ||
322 | return | ||
323 | } | ||
324 | |||
325 | // Bind this port first because it requires privilges then drop the | ||
326 | // privileges before we do anything else | ||
327 | metalistener, err := net.Listen("tcp", "169.254.169.254:80") | ||
328 | if err != nil { | ||
329 | jww.FATAL.Printf("Error setting up listener: %s", err.Error()) | ||
330 | return | ||
331 | } | ||
332 | |||
333 | if err := drop.DropPrivileges(args.User); err != nil { | ||
334 | jww.FATAL.Printf("Unable to drop privileges") | ||
335 | return | ||
336 | } | ||
337 | |||
338 | iface, err := getInterfaceContext() | ||
339 | if err != nil { | ||
340 | jww.FATAL.Printf("Error getting network interface info: %s", err) | ||
341 | return | ||
342 | } | ||
343 | |||
344 | credHandler := NewCredentialHandler( | ||
345 | &args.Region, | ||
346 | &args.RoleARN, | ||
347 | iface.PrimaryHostname, | ||
348 | ) | ||
349 | |||
350 | ctx := &appContext{ | ||
351 | PrivateIP: iface.PrimaryIP, | ||
352 | Region: &args.Region, | ||
353 | MacAddr: iface.MacAddr, | ||
354 | AvailabilityZone: &args.AvailabilityZone, | ||
355 | Hostname: iface.PrimaryHostname, | ||
356 | InstanceId: generateInstanceId(iface.PrimaryHostname), | ||
357 | InstanceType: &args.InstanceType, | ||
358 | AccountId: &args.AccountNumber, | ||
359 | ReservationId: generatePlausibleId("r"), | ||
360 | ImageId: generatePlausibleId("ami"), | ||
361 | InstanceProfileId: generatePlausibleProfileId(), | ||
362 | RoleARN: &args.RoleARN, | ||
363 | BootstrapSecret: &args.BootstrapSecret, | ||
364 | CredentialHandler: credHandler, | ||
365 | } | ||
366 | |||
367 | initialBootstrap("/etc/mmds/bootstrap-cred.json", credHandler) | ||
368 | |||
369 | go credHandler.Start() | ||
370 | go http.ListenAndServe(":8000", buildAdminHandler(ctx)) | ||
371 | ListenAndServeRaw(metalistener, buildMetadataHandler(ctx, DEFAULT_VALUES)) | ||
372 | } | ||
diff --git a/metadata-models.go b/metadata-models.go new file mode 100644 index 0000000..eff9467 --- /dev/null +++ b/metadata-models.go | |||
@@ -0,0 +1,98 @@ | |||
1 | package main | ||
2 | |||
3 | import ( | ||
4 | "crypto/rand" | ||
5 | "crypto/sha1" | ||
6 | "encoding/hex" | ||
7 | "errors" | ||
8 | "fmt" | ||
9 | "regexp" | ||
10 | "strconv" | ||
11 | "strings" | ||
12 | "time" | ||
13 | |||
14 | "github.com/aws/aws-sdk-go/aws/credentials" | ||
15 | ) | ||
16 | |||
17 | var ( | ||
18 | REGION_AZ_REGEXP = regexp.MustCompile("((?:us|ca|eu|ap|sa)-(?:north|south)?(?:east|west)-\\d)([a-f])") | ||
19 | ) | ||
20 | |||
21 | type IAMCredentials struct { | ||
22 | Code string | ||
23 | LastUpdated time.Time | ||
24 | Type string | ||
25 | AccessKeyId string | ||
26 | SecretAccessKey string | ||
27 | Token string | ||
28 | Expiration time.Time | ||
29 | rawCredentials *credentials.Credentials | ||
30 | } | ||
31 | |||
32 | func generatePlausibleId(prefix string) *string { | ||
33 | b := make([]byte, 10) | ||
34 | rand.Read(b) | ||
35 | h := sha1.New().Sum(b) | ||
36 | o := fmt.Sprintf("%s-%s", prefix, hex.EncodeToString(h)[0:17]) | ||
37 | return &o | ||
38 | } | ||
39 | |||
40 | func generateInstanceId(hostname *string) *string { | ||
41 | h := sha1.New().Sum([]byte(*hostname)) | ||
42 | o := fmt.Sprintf("i-%s", hex.EncodeToString(h)[0:17]) | ||
43 | return &o | ||
44 | } | ||
45 | |||
46 | func generatePlausibleProfileId() *string { | ||
47 | b := make([]byte, 16) | ||
48 | rand.Read(b) | ||
49 | |||
50 | for i, bb := range b { | ||
51 | if bb%3 == 0 { | ||
52 | b[i] = bb%10 + 48 | ||
53 | } else { | ||
54 | b[i] = bb%26 + 65 | ||
55 | } | ||
56 | } | ||
57 | |||
58 | res := fmt.Sprintf("AIPAI%s", string(b)) | ||
59 | |||
60 | return &res | ||
61 | } | ||
62 | |||
63 | func parseAccountFromARN(arn string) (int64, error) { | ||
64 | parts := strings.Split(arn, ":") | ||
65 | if len(parts) != 6 { | ||
66 | return 0, errors.New("Invalid ARN format") | ||
67 | } | ||
68 | |||
69 | id, err := strconv.ParseInt(parts[4], 10, 64) | ||
70 | if err != nil { | ||
71 | return 0, err | ||
72 | } | ||
73 | |||
74 | return id, nil | ||
75 | } | ||
76 | |||
77 | func parseRoleName(arn string) (string, error) { | ||
78 | parts := strings.Split(arn, ":") | ||
79 | if len(parts) != 6 { | ||
80 | return "", errors.New("Invalid ARN format") | ||
81 | } | ||
82 | |||
83 | role := strings.Split(parts[5], "/") | ||
84 | if len(role) != 2 { | ||
85 | return "", errors.New("Invalid role name format") | ||
86 | } | ||
87 | |||
88 | return role[1], nil | ||
89 | } | ||
90 | |||
91 | func parseRegionAZ(in string) (string, string, error) { | ||
92 | match := REGION_AZ_REGEXP.FindAllStringSubmatch("us-west-2a", -1) | ||
93 | if match == nil || len(match) == 0 { | ||
94 | return "", "", errors.New("Unable to parse region/AZ") | ||
95 | } | ||
96 | |||
97 | return match[0][1], match[0][2], nil | ||
98 | } | ||
diff --git a/network.go b/network.go new file mode 100644 index 0000000..d78a57d --- /dev/null +++ b/network.go | |||
@@ -0,0 +1,94 @@ | |||
1 | package main | ||
2 | |||
3 | import ( | ||
4 | "errors" | ||
5 | "net" | ||
6 | "os" | ||
7 | "strings" | ||
8 | |||
9 | jww "github.com/spf13/jwalterweatherman" | ||
10 | ) | ||
11 | |||
12 | type interfaceContext struct { | ||
13 | PrimaryIP *net.IP | ||
14 | MacAddr *net.HardwareAddr | ||
15 | PrimaryHostname *string | ||
16 | } | ||
17 | |||
18 | func interfaceIsUsable(i *net.Interface) bool { | ||
19 | if i.Flags&net.FlagLoopback != 0 { | ||
20 | return false | ||
21 | } | ||
22 | |||
23 | if i.Flags&net.FlagUp == 0 { | ||
24 | return false | ||
25 | } | ||
26 | |||
27 | addrs, err := i.Addrs() | ||
28 | if err != nil || len(addrs) == 0 { | ||
29 | return false | ||
30 | } | ||
31 | |||
32 | if strings.HasPrefix(i.Name, "docker") { | ||
33 | return false | ||
34 | } | ||
35 | |||
36 | return true | ||
37 | } | ||
38 | |||
39 | func getPrimaryInterface() (*net.Interface, error) { | ||
40 | ifaces, err := net.Interfaces() | ||
41 | if err != nil { | ||
42 | return nil, err | ||
43 | } | ||
44 | |||
45 | if len(ifaces) == 0 { | ||
46 | return nil, errors.New("no interfaces found") | ||
47 | } | ||
48 | |||
49 | var iface net.Interface | ||
50 | for _, iface = range ifaces { | ||
51 | if !interfaceIsUsable(&iface) { | ||
52 | continue | ||
53 | } else { | ||
54 | break | ||
55 | } | ||
56 | } | ||
57 | |||
58 | if interfaceIsUsable(&iface) { | ||
59 | return &iface, nil | ||
60 | } else { | ||
61 | return nil, errors.New("no usable interfaces found") | ||
62 | } | ||
63 | } | ||
64 | |||
65 | func getInterfaceContext() (*interfaceContext, error) { | ||
66 | iface, err := getPrimaryInterface() | ||
67 | if err != nil { | ||
68 | return nil, err | ||
69 | } | ||
70 | |||
71 | // Validated to have at least this in interfaceIsUsable | ||
72 | addrs, err := iface.Addrs() | ||
73 | if err != nil { | ||
74 | return nil, err | ||
75 | } | ||
76 | |||
77 | primaryAddr := addrs[0].String() | ||
78 | primaryIP, _, err := net.ParseCIDR(primaryAddr) | ||
79 | if err != nil { | ||
80 | return nil, err | ||
81 | } | ||
82 | |||
83 | name, err := os.Hostname() | ||
84 | if err != nil { | ||
85 | jww.ERROR.Printf("Unable to resolve hostname, using localhost: %s", err.Error()) | ||
86 | name = "localhost" | ||
87 | } | ||
88 | |||
89 | return &interfaceContext{ | ||
90 | PrimaryIP: &primaryIP, | ||
91 | MacAddr: &iface.HardwareAddr, | ||
92 | PrimaryHostname: &name, | ||
93 | }, nil | ||
94 | } | ||