aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorMike Crute <mcrute@gmail.com>2017-03-04 09:23:07 +0800
committerMike Crute <mcrute@gmail.com>2017-03-04 09:23:07 +0800
commit3f8eccef7a91950494199bce2063d6fb7c9a712b (patch)
treedab1f51403290f10c4d32c5d2d4667a3d7187ded
downloadmmds-3f8eccef7a91950494199bce2063d6fb7c9a712b.tar.bz2
mmds-3f8eccef7a91950494199bce2063d6fb7c9a712b.tar.xz
mmds-3f8eccef7a91950494199bce2063d6fb7c9a712b.zip
Initial Import
-rw-r--r--README.md115
-rw-r--r--credentials.go153
-rw-r--r--http.go124
-rw-r--r--main.go372
-rw-r--r--metadata-models.go98
-rw-r--r--network.go94
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
3This software is still heavily a work-in-progress. The IAM functionality should
4work but other stuff may not. Bug reports and pull requests welcome.
5
6This package provides a mock metadata service that returns plausible
7responses for most of the metadata service endpoints. It also provides a
8full IAM temporary credential endpoint that will assume an IAM role and
9continually refresh the credentials as time passes. All AWS SDKs and most AWS
10agents are able to work with this interface provided that it is bound to
11169.254.169.254 port 80.
12
13The daemon will attempt to bind two ports, port 80 on IP 169.245.169.254
14provides the mock metadata service that is only available on the instance.
15Additionally port 8998 will be bound on all interfaces for the administrative
16service. The administrative service is used to boostrap the daemon and provide
17health-checking.
18
19## Setting up Interfaces
20A loopback interface with IP address 169.254.169.254 is required by the daemon.
21This can be accomplished on Linux with the following command:
22
23```
24sudo ip addr add 169.254.169.254/24 broadcast 169.254.169.255 dev lo:metadata
25sudo ip link set dev lo:metadata up
26sudo 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
30on the network will be able to use your AWS credentials.
31
32## Startup
33On startup the daemon will bind the ports described above and will wait for a
34bootstrap credential. At this time it will accept requests for all endpoints
35but will return an IAM failure response for the assumed role. Once bootstrapped
36it will assume the requested IAM role and begin serving credentials.
37
38## Health Checking
39The daemon provides an HTTP endpoint on the administrative service to provide a
40health status. The endpoint is `/status` and will return a JSON boolean (`true`
41or `false`) to indicate that the daemon is running with a valid set of assumed
42credentials.
43
44## Bootstrapping
45Once the daemon has assumed a role it will continue to re-assume that role
46using the credentials provided by the AssumeRole API call. However, initial
47credentials are required to bootstrap the role. These credentials only need
48permissions to assume the role, all other permissions should be granted to the
49role itself. These credentials should be provided to the administrative service
50using a POST request with a JSON body.
51
52The POST endpoint is `/bootstrap/creds` and is write-only. The JSON formatted
53message should contain an access key ID, a secret access key and optionally, a
54session token. The format is:
55
56```
57{
58 "AccessKeyId": "AK...",
59 "SecretAccessKey": "...",
60 "Token": "..."
61}
62```
63
64It is required to omit the token key or set the value to an empty string if no
65token is available.
66
67As soon as the bootstrap token is submitted the daemon will attempt to assume
68the role it was started with and will begin allowing clients to reqeuest
69credentials.
70
71## Known Missing Features
72Many of these feature either don't make sense outside of AWS or are not
73possible to emulate.
74
75Instance identity document signing. This can not be implemented because only
76AWS 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
84Block 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
91SSH 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
98Network 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 @@
1package main
2
3import (
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.
16const REFRESH_INTERVAL = time.Duration(19) * time.Minute
17
18type CredentialHandler interface {
19 Start()
20 InGoodState() bool
21 SetBootstrapCredential(*credentials.Credentials)
22 Output() chan *IAMCredentials
23}
24
25type 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
34func 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
44func (h *credentialHandler) Output() chan *IAMCredentials {
45 return h.output
46}
47
48func (h *credentialHandler) InGoodState() bool {
49 c := <-h.Output()
50 return c.Code == "Success"
51}
52
53func (h *credentialHandler) SetBootstrapCredential(bc *credentials.Credentials) {
54 h.input <- bc
55}
56
57func (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
87func (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
125func (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}
diff --git a/http.go b/http.go
new file mode 100644
index 0000000..93bf545
--- /dev/null
+++ b/http.go
@@ -0,0 +1,124 @@
1package main
2
3import (
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
16const (
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.
24type tcpKeepAliveListener struct {
25 *net.TCPListener
26}
27
28func (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.
41func 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.
48type ContextAwareHandler struct {
49 ctx *appContext
50 handler http.HandlerFunc
51}
52
53func (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.
60type ContextPrintingHandler struct {
61 ctx *appContext
62 field string
63}
64
65func (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
86func 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
93func 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
103func 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
109type SecurityHandler struct {
110 handler http.Handler
111}
112
113func (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}
diff --git a/main.go b/main.go
new file mode 100644
index 0000000..9d12c9f
--- /dev/null
+++ b/main.go
@@ -0,0 +1,372 @@
1package main
2
3import (
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
28var (
29 DEFAULT_VALUES map[string]*string
30)
31
32func 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
44func StringPtr(v string) *string {
45 return &v
46}
47
48type 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
65func (c *appContext) FormatAZ() string {
66 return fmt.Sprintf("%s%s", *c.Region, *c.AvailabilityZone)
67}
68
69func 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
75func AvailabilityZoneHandler(w http.ResponseWriter, r *http.Request) {
76 ctx := getAppCtx(r)
77 fmt.Fprintf(w, ctx.FormatAZ())
78}
79
80func 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
98func StatusHandler(w http.ResponseWriter, r *http.Request) {
99 ctx := getAppCtx(r)
100 writeHTTPJson(w, ctx.CredentialHandler.InGoodState(), "IAMCredentialHandler")
101}
102
103type bootstrapInput struct {
104 AccessKeyId string
105 SecretAccessKey string
106 Token string
107 Signature string
108}
109
110func 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
133func 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
154func 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
167func 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
185func 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
208func 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
242func 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
251type 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
262func 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
298func 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
316func 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 @@
1package main
2
3import (
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
17var (
18 REGION_AZ_REGEXP = regexp.MustCompile("((?:us|ca|eu|ap|sa)-(?:north|south)?(?:east|west)-\\d)([a-f])")
19)
20
21type 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
32func 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
40func 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
46func 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
63func 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
77func 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
91func 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 @@
1package main
2
3import (
4 "errors"
5 "net"
6 "os"
7 "strings"
8
9 jww "github.com/spf13/jwalterweatherman"
10)
11
12type interfaceContext struct {
13 PrimaryIP *net.IP
14 MacAddr *net.HardwareAddr
15 PrimaryHostname *string
16}
17
18func 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
39func 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
65func 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}