From 3199e09cb245a49d22accf0e20b754a07dcafa34 Mon Sep 17 00:00:00 2001 From: Michael Tibben Date: Sat, 18 Apr 2020 23:50:13 +1000 Subject: [PATCH 1/6] Fix naming --- cli/exec.go | 51 ++++++++++++++++-------------------- server/{server.go => ec2.go} | 4 +-- 2 files changed, 25 insertions(+), 30 deletions(-) rename server/{server.go => ec2.go} (96%) diff --git a/cli/exec.go b/cli/exec.go index f54d74b9c..54143c0a0 100644 --- a/cli/exec.go +++ b/cli/exec.go @@ -25,7 +25,7 @@ type ExecCommandInput struct { Command string Args []string Keyring keyring.Keyring - StartServer bool + StartEc2Server bool CredentialHelper bool Config vault.Config SessionDuration time.Duration @@ -63,9 +63,9 @@ func ConfigureExecCommand(app *kingpin.Application) { Short('j'). BoolVar(&input.CredentialHelper) - cmd.Flag("server", "Run the server in the background for credentials"). + cmd.Flag("server", "Run a server in the background for credentials"). Short('s'). - BoolVar(&input.StartServer) + BoolVar(&input.StartEc2Server) cmd.Arg("profile", "Name of the profile"). Required(). @@ -118,10 +118,10 @@ func ExecCommand(input ExecCommandInput) error { return fmt.Errorf("aws-vault sessions should be nested with care, unset $AWS_VAULT to force") } - if input.StartServer && input.CredentialHelper { + if input.StartEc2Server && input.CredentialHelper { return fmt.Errorf("Can't use --server with --json") } - if input.StartServer && input.NoSession { + if input.StartEc2Server && input.NoSession { return fmt.Errorf("Can't use --server with --no-session") } @@ -140,9 +140,10 @@ func ExecCommand(input ExecCommandInput) error { return fmt.Errorf("Error getting temporary credentials: %w", err) } - if input.StartServer { - return execServer(input, config, creds) + if input.StartEc2Server { + return execEc2Server(input, config, creds) } + if input.CredentialHelper { return execCredentialHelper(input, config, creds) } @@ -150,13 +151,7 @@ func ExecCommand(input ExecCommandInput) error { return execEnvironment(input, config, creds) } -func execServer(input ExecCommandInput, config *vault.Config, creds *credentials.Credentials) error { - if err := server.StartLocalServer(creds, config.Region); err != nil { - return fmt.Errorf("Failed to start credential server: %w", err) - } - - env := environ(os.Environ()) - env.Set("AWS_VAULT", input.ProfileName) +func unsetAwsEnvVars(env environ) { env.Unset("AWS_ACCESS_KEY_ID") env.Unset("AWS_SECRET_ACCESS_KEY") env.Unset("AWS_SESSION_TOKEN") @@ -166,6 +161,16 @@ func execServer(input ExecCommandInput, config *vault.Config, creds *credentials env.Unset("AWS_PROFILE") env.Unset("AWS_DEFAULT_REGION") env.Unset("AWS_REGION") +} + +func execEc2Server(input ExecCommandInput, config *vault.Config, creds *credentials.Credentials) error { + if err := server.StartLocalEc2Server(creds, config.Region); err != nil { + return fmt.Errorf("Failed to start credential server: %w", err) + } + + env := environ(os.Environ()) + env.Set("AWS_VAULT", input.ProfileName) + unsetAwsEnvVars(env) return execCmd(input.Command, input.Args, env) } @@ -206,21 +211,11 @@ func execEnvironment(input ExecCommandInput, config *vault.Config, creds *creden env := environ(os.Environ()) env.Set("AWS_VAULT", input.ProfileName) + unsetAwsEnvVars(env) - env.Unset("AWS_ACCESS_KEY_ID") - env.Unset("AWS_SECRET_ACCESS_KEY") - env.Unset("AWS_SESSION_TOKEN") - env.Unset("AWS_SECURITY_TOKEN") - env.Unset("AWS_CREDENTIAL_FILE") - env.Unset("AWS_DEFAULT_PROFILE") - env.Unset("AWS_PROFILE") - env.Unset("AWS_SESSION_EXPIRATION") - - if config.Region != "" { - log.Printf("Setting subprocess env: AWS_DEFAULT_REGION=%s, AWS_REGION=%s", config.Region, config.Region) - env.Set("AWS_DEFAULT_REGION", config.Region) - env.Set("AWS_REGION", config.Region) - } + log.Printf("Setting subprocess env: AWS_DEFAULT_REGION=%s, AWS_REGION=%s", config.Region, config.Region) + env.Set("AWS_DEFAULT_REGION", config.Region) + env.Set("AWS_REGION", config.Region) log.Println("Setting subprocess env: AWS_ACCESS_KEY_ID, AWS_SECRET_ACCESS_KEY") env.Set("AWS_ACCESS_KEY_ID", val.AccessKeyID) diff --git a/server/server.go b/server/ec2.go similarity index 96% rename from server/server.go rename to server/ec2.go index 8c8509bc5..77b62b7d0 100644 --- a/server/server.go +++ b/server/ec2.go @@ -44,8 +44,8 @@ func isServerRunning(bind string) bool { return err == nil } -// StartLocalServer starts a http server to service the EC2 Instance Metadata endpoint -func StartLocalServer(creds *credentials.Credentials, region string) error { +// StartLocalEc2Server starts a http server to service the EC2 Instance Metadata endpoint +func StartLocalEc2Server(creds *credentials.Credentials, region string) error { if !isServerRunning(ec2ServerBind) { if err := StartProxyServerProcess(); err != nil { return err From 6d5ef5d2fd079cb172007c567e5ef5cacfea079e Mon Sep 17 00:00:00 2001 From: Jonathan Stewmon Date: Fri, 24 May 2019 22:15:39 -0500 Subject: [PATCH 2/6] support ECS Credential Provider with `exec --ecs-server` AWS SDKs universally support the ECS Credential Service, which supports reading the service URI from environment variables. Providing an http interface allows the subprocess to refresh credentials as long as the master credentials session is valid. Supporting the ECS Credential provider offers the following advantages over the EC2 Metadata provider: - Binding to a random, ephimeral port - Does not require adminstrator privileges - Allows multiple providers simultaneously for discrete processes - Partially mitigates the security issues that accompany the EC2 Metadata Service because the address is not well-known - Requiring an Authorization token further mitigates the potential for another process to access the credentials, since the Authorization token is only exposed to the subprocess via environment variables --- cli/exec.go | 39 ++++++++++++++++++ server/ecs.go | 110 ++++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 149 insertions(+) create mode 100644 server/ecs.go diff --git a/cli/exec.go b/cli/exec.go index 54143c0a0..827c50699 100644 --- a/cli/exec.go +++ b/cli/exec.go @@ -26,6 +26,7 @@ type ExecCommandInput struct { Args []string Keyring keyring.Keyring StartEc2Server bool + StartEcsServer bool CredentialHelper bool Config vault.Config SessionDuration time.Duration @@ -67,6 +68,14 @@ func ConfigureExecCommand(app *kingpin.Application) { Short('s'). BoolVar(&input.StartEc2Server) + cmd.Flag("ec2-server", "Run a EC2 metadata server in the background for credentials"). + Hidden(). + BoolVar(&input.StartEc2Server) + + cmd.Flag("ecs-server", "Run a ECS credential server in the background for credentials"). + Hidden(). + BoolVar(&input.StartEcsServer) + cmd.Arg("profile", "Name of the profile"). Required(). HintAction(getProfileNames). @@ -118,12 +127,21 @@ func ExecCommand(input ExecCommandInput) error { return fmt.Errorf("aws-vault sessions should be nested with care, unset $AWS_VAULT to force") } + if input.StartEc2Server && input.StartEcsServer { + return fmt.Errorf("Can't use --server with --ecs-server") + } if input.StartEc2Server && input.CredentialHelper { return fmt.Errorf("Can't use --server with --json") } if input.StartEc2Server && input.NoSession { return fmt.Errorf("Can't use --server with --no-session") } + if input.StartEcsServer && input.CredentialHelper { + return fmt.Errorf("Can't use --ecs-server with --json") + } + if input.StartEcsServer && input.NoSession { + return fmt.Errorf("Can't use --ecs-server with --no-session") + } vault.UseSession = !input.NoSession @@ -144,6 +162,10 @@ func ExecCommand(input ExecCommandInput) error { return execEc2Server(input, config, creds) } + if input.StartEcsServer { + return execEcsServer(input, config, creds) + } + if input.CredentialHelper { return execCredentialHelper(input, config, creds) } @@ -175,6 +197,23 @@ func execEc2Server(input ExecCommandInput, config *vault.Config, creds *credenti return execCmd(input.Command, input.Args, env) } +func execEcsServer(input ExecCommandInput, config *vault.Config, creds *credentials.Credentials) error { + ecsServer, err := server.StartEcsCredentialServer(creds) + if err != nil { + return fmt.Errorf("Failed to start credential server: %w", err) + } + + env := environ(os.Environ()) + env.Set("AWS_VAULT", input.ProfileName) + unsetAwsEnvVars(env) + + log.Println("Setting subprocess env AWS_CONTAINER_CREDENTIALS_FULL_URI, AWS_CONTAINER_AUTHORIZATION_TOKEN") + env.Set("AWS_CONTAINER_CREDENTIALS_FULL_URI", ecsServer.Url) + env.Set("AWS_CONTAINER_AUTHORIZATION_TOKEN", ecsServer.Authorization) + + return execCmd(input.Command, input.Args, env) +} + func execCredentialHelper(input ExecCommandInput, config *vault.Config, creds *credentials.Credentials) error { val, err := creds.Get() if err != nil { diff --git a/server/ecs.go b/server/ecs.go new file mode 100644 index 000000000..616728182 --- /dev/null +++ b/server/ecs.go @@ -0,0 +1,110 @@ +package server + +import ( + "crypto/rand" + "encoding/base64" + "encoding/json" + "fmt" + "log" + "net" + "net/http" + + "github.com/aws/aws-sdk-go/aws/credentials" +) + +type EcsCredentialServer struct { + Url string + Authorization string +} + +type EcsCredentialData struct { + AccessKeyID string `json:"AccessKeyId"` + SecretAccessKey string `json:"SecretAccessKey"` + SessionToken string `json:"Token"` + Expiration string `json:"Expiration"` +} + +type EcsCredentialError struct { + Message string `json:"message"` +} + +func StartEcsCredentialServer(creds *credentials.Credentials) (*EcsCredentialServer, error) { + listener, err := net.Listen("tcp", "127.0.0.1:0") + if err != nil { + return nil, err + } + token, err := GenerateToken(16) + if err != nil { + return nil, err + } + srv := &http.Server{Addr: listener.Addr().String()} + + http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { + if r.Header.Get("Authorization") == token { + body, err := getResponse(creds) + if err != nil { + body, err = json.Marshal(&EcsCredentialError{Message: err.Error()}) + if err != nil { + log.Fatalf("Failed to serialize err: %s", err) + } + } + w.Write(body) + } else { + w.WriteHeader(http.StatusForbidden) + body, err := json.Marshal(&EcsCredentialError{Message: "invalid Authorization token"}) + if err != nil { + log.Fatalf("Failed to serialize err: %s", err) + } + w.Write(body) + } + }) + + go func() { + // returns ErrServerClosed on graceful close + if err := srv.Serve(listener); err != http.ErrServerClosed { + log.Fatalf("Serve(): %s", err) + } + }() + + return &EcsCredentialServer{ + Authorization: token, + Url: fmt.Sprintf("http://%s", listener.Addr().String()), + }, nil +} + +func getResponse(creds *credentials.Credentials) ([]byte, error) { + val, err := creds.Get() + if err != nil { + return nil, err + } + + credsExpiresAt, err := creds.ExpiresAt() + if err != nil { + return nil, err + } + + ecsCredential := &EcsCredentialData{ + AccessKeyID: val.AccessKeyID, + SecretAccessKey: val.SecretAccessKey, + SessionToken: val.SessionToken, + Expiration: credsExpiresAt.Format("2006-01-02T15:04:05Z"), + } + serialized, err := json.Marshal(&ecsCredential) + if err != nil { + return nil, err + } + return serialized, nil +} + +func GenerateToken(bytes int) (string, error) { + b, err := GenerateRandomBytes(bytes) + return base64.RawURLEncoding.EncodeToString(b), err +} + +func GenerateRandomBytes(n int) ([]byte, error) { + b := make([]byte, n) + if _, err := rand.Read(b); err != nil { + return nil, err + } + return b, nil +} From c95bb48e74e7c319b9f3cdda456207653530b362 Mon Sep 17 00:00:00 2001 From: Michael Tibben Date: Sun, 19 Apr 2020 01:11:01 +1000 Subject: [PATCH 3/6] Refactor ECS server --- cli/exec.go | 6 +-- server/ecs.go | 117 ++++++++++++++++++++------------------------------ 2 files changed, 50 insertions(+), 73 deletions(-) diff --git a/cli/exec.go b/cli/exec.go index 827c50699..02d80dcc3 100644 --- a/cli/exec.go +++ b/cli/exec.go @@ -198,7 +198,7 @@ func execEc2Server(input ExecCommandInput, config *vault.Config, creds *credenti } func execEcsServer(input ExecCommandInput, config *vault.Config, creds *credentials.Credentials) error { - ecsServer, err := server.StartEcsCredentialServer(creds) + uri, token, err := server.StartEcsCredentialServer(creds) if err != nil { return fmt.Errorf("Failed to start credential server: %w", err) } @@ -208,8 +208,8 @@ func execEcsServer(input ExecCommandInput, config *vault.Config, creds *credenti unsetAwsEnvVars(env) log.Println("Setting subprocess env AWS_CONTAINER_CREDENTIALS_FULL_URI, AWS_CONTAINER_AUTHORIZATION_TOKEN") - env.Set("AWS_CONTAINER_CREDENTIALS_FULL_URI", ecsServer.Url) - env.Set("AWS_CONTAINER_AUTHORIZATION_TOKEN", ecsServer.Authorization) + env.Set("AWS_CONTAINER_CREDENTIALS_FULL_URI", uri) + env.Set("AWS_CONTAINER_AUTHORIZATION_TOKEN", token) return execCmd(input.Command, input.Args, env) } diff --git a/server/ecs.go b/server/ecs.go index 616728182..df8b8fc34 100644 --- a/server/ecs.go +++ b/server/ecs.go @@ -12,99 +12,76 @@ import ( "github.com/aws/aws-sdk-go/aws/credentials" ) -type EcsCredentialServer struct { - Url string - Authorization string -} - -type EcsCredentialData struct { - AccessKeyID string `json:"AccessKeyId"` - SecretAccessKey string `json:"SecretAccessKey"` - SessionToken string `json:"Token"` - Expiration string `json:"Expiration"` +func writeErrorMessage(w http.ResponseWriter, msg string, status int) { + err := json.NewEncoder(w).Encode(map[string]string{"Message": msg}) + if err != nil { + http.Error(w, err.Error(), status) + } } -type EcsCredentialError struct { - Message string `json:"message"` +func withAuthorizationCheck(token string, next http.HandlerFunc) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + if r.Header.Get("Authorization") != token { + writeErrorMessage(w, "invalid Authorization token", http.StatusForbidden) + return + } + next.ServeHTTP(w, r) + } } -func StartEcsCredentialServer(creds *credentials.Credentials) (*EcsCredentialServer, error) { +func StartEcsCredentialServer(creds *credentials.Credentials) (string, string, error) { listener, err := net.Listen("tcp", "127.0.0.1:0") if err != nil { - return nil, err + return "", "", err } - token, err := GenerateToken(16) + token, err := generateRandomString() if err != nil { - return nil, err + return "", "", err } - srv := &http.Server{Addr: listener.Addr().String()} - - http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { - if r.Header.Get("Authorization") == token { - body, err := getResponse(creds) - if err != nil { - body, err = json.Marshal(&EcsCredentialError{Message: err.Error()}) - if err != nil { - log.Fatalf("Failed to serialize err: %s", err) - } - } - w.Write(body) - } else { - w.WriteHeader(http.StatusForbidden) - body, err := json.Marshal(&EcsCredentialError{Message: "invalid Authorization token"}) - if err != nil { - log.Fatalf("Failed to serialize err: %s", err) - } - w.Write(body) - } - }) go func() { + err := http.Serve(listener, withAuthorizationCheck(token, ecsCredsHandler(creds))) // returns ErrServerClosed on graceful close - if err := srv.Serve(listener); err != http.ErrServerClosed { + if err != http.ErrServerClosed { log.Fatalf("Serve(): %s", err) } }() - return &EcsCredentialServer{ - Authorization: token, - Url: fmt.Sprintf("http://%s", listener.Addr().String()), - }, nil + uri := fmt.Sprintf("http://%s", listener.Addr().String()) + return uri, token, nil } -func getResponse(creds *credentials.Credentials) ([]byte, error) { - val, err := creds.Get() - if err != nil { - return nil, err - } +func ecsCredsHandler(creds *credentials.Credentials) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + val, err := creds.Get() + if err != nil { + writeErrorMessage(w, err.Error(), http.StatusInternalServerError) + return + } - credsExpiresAt, err := creds.ExpiresAt() - if err != nil { - return nil, err - } + credsExpiresAt, err := creds.ExpiresAt() + if err != nil { + writeErrorMessage(w, err.Error(), http.StatusInternalServerError) + return + } - ecsCredential := &EcsCredentialData{ - AccessKeyID: val.AccessKeyID, - SecretAccessKey: val.SecretAccessKey, - SessionToken: val.SessionToken, - Expiration: credsExpiresAt.Format("2006-01-02T15:04:05Z"), - } - serialized, err := json.Marshal(&ecsCredential) - if err != nil { - return nil, err + err = json.NewEncoder(w).Encode(map[string]string{ + "AccessKeyId": val.AccessKeyID, + "SecretAccessKey": val.SecretAccessKey, + "Token": val.SessionToken, + "Expiration": credsExpiresAt.Format("2006-01-02T15:04:05Z"), + }) + if err != nil { + writeErrorMessage(w, err.Error(), http.StatusInternalServerError) + return + } } - return serialized, nil -} - -func GenerateToken(bytes int) (string, error) { - b, err := GenerateRandomBytes(bytes) - return base64.RawURLEncoding.EncodeToString(b), err } -func GenerateRandomBytes(n int) ([]byte, error) { - b := make([]byte, n) +func generateRandomString() (string, error) { + b := make([]byte, 30) if _, err := rand.Read(b); err != nil { - return nil, err + return "", err } - return b, nil + return base64.RawURLEncoding.EncodeToString(b), nil } From 0e416cc05638119a1f00e894860b1382fbe39ad3 Mon Sep 17 00:00:00 2001 From: Michael Tibben Date: Sun, 19 Apr 2020 11:10:04 +1000 Subject: [PATCH 4/6] Always set the aws region env vars --- cli/exec.go | 20 +++++++++++--------- 1 file changed, 11 insertions(+), 9 deletions(-) diff --git a/cli/exec.go b/cli/exec.go index 02d80dcc3..65c9e9ea0 100644 --- a/cli/exec.go +++ b/cli/exec.go @@ -173,7 +173,7 @@ func ExecCommand(input ExecCommandInput) error { return execEnvironment(input, config, creds) } -func unsetAwsEnvVars(env environ) { +func updateEnvForAwsVault(env environ, region string, profileName string) { env.Unset("AWS_ACCESS_KEY_ID") env.Unset("AWS_SECRET_ACCESS_KEY") env.Unset("AWS_SESSION_TOKEN") @@ -181,8 +181,13 @@ func unsetAwsEnvVars(env environ) { env.Unset("AWS_CREDENTIAL_FILE") env.Unset("AWS_DEFAULT_PROFILE") env.Unset("AWS_PROFILE") - env.Unset("AWS_DEFAULT_REGION") - env.Unset("AWS_REGION") + env.Unset("AWS_SDK_LOAD_CONFIG") + + env.Set("AWS_VAULT", profileName) + + log.Printf("Setting subprocess env: AWS_DEFAULT_REGION=%s, AWS_REGION=%s", region, region) + env.Set("AWS_DEFAULT_REGION", region) + env.Set("AWS_REGION", region) } func execEc2Server(input ExecCommandInput, config *vault.Config, creds *credentials.Credentials) error { @@ -191,8 +196,7 @@ func execEc2Server(input ExecCommandInput, config *vault.Config, creds *credenti } env := environ(os.Environ()) - env.Set("AWS_VAULT", input.ProfileName) - unsetAwsEnvVars(env) + updateEnvForAwsVault(env, input.ProfileName, config.Region) return execCmd(input.Command, input.Args, env) } @@ -204,8 +208,7 @@ func execEcsServer(input ExecCommandInput, config *vault.Config, creds *credenti } env := environ(os.Environ()) - env.Set("AWS_VAULT", input.ProfileName) - unsetAwsEnvVars(env) + updateEnvForAwsVault(env, input.ProfileName, config.Region) log.Println("Setting subprocess env AWS_CONTAINER_CREDENTIALS_FULL_URI, AWS_CONTAINER_AUTHORIZATION_TOKEN") env.Set("AWS_CONTAINER_CREDENTIALS_FULL_URI", uri) @@ -249,8 +252,7 @@ func execEnvironment(input ExecCommandInput, config *vault.Config, creds *creden } env := environ(os.Environ()) - env.Set("AWS_VAULT", input.ProfileName) - unsetAwsEnvVars(env) + updateEnvForAwsVault(env, input.ProfileName, config.Region) log.Printf("Setting subprocess env: AWS_DEFAULT_REGION=%s, AWS_REGION=%s", config.Region, config.Region) env.Set("AWS_DEFAULT_REGION", config.Region) From cde50d5d3c271448687125d042f6db3715df7e3c Mon Sep 17 00:00:00 2001 From: Michael Tibben Date: Sun, 19 Apr 2020 12:42:24 +1000 Subject: [PATCH 5/6] Set env correctly --- cli/exec.go | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/cli/exec.go b/cli/exec.go index 65c9e9ea0..6fd240941 100644 --- a/cli/exec.go +++ b/cli/exec.go @@ -173,7 +173,7 @@ func ExecCommand(input ExecCommandInput) error { return execEnvironment(input, config, creds) } -func updateEnvForAwsVault(env environ, region string, profileName string) { +func updateEnvForAwsVault(env environ, profileName string, region string) environ { env.Unset("AWS_ACCESS_KEY_ID") env.Unset("AWS_SECRET_ACCESS_KEY") env.Unset("AWS_SESSION_TOKEN") @@ -188,6 +188,8 @@ func updateEnvForAwsVault(env environ, region string, profileName string) { log.Printf("Setting subprocess env: AWS_DEFAULT_REGION=%s, AWS_REGION=%s", region, region) env.Set("AWS_DEFAULT_REGION", region) env.Set("AWS_REGION", region) + + return env } func execEc2Server(input ExecCommandInput, config *vault.Config, creds *credentials.Credentials) error { @@ -196,7 +198,7 @@ func execEc2Server(input ExecCommandInput, config *vault.Config, creds *credenti } env := environ(os.Environ()) - updateEnvForAwsVault(env, input.ProfileName, config.Region) + env = updateEnvForAwsVault(env, input.ProfileName, config.Region) return execCmd(input.Command, input.Args, env) } @@ -208,7 +210,7 @@ func execEcsServer(input ExecCommandInput, config *vault.Config, creds *credenti } env := environ(os.Environ()) - updateEnvForAwsVault(env, input.ProfileName, config.Region) + env = updateEnvForAwsVault(env, input.ProfileName, config.Region) log.Println("Setting subprocess env AWS_CONTAINER_CREDENTIALS_FULL_URI, AWS_CONTAINER_AUTHORIZATION_TOKEN") env.Set("AWS_CONTAINER_CREDENTIALS_FULL_URI", uri) @@ -252,7 +254,7 @@ func execEnvironment(input ExecCommandInput, config *vault.Config, creds *creden } env := environ(os.Environ()) - updateEnvForAwsVault(env, input.ProfileName, config.Region) + env = updateEnvForAwsVault(env, input.ProfileName, config.Region) log.Printf("Setting subprocess env: AWS_DEFAULT_REGION=%s, AWS_REGION=%s", config.Region, config.Region) env.Set("AWS_DEFAULT_REGION", config.Region) From 2d2303d97fb39fa6df2bbc6430c76f5b529b86aa Mon Sep 17 00:00:00 2001 From: Michael Tibben Date: Sun, 19 Apr 2020 13:48:44 +1000 Subject: [PATCH 6/6] More naming fixes and better http logging --- cli/exec.go | 2 +- cli/server.go | 2 +- server/ec2.go | 78 ++++++++++--------- server/{alias_bsd.go => ec2alias_bsd.go} | 2 +- server/{alias_linux.go => ec2alias_linux.go} | 2 +- .../{alias_windows.go => ec2alias_windows.go} | 2 +- .../{proxy_default.go => ec2proxy_default.go} | 8 +- server/{proxy_unix.go => ec2proxy_unix.go} | 4 +- server/ecs.go | 5 +- server/httplog.go | 26 +++++++ 10 files changed, 81 insertions(+), 50 deletions(-) rename server/{alias_bsd.go => ec2alias_bsd.go} (72%) rename server/{alias_linux.go => ec2alias_linux.go} (74%) rename server/{alias_windows.go => ec2alias_windows.go} (90%) rename server/{proxy_default.go => ec2proxy_default.go} (51%) rename server/{proxy_unix.go => ec2proxy_unix.go} (70%) create mode 100644 server/httplog.go diff --git a/cli/exec.go b/cli/exec.go index 6fd240941..d2b6b2df7 100644 --- a/cli/exec.go +++ b/cli/exec.go @@ -193,7 +193,7 @@ func updateEnvForAwsVault(env environ, profileName string, region string) enviro } func execEc2Server(input ExecCommandInput, config *vault.Config, creds *credentials.Credentials) error { - if err := server.StartLocalEc2Server(creds, config.Region); err != nil { + if err := server.StartEc2CredentialsServer(creds, config.Region); err != nil { return fmt.Errorf("Failed to start credential server: %w", err) } diff --git a/cli/server.go b/cli/server.go index 8d836e49b..8a6c6c5bd 100644 --- a/cli/server.go +++ b/cli/server.go @@ -21,7 +21,7 @@ func ConfigureServerCommand(app *kingpin.Application) { } func ServerCommand(app *kingpin.Application, input ServerCommandInput) { - if err := server.StartEc2MetadataProxyServer(); err != nil { + if err := server.StartEc2MetadataEndpointProxy(); err != nil { app.Fatalf("Server failed: %v", err) } } diff --git a/server/ec2.go b/server/ec2.go index 77b62b7d0..50a8e47d8 100644 --- a/server/ec2.go +++ b/server/ec2.go @@ -14,28 +14,28 @@ import ( ) const ( - awsTimeFormat = "2006-01-02T15:04:05Z" - ec2ServerBind = "169.254.169.254:80" - localServerBind = "127.0.0.1:9099" + awsTimeFormat = "2006-01-02T15:04:05Z" + ec2MetadataEndpointAddr = "169.254.169.254:80" + ec2CredentialsServerAddr = "127.0.0.1:9099" ) -// StartEc2MetadataProxyServer starts a proxy server on the standard Ec2 Instance Metadata address -func StartEc2MetadataProxyServer() error { - var localServerURL, err = url.Parse(fmt.Sprintf("http://%s/", localServerBind)) +// StartEc2MetadataEndpointProxy starts a http proxy server on the standard EC2 Instance Metadata endpoint +func StartEc2MetadataEndpointProxy() error { + var localServerURL, err = url.Parse(fmt.Sprintf("http://%s/", ec2CredentialsServerAddr)) if err != nil { log.Fatal(err) } - if _, err := installNetworkAlias(); err != nil { + if _, err := installEc2EndpointNetworkAlias(); err != nil { return err } - l, err := net.Listen("tcp", ec2ServerBind) + l, err := net.Listen("tcp", ec2MetadataEndpointAddr) if err != nil { return err } - log.Printf("EC2 Instance Metadata server running on %s", l.Addr()) + log.Printf("EC2 Instance Metadata endpoint proxy server running on %s", l.Addr()) return http.Serve(l, httputil.NewSingleHostReverseProxy(localServerURL)) } @@ -44,43 +44,49 @@ func isServerRunning(bind string) bool { return err == nil } -// StartLocalEc2Server starts a http server to service the EC2 Instance Metadata endpoint -func StartLocalEc2Server(creds *credentials.Credentials, region string) error { - if !isServerRunning(ec2ServerBind) { - if err := StartProxyServerProcess(); err != nil { +// StartEc2CredentialsServer starts a EC2 Instance Metadata server and endpoint proxy +func StartEc2CredentialsServer(creds *credentials.Credentials, region string) error { + if !isServerRunning(ec2MetadataEndpointAddr) { + if err := StartEc2EndpointProxyServerProcess(); err != nil { return err } } - log.Printf("Starting local credentials server on %s", localServerBind) - go func() { - router := http.NewServeMux() + // pre-fetch credentials so that we can respond quickly to the first request + _, _ = creds.Get() - router.HandleFunc("/latest/meta-data/iam/security-credentials/", func(w http.ResponseWriter, r *http.Request) { - fmt.Fprintf(w, "local-credentials") - }) + go startEc2CredentialsServer(creds, region) - // The AWS Go SDK checks the instance-id endpoint to validate the existence of EC2 Metadata - router.HandleFunc("/latest/meta-data/instance-id/", func(w http.ResponseWriter, r *http.Request) { - fmt.Fprintf(w, "aws-vault") - }) + return nil +} - // The AWS .NET SDK checks this endpoint during obtaining credentials/refreshing them - router.HandleFunc("/latest/meta-data/iam/info/", func(w http.ResponseWriter, r *http.Request) { - fmt.Fprintf(w, `{"Code" : "Success"}`) - }) +func startEc2CredentialsServer(creds *credentials.Credentials, region string) { - // used by AWS SDK to determine region - router.HandleFunc("/latest/meta-data/dynamic/instance-identity/document", func(w http.ResponseWriter, r *http.Request) { - fmt.Fprintf(w, `{"region": "`+region+`"}`) - }) + log.Printf("Starting EC2 Instance Metadata server on %s", ec2CredentialsServerAddr) + router := http.NewServeMux() - router.HandleFunc("/latest/meta-data/iam/security-credentials/local-credentials", credsHandler(creds)) + router.HandleFunc("/latest/meta-data/iam/security-credentials/", func(w http.ResponseWriter, r *http.Request) { + fmt.Fprintf(w, "local-credentials") + }) - log.Fatalln(http.ListenAndServe(localServerBind, withLoopbackSecurityCheck(router))) - }() + // The AWS Go SDK checks the instance-id endpoint to validate the existence of EC2 Metadata + router.HandleFunc("/latest/meta-data/instance-id/", func(w http.ResponseWriter, r *http.Request) { + fmt.Fprintf(w, "aws-vault") + }) - return nil + // The AWS .NET SDK checks this endpoint during obtaining credentials/refreshing them + router.HandleFunc("/latest/meta-data/iam/info/", func(w http.ResponseWriter, r *http.Request) { + fmt.Fprintf(w, `{"Code" : "Success"}`) + }) + + // used by AWS SDK to determine region + router.HandleFunc("/latest/meta-data/dynamic/instance-identity/document", func(w http.ResponseWriter, r *http.Request) { + fmt.Fprintf(w, `{"region": "`+region+`"}`) + }) + + router.HandleFunc("/latest/meta-data/iam/security-credentials/local-credentials", credsHandler(creds)) + + log.Fatalln(http.ListenAndServe(ec2CredentialsServerAddr, logRequest(withLoopbackSecurityCheck(router)))) } // withLoopbackSecurityCheck is middleware to check that the request comes from the loopback device @@ -100,8 +106,6 @@ func withLoopbackSecurityCheck(next *http.ServeMux) http.HandlerFunc { return } - log.Printf("RemoteAddr = %v", r.RemoteAddr) - next.ServeHTTP(w, r) } } diff --git a/server/alias_bsd.go b/server/ec2alias_bsd.go similarity index 72% rename from server/alias_bsd.go rename to server/ec2alias_bsd.go index b514f6ed9..5d6c9870f 100644 --- a/server/alias_bsd.go +++ b/server/ec2alias_bsd.go @@ -4,6 +4,6 @@ package server import "os/exec" -func installNetworkAlias() ([]byte, error) { +func installEc2EndpointNetworkAlias() ([]byte, error) { return exec.Command("ifconfig", "lo0", "alias", "169.254.169.254").CombinedOutput() } diff --git a/server/alias_linux.go b/server/ec2alias_linux.go similarity index 74% rename from server/alias_linux.go rename to server/ec2alias_linux.go index d86a99b7a..a542a6bb8 100644 --- a/server/alias_linux.go +++ b/server/ec2alias_linux.go @@ -4,6 +4,6 @@ package server import "os/exec" -func installNetworkAlias() ([]byte, error) { +func installEc2EndpointNetworkAlias() ([]byte, error) { return exec.Command("ip", "addr", "add", "169.254.169.254/24", "dev", "lo", "label", "lo:0").CombinedOutput() } diff --git a/server/alias_windows.go b/server/ec2alias_windows.go similarity index 90% rename from server/alias_windows.go rename to server/ec2alias_windows.go index 9cdcad339..a57856feb 100644 --- a/server/alias_windows.go +++ b/server/ec2alias_windows.go @@ -8,7 +8,7 @@ import ( "strings" ) -func installNetworkAlias() ([]byte, error) { +func installEc2EndpointNetworkAlias() ([]byte, error) { out, err := exec.Command("netsh", "interface", "ipv4", "add", "address", "Loopback Pseudo-Interface 1", "169.254.169.254", "255.255.0.0").CombinedOutput() if err == nil || strings.Contains(string(out), "The object already exists") { diff --git a/server/proxy_default.go b/server/ec2proxy_default.go similarity index 51% rename from server/proxy_default.go rename to server/ec2proxy_default.go index bfa0d26c0..591f873be 100644 --- a/server/proxy_default.go +++ b/server/ec2proxy_default.go @@ -10,8 +10,8 @@ import ( "time" ) -// StartProxyServerProcess starts a `aws-vault server` process -func StartProxyServerProcess() error { +// StartEc2EndpointProxyServerProcess starts a `aws-vault server` process +func StartEc2EndpointProxyServerProcess() error { log.Println("Starting `aws-vault server` in the background") cmd := exec.Command(os.Args[0], "server") cmd.Stdin = os.Stdin @@ -21,8 +21,8 @@ func StartProxyServerProcess() error { return err } time.Sleep(time.Second * 1) - if !isServerRunning(metadataBind) { - return errors.New("The credential proxy server isn't running. Run aws-vault server as Administrator in the background and then try this command again") + if !isServerRunning(ec2MetadataEndpointAddr) { + return errors.New("The EC2 Instance Metadata endpoint proxy server isn't running. Run `aws-vault server` as Administrator or root in the background and then try this command again") } return nil } diff --git a/server/proxy_unix.go b/server/ec2proxy_unix.go similarity index 70% rename from server/proxy_unix.go rename to server/ec2proxy_unix.go index ed16883a0..853ac8e47 100644 --- a/server/proxy_unix.go +++ b/server/ec2proxy_unix.go @@ -8,8 +8,8 @@ import ( "os/exec" ) -// StartProxyServerProcess starts a `aws-vault server` process -func StartProxyServerProcess() error { +// StartEc2EndpointProxyServerProcess starts a `aws-vault server` process +func StartEc2EndpointProxyServerProcess() error { log.Println("Starting `aws-vault server` as root in the background") cmd := exec.Command("sudo", "-b", os.Args[0], "server") cmd.Stdin = os.Stdin diff --git a/server/ecs.go b/server/ecs.go index df8b8fc34..e915c6e8e 100644 --- a/server/ecs.go +++ b/server/ecs.go @@ -29,6 +29,7 @@ func withAuthorizationCheck(token string, next http.HandlerFunc) http.HandlerFun } } +// StartEcsCredentialServer starts an ECS credential server on a random port func StartEcsCredentialServer(creds *credentials.Credentials) (string, string, error) { listener, err := net.Listen("tcp", "127.0.0.1:0") if err != nil { @@ -40,10 +41,10 @@ func StartEcsCredentialServer(creds *credentials.Credentials) (string, string, e } go func() { - err := http.Serve(listener, withAuthorizationCheck(token, ecsCredsHandler(creds))) + err := http.Serve(listener, logRequest(withAuthorizationCheck(token, ecsCredsHandler(creds)))) // returns ErrServerClosed on graceful close if err != http.ErrServerClosed { - log.Fatalf("Serve(): %s", err) + log.Fatalf("ecs server: %s", err.Error()) } }() diff --git a/server/httplog.go b/server/httplog.go new file mode 100644 index 000000000..1092b1848 --- /dev/null +++ b/server/httplog.go @@ -0,0 +1,26 @@ +package server + +import ( + "log" + "net/http" + "time" +) + +type loggingMiddlewareResponseWriter struct { + http.ResponseWriter + Code int +} + +func (w *loggingMiddlewareResponseWriter) WriteHeader(statusCode int) { + w.Code = statusCode + w.ResponseWriter.WriteHeader(statusCode) +} + +func logRequest(handler http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + requestStart := time.Now() + w2 := &loggingMiddlewareResponseWriter{w, http.StatusOK} + handler.ServeHTTP(w2, r) + log.Printf("http: %s: %d %s %s (%s)", r.RemoteAddr, w2.Code, r.Method, r.URL, time.Since(requestStart)) + }) +}