diff options
Diffstat (limited to 'main.go')
-rw-r--r-- | main.go | 372 |
1 files changed, 372 insertions, 0 deletions
@@ -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 | } | ||