From 3f8eccef7a91950494199bce2063d6fb7c9a712b Mon Sep 17 00:00:00 2001 From: Mike Crute Date: Sat, 4 Mar 2017 09:23:07 +0800 Subject: Initial Import --- README.md | 115 +++++++++++++++++ credentials.go | 153 ++++++++++++++++++++++ http.go | 124 ++++++++++++++++++ main.go | 372 +++++++++++++++++++++++++++++++++++++++++++++++++++++ metadata-models.go | 98 ++++++++++++++ network.go | 94 ++++++++++++++ 6 files changed, 956 insertions(+) create mode 100644 README.md create mode 100644 credentials.go create mode 100644 http.go create mode 100644 main.go create mode 100644 metadata-models.go create mode 100644 network.go diff --git a/README.md b/README.md new file mode 100644 index 0000000..817e194 --- /dev/null +++ b/README.md @@ -0,0 +1,115 @@ +# Mock Metadata Service + +This software is still heavily a work-in-progress. The IAM functionality should +work but other stuff may not. Bug reports and pull requests welcome. + +This package provides a mock metadata service that returns plausible +responses for most of the metadata service endpoints. It also provides a +full IAM temporary credential endpoint that will assume an IAM role and +continually refresh the credentials as time passes. All AWS SDKs and most AWS +agents are able to work with this interface provided that it is bound to +169.254.169.254 port 80. + +The daemon will attempt to bind two ports, port 80 on IP 169.245.169.254 +provides the mock metadata service that is only available on the instance. +Additionally port 8998 will be bound on all interfaces for the administrative +service. The administrative service is used to boostrap the daemon and provide +health-checking. + +## Setting up Interfaces +A loopback interface with IP address 169.254.169.254 is required by the daemon. +This can be accomplished on Linux with the following command: + +``` +sudo ip addr add 169.254.169.254/24 broadcast 169.254.169.255 dev lo:metadata +sudo ip link set dev lo:metadata up +sudo iptables -I INPUT 1 -d 169.254.0.0/16 ! -i lo -j DROP +``` + +*Note*: Do not bind this address to a publicly accessible interface or anyone +on the network will be able to use your AWS credentials. + +## Startup +On startup the daemon will bind the ports described above and will wait for a +bootstrap credential. At this time it will accept requests for all endpoints +but will return an IAM failure response for the assumed role. Once bootstrapped +it will assume the requested IAM role and begin serving credentials. + +## Health Checking +The daemon provides an HTTP endpoint on the administrative service to provide a +health status. The endpoint is `/status` and will return a JSON boolean (`true` +or `false`) to indicate that the daemon is running with a valid set of assumed +credentials. + +## Bootstrapping +Once the daemon has assumed a role it will continue to re-assume that role +using the credentials provided by the AssumeRole API call. However, initial +credentials are required to bootstrap the role. These credentials only need +permissions to assume the role, all other permissions should be granted to the +role itself. These credentials should be provided to the administrative service +using a POST request with a JSON body. + +The POST endpoint is `/bootstrap/creds` and is write-only. The JSON formatted +message should contain an access key ID, a secret access key and optionally, a +session token. The format is: + +``` +{ + "AccessKeyId": "AK...", + "SecretAccessKey": "...", + "Token": "..." +} +``` + +It is required to omit the token key or set the value to an empty string if no +token is available. + +As soon as the bootstrap token is submitted the daemon will attempt to assume +the role it was started with and will begin allowing clients to reqeuest +credentials. + +## Known Missing Features +Many of these feature either don't make sense outside of AWS or are not +possible to emulate. + +Instance identity document signing. This can not be implemented because only +AWS has the private key. + +``` +/latest/dynamic/instance-identity/signature +/latest/dynamic/instance-identity/pkcs7 +/latest/dynamic/instance-identity/rsa2048 +``` + +Block device mappings. May be available in the future. + +``` +/latest/meta-data/block-device-mapping/ami +/latest/meta-data/block-device-mapping/root +``` + +SSH keys. Will be available in a future release. + +``` +/latest/meta-data/public-keys/ +/latest/meta-data/public-keys/0/openssh-key +``` + +Network interface mapping. May be available in the future. +``` +/latest/meta-data/network/interfaces/macs/ +/latest/meta-data/network/interfaces/macs/{mac}/device-number +/latest/meta-data/network/interfaces/macs/{mac}/interface-id +/latest/meta-data/network/interfaces/macs/{mac}/local-hostname +/latest/meta-data/network/interfaces/macs/{mac}/local-ipv4s +/latest/meta-data/network/interfaces/macs/{mac}/mac +/latest/meta-data/network/interfaces/macs/{mac}/owner-id +/latest/meta-data/network/interfaces/macs/{mac}/security-group-ids +/latest/meta-data/network/interfaces/macs/{mac}/security-groups +/latest/meta-data/network/interfaces/macs/{mac}/subnet-id +/latest/meta-data/network/interfaces/macs/{mac}/subnet-ipv4-cidr-block +/latest/meta-data/network/interfaces/macs/{mac}/vpc-id +/latest/meta-data/network/interfaces/macs/{mac}/vpc-ipv4-cidr-block +/latest/meta-data/network/interfaces/macs/{mac}/vpc-ipv4-cidr-blocks +/latest/meta-data/network/interfaces/macs/{mac}/vpc-ipv6-cidr-blocks +``` diff --git a/credentials.go b/credentials.go new file mode 100644 index 0000000..a9412da --- /dev/null +++ b/credentials.go @@ -0,0 +1,153 @@ +package main + +import ( + "time" + + "github.com/aws/aws-sdk-go/aws" + "github.com/aws/aws-sdk-go/aws/credentials" + "github.com/aws/aws-sdk-go/aws/session" + "github.com/aws/aws-sdk-go/service/sts" + jww "github.com/spf13/jwalterweatherman" +) + +// Try to refresh credentials 3 times an hour but in the worst case if the +// credential refresh fails twice try to get one last refresh in before the end +// of the hour when the credential expires. +const REFRESH_INTERVAL = time.Duration(19) * time.Minute + +type CredentialHandler interface { + Start() + InGoodState() bool + SetBootstrapCredential(*credentials.Credentials) + Output() chan *IAMCredentials +} + +type credentialHandler struct { + region *string + roleARN *string + sessionName *string + bootstrapCreds *credentials.Credentials + output chan *IAMCredentials + input chan *credentials.Credentials +} + +func NewCredentialHandler(region, arn, name *string) CredentialHandler { + return &credentialHandler{ + region: region, + roleARN: arn, + sessionName: name, + output: make(chan *IAMCredentials), + input: make(chan *credentials.Credentials, 1), // 1-item buffer to allow pre-start bootstrapping + } +} + +func (h *credentialHandler) Output() chan *IAMCredentials { + return h.output +} + +func (h *credentialHandler) InGoodState() bool { + c := <-h.Output() + return c.Code == "Success" +} + +func (h *credentialHandler) SetBootstrapCredential(bc *credentials.Credentials) { + h.input <- bc +} + +func (h *credentialHandler) Start() { + c := &IAMCredentials{Code: "Failure"} + updateChan := make(chan *IAMCredentials) + + ticker := time.NewTicker(REFRESH_INTERVAL) + defer ticker.Stop() + + jww.INFO.Printf("Starting credential handler, awaiting bootstrap") + + for { + select { + // Read and update bootstrap credentials + case h.bootstrapCreds = <-h.input: + go h.refreshCredential(nil, updateChan) + // HTTP handler requests credential + case h.output <- c: + // Time to refresh credentials + case <-ticker.C: + go h.refreshCredential(c.rawCredentials, updateChan) + // Updated credentials arrive + case up := <-updateChan: + if up == nil && c.Expiration.After(time.Now()) { + c = &IAMCredentials{Code: "Failure"} + } else { + c = up + } + } + } +} + +func (h *credentialHandler) refreshCredential(creds *credentials.Credentials, out chan *IAMCredentials) { + jww.INFO.Printf("Attempting to obtain credentials") + + if creds == nil && h.bootstrapCreds == nil { + jww.WARN.Printf("No session or bootstrap credentials available") + return + } + + if creds != nil { + jww.DEBUG.Printf("Attempting to use session credentials") + + c, err := h.assumeRole(creds) + if err != nil { + jww.WARN.Printf("Failed to obtain with session credentials: %s", err) + } else { + jww.INFO.Printf("Successfully obtained credentials") + out <- c + return + } + } + + if h.bootstrapCreds != nil { + jww.DEBUG.Printf("Attempting to use bootstrap credentials") + + c, err := h.assumeRole(h.bootstrapCreds) + if err != nil { + jww.WARN.Printf("Failed to obtain with bootstrap credentials: %s", err) + } else { + jww.INFO.Printf("Successfully obtained credentials") + out <- c + return + } + } + + jww.ERROR.Printf("Failed to obtain credentials") + out <- nil +} + +func (h *credentialHandler) assumeRole(creds *credentials.Credentials) (*IAMCredentials, error) { + ses := session.New(&aws.Config{ + Region: h.region, + Credentials: creds, + }) + + assumed, err := sts.New(ses).AssumeRole(&sts.AssumeRoleInput{ + RoleArn: h.roleARN, + RoleSessionName: h.sessionName, + }) + if err != nil { + return nil, err + } + + return &IAMCredentials{ + Code: "Success", + Type: "AWS-HMAC", + AccessKeyId: *assumed.Credentials.AccessKeyId, + SecretAccessKey: *assumed.Credentials.SecretAccessKey, + Token: *assumed.Credentials.SessionToken, + LastUpdated: time.Now().UTC().Round(time.Second), + Expiration: *assumed.Credentials.Expiration, + rawCredentials: credentials.NewStaticCredentials( + *assumed.Credentials.AccessKeyId, + *assumed.Credentials.SecretAccessKey, + *assumed.Credentials.SessionToken, + ), + }, nil +} diff --git a/http.go b/http.go new file mode 100644 index 0000000..93bf545 --- /dev/null +++ b/http.go @@ -0,0 +1,124 @@ +package main + +import ( + "context" + "encoding/json" + "fmt" + "net" + "net/http" + "reflect" + "strings" + "time" + + jww "github.com/spf13/jwalterweatherman" +) + +const ( + APP_CTX_KEY = "app" +) + +// tcpKeepAliveListener sets TCP keep-alive timeouts on accepted +// connections. It's used by ListenAndServe and ListenAndServeTLS so +// dead TCP connections (e.g. closing laptop mid-download) eventually +// go away. +type tcpKeepAliveListener struct { + *net.TCPListener +} + +func (ln tcpKeepAliveListener) Accept() (c net.Conn, err error) { + tc, err := ln.AcceptTCP() + if err != nil { + return + } + tc.SetKeepAlive(true) + tc.SetKeepAlivePeriod(3 * time.Minute) + return tc, nil +} + +// Do the same thing ListenAndServe does but allow passing in the listener +// instead of the address so that we can bind privileged ports before dropping +// permissions to start the server itself. +func ListenAndServeRaw(ln net.Listener, handler http.Handler) error { + server := &http.Server{Addr: ln.Addr().String(), Handler: handler} + return server.Serve(tcpKeepAliveListener{ln.(*net.TCPListener)}) +} + +// Handler that pushes the application context onto the request context stack +// so that it's available to all other handlers in the stack. +type ContextAwareHandler struct { + ctx *appContext + handler http.HandlerFunc +} + +func (h ContextAwareHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { + defaultHeaders(w) + h.handler(w, r.WithContext(context.WithValue(r.Context(), APP_CTX_KEY, h.ctx))) +} + +// Handler that is able to inspect the application context and print out the +// value of specific fields. +type ContextPrintingHandler struct { + ctx *appContext + field string +} + +func (h ContextPrintingHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { + defaultHeaders(w) + + c := reflect.ValueOf(h.ctx) + f := reflect.Indirect(c).FieldByName(h.field) + + switch i := f.Interface().(type) { + case fmt.Stringer: + fmt.Fprintln(w, i.String()) + case *string: + if i == nil { + fmt.Fprintln(w, "") + } else { + fmt.Fprintln(w, *i) + } + default: + fmt.Fprintln(w, i) + } +} + +// Print default headers for JSON data +func defaultHeaders(w http.ResponseWriter) { + w.Header().Set("Content-Type", "text/plain") + w.Header().Set("Server", "EC2ws") + w.Header().Set("Connection", "close") +} + +// Write out JSON data in formatted form or an error +func writeHTTPJson(w http.ResponseWriter, data interface{}, name string) { + jd, err := json.MarshalIndent(data, "", " ") + if err != nil { + jww.ERROR.Printf("Error marshaling json in %s: %s", name, err) + http.Error(w, err.Error(), http.StatusInternalServerError) + } + fmt.Fprintf(w, string(jd)) +} + +// Get the application context from the request context +func getAppCtx(r *http.Request) *appContext { + return r.Context().Value(APP_CTX_KEY).(*appContext) +} + +// Handler that will reject non-local requests in case the daemon gets bound +// incorrectly to a public interface +type SecurityHandler struct { + handler http.Handler +} + +func (h SecurityHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { + // String is ip:port formatted + ip := strings.Split(r.RemoteAddr, ":")[0] + + if ip != "169.254.169.254" && ip != "127.0.0.1" { + jww.ERROR.Printf("Non-local metadata request from %s!", ip) + http.NotFound(w, r) + return + } else { + h.handler.ServeHTTP(w, r) + } +} diff --git a/main.go b/main.go new file mode 100644 index 0000000..9d12c9f --- /dev/null +++ b/main.go @@ -0,0 +1,372 @@ +package main + +import ( + "bytes" + "crypto/hmac" + "crypto/sha256" + "encoding/hex" + "encoding/json" + "errors" + "flag" + "fmt" + "io/ioutil" + "net" + "net/http" + "os" + "os/user" + "strconv" + "time" + + "github.com/aws/aws-sdk-go/aws/credentials" + "github.com/aws/aws-sdk-go/aws/ec2metadata" + "github.com/gorilla/handlers" + "github.com/gorilla/mux" + "github.com/sid77/drop" + jww "github.com/spf13/jwalterweatherman" +) + +var ( + DEFAULT_VALUES map[string]*string +) + +func init() { + DEFAULT_VALUES = map[string]*string{ + "domain": StringPtr("amazonaws.com"), + "partition": StringPtr("aws"), + "ami-launch-index": StringPtr("0"), + "ami-manifest-path": StringPtr("(unknown)"), + "instance-action": StringPtr("none"), + "profile": StringPtr("default-hvm"), + "security-groups": StringPtr("default"), + } +} + +func StringPtr(v string) *string { + return &v +} + +type appContext struct { + PrivateIP *net.IP + Region *string + MacAddr *net.HardwareAddr + AvailabilityZone *string + Hostname *string + InstanceId *string + InstanceType *string + AccountId *int64 + ImageId *string + ReservationId *string + InstanceProfileId *string + RoleARN *string + BootstrapSecret *string + CredentialHandler CredentialHandler +} + +func (c *appContext) FormatAZ() string { + return fmt.Sprintf("%s%s", *c.Region, *c.AvailabilityZone) +} + +func GetKeyHandler(content map[string]*string, key string) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.Write([]byte(*content[key])) + }) +} + +func AvailabilityZoneHandler(w http.ResponseWriter, r *http.Request) { + ctx := getAppCtx(r) + fmt.Fprintf(w, ctx.FormatAZ()) +} + +func IAMCredentialHandler(w http.ResponseWriter, r *http.Request) { + ctx := getAppCtx(r) + vars := mux.Vars(r) + + name, err := parseRoleName(*ctx.RoleARN) + if err != nil { + jww.ERROR.Printf("Error parsing role name in IAMCredentialHandler: %s", err) + http.Error(w, err.Error(), http.StatusInternalServerError) + } + + if vars["profile"] != name { + http.NotFound(w, r) + return + } + + writeHTTPJson(w, <-ctx.CredentialHandler.Output(), "IAMCredentialHandler") +} + +func StatusHandler(w http.ResponseWriter, r *http.Request) { + ctx := getAppCtx(r) + writeHTTPJson(w, ctx.CredentialHandler.InGoodState(), "IAMCredentialHandler") +} + +type bootstrapInput struct { + AccessKeyId string + SecretAccessKey string + Token string + Signature string +} + +func validateSignature(r *bootstrapInput, key *string) bool { + buf := bytes.Buffer{} + // Alphabetical order matters here + buf.WriteString(fmt.Sprintf("AccessKeyId%s", r.AccessKeyId)) + buf.WriteString(fmt.Sprintf("SecretAccessKey%s", r.SecretAccessKey)) + + // Only hash token if it was presented + if r.Token != "" { + buf.WriteString(fmt.Sprintf("Token%s", r.Token)) + } + + mac := hmac.New(sha256.New, []byte(*key)) + mac.Write(buf.Bytes()) + expected := mac.Sum(nil) + + sig, err := hex.DecodeString(r.Signature) + if err != nil { + return false + } + + return hmac.Equal(expected, sig) +} + +func BootstrapCredentialHandler(w http.ResponseWriter, r *http.Request) { + ctx := getAppCtx(r) + d := json.NewDecoder(r.Body) + var cred bootstrapInput + + err := d.Decode(&cred) + if err != nil { + jww.ERROR.Printf("Error decoding bootstrap JSON: %s", err) + http.Error(w, err.Error(), http.StatusInternalServerError) + } + + if !validateSignature(&cred, ctx.BootstrapSecret) { + jww.ERROR.Printf("Invalid signature for bootstrapping credentials") + http.Error(w, "Not allowed", http.StatusForbidden) + return + } + + creds := credentials.NewStaticCredentials(cred.AccessKeyId, cred.SecretAccessKey, cred.Token) + ctx.CredentialHandler.SetBootstrapCredential(creds) +} + +func IAMInfoHandler(w http.ResponseWriter, r *http.Request) { + ctx := getAppCtx(r) + + p := &ec2metadata.EC2IAMInfo{ + Code: "Success", + LastUpdated: time.Now().UTC().Round(time.Second), + InstanceProfileArn: *ctx.RoleARN, + InstanceProfileID: *ctx.InstanceProfileId, + } + + writeHTTPJson(w, p, "IAMInfoHandler") +} + +func InstanceProfileListHandler(w http.ResponseWriter, r *http.Request) { + ctx := getAppCtx(r) + + if !ctx.CredentialHandler.InGoodState() { + jww.ERROR.Printf("Credential handler in a bad state") + fmt.Fprintf(w, "") + return + } + + name, err := parseRoleName(*ctx.RoleARN) + if err != nil { + jww.ERROR.Printf("Error parsing role name in InstanceProfileListHandler: %s", err) + http.Error(w, err.Error(), http.StatusInternalServerError) + } + + fmt.Fprintf(w, name) +} + +func IdentityDocumentHandler(w http.ResponseWriter, r *http.Request) { + ctx := getAppCtx(r) + + id := &ec2metadata.EC2InstanceIdentityDocument{ + PrivateIP: ctx.PrivateIP.String(), + DevpayProductCodes: nil, + AvailabilityZone: ctx.FormatAZ(), + Version: "2010-08-31", + InstanceID: *ctx.InstanceId, + BillingProducts: nil, + InstanceType: *ctx.InstanceType, + ImageID: *ctx.ImageId, + AccountID: strconv.FormatInt(*ctx.AccountId, 10), + Architecture: "x86_64", + KernelID: "", + RamdiskID: "", + PendingTime: time.Now().Round(time.Second), + Region: *ctx.Region, + } + + writeHTTPJson(w, id, "IdentityDocumentHandler") +} + +func buildMetadataHandler(ctx *appContext, defaults map[string]*string) http.Handler { + r := mux.NewRouter() + + // Static Data Handlers + r.Handle("/latest/meta-data/services/domain", GetKeyHandler(defaults, "domain")) + r.Handle("/latest/meta-data/services/partition", GetKeyHandler(defaults, "partition")) + r.Handle("/latest/meta-data/ami-launch-index", GetKeyHandler(defaults, "ami-launch-index")) + r.Handle("/latest/meta-data/ami-manifest-path", GetKeyHandler(defaults, "ami-manifest-path")) + r.Handle("/latest/meta-data/instance-action", GetKeyHandler(defaults, "instance-action")) + r.Handle("/latest/meta-data/profile", GetKeyHandler(defaults, "profile")) + r.Handle("/latest/meta-data/security-groups", GetKeyHandler(defaults, "security-groups")) + + // Machine-specific Pseudo-static Handlers + r.Handle("/latest/meta-data/mac", ContextPrintingHandler{ctx, "MacAddr"}) + r.Handle("/latest/meta-data/hostname", ContextPrintingHandler{ctx, "Hostname"}) + r.Handle("/latest/meta-data/local-hostname", ContextPrintingHandler{ctx, "Hostname"}) + r.Handle("/latest/meta-data/local-ipv4", ContextPrintingHandler{ctx, "PrivateIP"}) + r.Handle("/latest/meta-data/instance-id", ContextPrintingHandler{ctx, "InstanceId"}) + r.Handle("/latest/meta-data/instance-type", ContextPrintingHandler{ctx, "InstanceType"}) + r.Handle("/latest/meta-data/ami-id", ContextPrintingHandler{ctx, "ImageId"}) + r.Handle("/latest/meta-data/reservation-id", ContextPrintingHandler{ctx, "ReservationId"}) + + // Context-specific Handlers + r.Handle("/latest/meta-data/placement/availability-zone", ContextAwareHandler{ctx, AvailabilityZoneHandler}) + r.Handle("/latest/dynamic/instance-identity/document", ContextAwareHandler{ctx, IdentityDocumentHandler}) + + // IAM Credential Handlers + r.Handle("/latest/meta-data/iam/info", ContextAwareHandler{ctx, IAMInfoHandler}) + r.Handle("/latest/meta-data/iam/security-credentials/", ContextAwareHandler{ctx, InstanceProfileListHandler}) + r.Handle("/latest/meta-data/iam/security-credentials/{profile}", ContextAwareHandler{ctx, IAMCredentialHandler}) + + return handlers.LoggingHandler(os.Stdout, SecurityHandler{r}) +} + +func buildAdminHandler(ctx *appContext) http.Handler { + r := mux.NewRouter() + + r.Handle("/bootstrap/creds", ContextAwareHandler{ctx, BootstrapCredentialHandler}).Methods("POST") + r.Handle("/status", ContextAwareHandler{ctx, StatusHandler}).Methods("GET") + + return handlers.LoggingHandler(os.Stdout, r) +} + +type UserArgs struct { + regionAZ string + InstanceType string + Region string + AvailabilityZone string + RoleARN string + User string + AccountNumber int64 + BootstrapSecret string +} + +func parseArgs() (*UserArgs, error) { + a := &UserArgs{} + + flag.StringVar(&a.regionAZ, "region", "us-west-2a", "region and availability zone") + flag.StringVar(&a.InstanceType, "instance", "t2.micro", "instance type") + flag.StringVar(&a.RoleARN, "arn", "", "ARN of role to assume") + flag.StringVar(&a.User, "user", "nobody", "run-as user after port binding") + flag.StringVar(&a.BootstrapSecret, "secret", "", "bootstrap signing secret") + + flag.Parse() + + _, err := user.Lookup(a.User) + if err != nil { + return nil, err + } + + region, az, err := parseRegionAZ(a.regionAZ) + if err != nil { + return nil, err + } + a.Region = region + a.AvailabilityZone = az + + act, err := parseAccountFromARN(a.RoleARN) + if err != nil { + return nil, err + } + a.AccountNumber = act + + if a.BootstrapSecret == "" { + return nil, errors.New("Bootstrap secret must be set") + } + + return a, nil +} + +func initialBootstrap(file string, handler CredentialHandler) { + bd, err := ioutil.ReadFile(file) + if err != nil { + jww.ERROR.Printf("Error reading bootstrap file: %s", err.Error()) + return + } + + var cred bootstrapInput + err = json.Unmarshal(bd, &cred) + if err != nil { + jww.ERROR.Printf("Error decoding bootstrap JSON: %s", err) + return + } + + creds := credentials.NewStaticCredentials(cred.AccessKeyId, cred.SecretAccessKey, cred.Token) + handler.SetBootstrapCredential(creds) +} + +func main() { + jww.SetStdoutThreshold(jww.LevelInfo) + + args, err := parseArgs() + if err != nil { + fmt.Println(err.Error()) + return + } + + // Bind this port first because it requires privilges then drop the + // privileges before we do anything else + metalistener, err := net.Listen("tcp", "169.254.169.254:80") + if err != nil { + jww.FATAL.Printf("Error setting up listener: %s", err.Error()) + return + } + + if err := drop.DropPrivileges(args.User); err != nil { + jww.FATAL.Printf("Unable to drop privileges") + return + } + + iface, err := getInterfaceContext() + if err != nil { + jww.FATAL.Printf("Error getting network interface info: %s", err) + return + } + + credHandler := NewCredentialHandler( + &args.Region, + &args.RoleARN, + iface.PrimaryHostname, + ) + + ctx := &appContext{ + PrivateIP: iface.PrimaryIP, + Region: &args.Region, + MacAddr: iface.MacAddr, + AvailabilityZone: &args.AvailabilityZone, + Hostname: iface.PrimaryHostname, + InstanceId: generateInstanceId(iface.PrimaryHostname), + InstanceType: &args.InstanceType, + AccountId: &args.AccountNumber, + ReservationId: generatePlausibleId("r"), + ImageId: generatePlausibleId("ami"), + InstanceProfileId: generatePlausibleProfileId(), + RoleARN: &args.RoleARN, + BootstrapSecret: &args.BootstrapSecret, + CredentialHandler: credHandler, + } + + initialBootstrap("/etc/mmds/bootstrap-cred.json", credHandler) + + go credHandler.Start() + go http.ListenAndServe(":8000", buildAdminHandler(ctx)) + ListenAndServeRaw(metalistener, buildMetadataHandler(ctx, DEFAULT_VALUES)) +} 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 @@ +package main + +import ( + "crypto/rand" + "crypto/sha1" + "encoding/hex" + "errors" + "fmt" + "regexp" + "strconv" + "strings" + "time" + + "github.com/aws/aws-sdk-go/aws/credentials" +) + +var ( + REGION_AZ_REGEXP = regexp.MustCompile("((?:us|ca|eu|ap|sa)-(?:north|south)?(?:east|west)-\\d)([a-f])") +) + +type IAMCredentials struct { + Code string + LastUpdated time.Time + Type string + AccessKeyId string + SecretAccessKey string + Token string + Expiration time.Time + rawCredentials *credentials.Credentials +} + +func generatePlausibleId(prefix string) *string { + b := make([]byte, 10) + rand.Read(b) + h := sha1.New().Sum(b) + o := fmt.Sprintf("%s-%s", prefix, hex.EncodeToString(h)[0:17]) + return &o +} + +func generateInstanceId(hostname *string) *string { + h := sha1.New().Sum([]byte(*hostname)) + o := fmt.Sprintf("i-%s", hex.EncodeToString(h)[0:17]) + return &o +} + +func generatePlausibleProfileId() *string { + b := make([]byte, 16) + rand.Read(b) + + for i, bb := range b { + if bb%3 == 0 { + b[i] = bb%10 + 48 + } else { + b[i] = bb%26 + 65 + } + } + + res := fmt.Sprintf("AIPAI%s", string(b)) + + return &res +} + +func parseAccountFromARN(arn string) (int64, error) { + parts := strings.Split(arn, ":") + if len(parts) != 6 { + return 0, errors.New("Invalid ARN format") + } + + id, err := strconv.ParseInt(parts[4], 10, 64) + if err != nil { + return 0, err + } + + return id, nil +} + +func parseRoleName(arn string) (string, error) { + parts := strings.Split(arn, ":") + if len(parts) != 6 { + return "", errors.New("Invalid ARN format") + } + + role := strings.Split(parts[5], "/") + if len(role) != 2 { + return "", errors.New("Invalid role name format") + } + + return role[1], nil +} + +func parseRegionAZ(in string) (string, string, error) { + match := REGION_AZ_REGEXP.FindAllStringSubmatch("us-west-2a", -1) + if match == nil || len(match) == 0 { + return "", "", errors.New("Unable to parse region/AZ") + } + + return match[0][1], match[0][2], nil +} diff --git a/network.go b/network.go new file mode 100644 index 0000000..d78a57d --- /dev/null +++ b/network.go @@ -0,0 +1,94 @@ +package main + +import ( + "errors" + "net" + "os" + "strings" + + jww "github.com/spf13/jwalterweatherman" +) + +type interfaceContext struct { + PrimaryIP *net.IP + MacAddr *net.HardwareAddr + PrimaryHostname *string +} + +func interfaceIsUsable(i *net.Interface) bool { + if i.Flags&net.FlagLoopback != 0 { + return false + } + + if i.Flags&net.FlagUp == 0 { + return false + } + + addrs, err := i.Addrs() + if err != nil || len(addrs) == 0 { + return false + } + + if strings.HasPrefix(i.Name, "docker") { + return false + } + + return true +} + +func getPrimaryInterface() (*net.Interface, error) { + ifaces, err := net.Interfaces() + if err != nil { + return nil, err + } + + if len(ifaces) == 0 { + return nil, errors.New("no interfaces found") + } + + var iface net.Interface + for _, iface = range ifaces { + if !interfaceIsUsable(&iface) { + continue + } else { + break + } + } + + if interfaceIsUsable(&iface) { + return &iface, nil + } else { + return nil, errors.New("no usable interfaces found") + } +} + +func getInterfaceContext() (*interfaceContext, error) { + iface, err := getPrimaryInterface() + if err != nil { + return nil, err + } + + // Validated to have at least this in interfaceIsUsable + addrs, err := iface.Addrs() + if err != nil { + return nil, err + } + + primaryAddr := addrs[0].String() + primaryIP, _, err := net.ParseCIDR(primaryAddr) + if err != nil { + return nil, err + } + + name, err := os.Hostname() + if err != nil { + jww.ERROR.Printf("Unable to resolve hostname, using localhost: %s", err.Error()) + name = "localhost" + } + + return &interfaceContext{ + PrimaryIP: &primaryIP, + MacAddr: &iface.HardwareAddr, + PrimaryHostname: &name, + }, nil +} -- cgit v1.2.3