From f1f602cdf63688cd93074e12fb4552b3b8071ac6 Mon Sep 17 00:00:00 2001 From: Colin Hebert Date: Sun, 13 Dec 2015 09:58:19 +1100 Subject: [PATCH] aws: Enable account ID check for assumed roles + EC2 instances --- builtin/providers/aws/auth_helpers.go | 134 ++++ builtin/providers/aws/auth_helpers_test.go | 757 ++++++++++++++++++ builtin/providers/aws/config.go | 90 +-- builtin/providers/aws/config_test.go | 376 --------- .../docs/providers/aws/index.html.markdown | 39 +- 5 files changed, 939 insertions(+), 457 deletions(-) create mode 100644 builtin/providers/aws/auth_helpers.go create mode 100644 builtin/providers/aws/auth_helpers_test.go delete mode 100644 builtin/providers/aws/config_test.go diff --git a/builtin/providers/aws/auth_helpers.go b/builtin/providers/aws/auth_helpers.go new file mode 100644 index 000000000..914c7e971 --- /dev/null +++ b/builtin/providers/aws/auth_helpers.go @@ -0,0 +1,134 @@ +package aws + +import ( + "fmt" + "log" + "os" + "strings" + "time" + + "github.com/aws/aws-sdk-go/aws" + "github.com/aws/aws-sdk-go/aws/awserr" + awsCredentials "github.com/aws/aws-sdk-go/aws/credentials" + "github.com/aws/aws-sdk-go/aws/credentials/ec2rolecreds" + "github.com/aws/aws-sdk-go/aws/ec2metadata" + "github.com/aws/aws-sdk-go/aws/session" + "github.com/aws/aws-sdk-go/service/iam" + "github.com/hashicorp/go-cleanhttp" +) + +func GetAccountId(iamconn *iam.IAM, authProviderName string) (string, error) { + // If we have creds from instance profile, we can use metadata API + if authProviderName == ec2rolecreds.ProviderName { + log.Println("[DEBUG] Trying to get account ID via AWS Metadata API") + + cfg := &aws.Config{} + setOptionalEndpoint(cfg) + metadataClient := ec2metadata.New(session.New(cfg)) + info, err := metadataClient.IAMInfo() + if err != nil { + // This can be triggered when no IAM Role is assigned + // or AWS just happens to return invalid response + return "", fmt.Errorf("Failed getting EC2 IAM info: %s", err) + } + + return parseAccountIdFromArn(info.InstanceProfileArn) + } + + // Then try IAM GetUser + log.Println("[DEBUG] Trying to get account ID via iam:GetUser") + outUser, err := iamconn.GetUser(nil) + if err == nil { + return parseAccountIdFromArn(*outUser.User.Arn) + } + + // Then try IAM ListRoles + awsErr, ok := err.(awserr.Error) + // AccessDenied and ValidationError can be raised + // if credentials belong to federated profile, so we ignore these + if !ok || (awsErr.Code() != "AccessDenied" && awsErr.Code() != "ValidationError") { + return "", fmt.Errorf("Failed getting account ID via 'iam:GetUser': %s", err) + } + + log.Printf("[DEBUG] Getting account ID via iam:GetUser failed: %s", err) + log.Println("[DEBUG] Trying to get account ID via iam:ListRoles instead") + outRoles, err := iamconn.ListRoles(&iam.ListRolesInput{ + MaxItems: aws.Int64(int64(1)), + }) + if err != nil { + return "", fmt.Errorf("Failed getting account ID via 'iam:ListRoles': %s", err) + } + + if len(outRoles.Roles) < 1 { + return "", fmt.Errorf("Failed getting account ID via 'iam:ListRoles': No roles available") + } + + return parseAccountIdFromArn(*outRoles.Roles[0].Arn) +} + +func parseAccountIdFromArn(arn string) (string, error) { + parts := strings.Split(arn, ":") + if len(parts) < 5 { + return "", fmt.Errorf("Unable to parse ID from invalid ARN: %q", arn) + } + return parts[4], nil +} + +// This function is responsible for reading credentials from the +// environment in the case that they're not explicitly specified +// in the Terraform configuration. +func GetCredentials(key, secret, token, profile, credsfile string) *awsCredentials.Credentials { + // build a chain provider, lazy-evaulated by aws-sdk + providers := []awsCredentials.Provider{ + &awsCredentials.StaticProvider{Value: awsCredentials.Value{ + AccessKeyID: key, + SecretAccessKey: secret, + SessionToken: token, + }}, + &awsCredentials.EnvProvider{}, + &awsCredentials.SharedCredentialsProvider{ + Filename: credsfile, + Profile: profile, + }, + } + + // Build isolated HTTP client to avoid issues with globally-shared settings + client := cleanhttp.DefaultClient() + + // Keep the timeout low as we don't want to wait in non-EC2 environments + client.Timeout = 100 * time.Millisecond + cfg := &aws.Config{ + HTTPClient: client, + } + usedEndpoint := setOptionalEndpoint(cfg) + + // Real AWS should reply to a simple metadata request. + // We check it actually does to ensure something else didn't just + // happen to be listening on the same IP:Port + metadataClient := ec2metadata.New(session.New(cfg)) + if metadataClient.Available() { + providers = append(providers, &ec2rolecreds.EC2RoleProvider{ + Client: metadataClient, + }) + log.Printf("[INFO] AWS EC2 instance detected via default metadata" + + " API endpoint, EC2RoleProvider added to the auth chain") + } else { + if usedEndpoint == "" { + usedEndpoint = "default location" + } + log.Printf("[WARN] Ignoring AWS metadata API endpoint at %s "+ + "as it doesn't return any instance-id", usedEndpoint) + } + + return awsCredentials.NewChainCredentials(providers) +} + +func setOptionalEndpoint(cfg *aws.Config) string { + endpoint := os.Getenv("AWS_METADATA_URL") + if endpoint != "" { + log.Printf("[INFO] Setting custom metadata endpoint: %q", endpoint) + cfg.Endpoint = aws.String(endpoint) + return endpoint + } + return "" +} diff --git a/builtin/providers/aws/auth_helpers_test.go b/builtin/providers/aws/auth_helpers_test.go new file mode 100644 index 000000000..b3f134039 --- /dev/null +++ b/builtin/providers/aws/auth_helpers_test.go @@ -0,0 +1,757 @@ +package aws + +import ( + "bytes" + "encoding/json" + "fmt" + "io/ioutil" + "log" + "net/http" + "net/http/httptest" + "os" + "testing" + "time" + + "github.com/aws/aws-sdk-go/aws" + "github.com/aws/aws-sdk-go/aws/awserr" + awsCredentials "github.com/aws/aws-sdk-go/aws/credentials" + "github.com/aws/aws-sdk-go/aws/credentials/ec2rolecreds" + "github.com/aws/aws-sdk-go/aws/session" + "github.com/aws/aws-sdk-go/service/iam" +) + +func TestAWSGetAccountId_shouldBeValid_fromEC2Role(t *testing.T) { + resetEnv := unsetEnv(t) + defer resetEnv() + // capture the test server's close method, to call after the test returns + awsTs := awsEnv(t) + defer awsTs() + + iamEndpoints := []*iamEndpoint{} + ts, iamConn := getMockedAwsIamApi(iamEndpoints) + defer ts() + + id, err := GetAccountId(iamConn, ec2rolecreds.ProviderName) + if err != nil { + t.Fatalf("Getting account ID from EC2 metadata API failed: %s", err) + } + + expectedAccountId := "123456789013" + if id != expectedAccountId { + t.Fatalf("Expected account ID: %s, given: %s", expectedAccountId, id) + } +} + +func TestAWSGetAccountId_shouldBeValid_EC2RoleHasPriority(t *testing.T) { + resetEnv := unsetEnv(t) + defer resetEnv() + // capture the test server's close method, to call after the test returns + awsTs := awsEnv(t) + defer awsTs() + + iamEndpoints := []*iamEndpoint{ + &iamEndpoint{ + Request: &iamRequest{"POST", "/", "Action=GetUser&Version=2010-05-08"}, + Response: &iamResponse{200, iamResponse_GetUser_valid, "text/xml"}, + }, + } + ts, iamConn := getMockedAwsIamApi(iamEndpoints) + defer ts() + + id, err := GetAccountId(iamConn, ec2rolecreds.ProviderName) + if err != nil { + t.Fatalf("Getting account ID from EC2 metadata API failed: %s", err) + } + + expectedAccountId := "123456789013" + if id != expectedAccountId { + t.Fatalf("Expected account ID: %s, given: %s", expectedAccountId, id) + } +} + +func TestAWSGetAccountId_shouldBeValid_fromIamUser(t *testing.T) { + iamEndpoints := []*iamEndpoint{ + &iamEndpoint{ + Request: &iamRequest{"POST", "/", "Action=GetUser&Version=2010-05-08"}, + Response: &iamResponse{200, iamResponse_GetUser_valid, "text/xml"}, + }, + } + ts, iamConn := getMockedAwsIamApi(iamEndpoints) + defer ts() + + id, err := GetAccountId(iamConn, "") + if err != nil { + t.Fatalf("Getting account ID via GetUser failed: %s", err) + } + + expectedAccountId := "123456789012" + if id != expectedAccountId { + t.Fatalf("Expected account ID: %s, given: %s", expectedAccountId, id) + } +} + +func TestAWSGetAccountId_shouldBeValid_fromIamListRoles(t *testing.T) { + iamEndpoints := []*iamEndpoint{ + &iamEndpoint{ + Request: &iamRequest{"POST", "/", "Action=GetUser&Version=2010-05-08"}, + Response: &iamResponse{403, iamResponse_GetUser_unauthorized, "text/xml"}, + }, + &iamEndpoint{ + Request: &iamRequest{"POST", "/", "Action=ListRoles&MaxItems=1&Version=2010-05-08"}, + Response: &iamResponse{200, iamResponse_ListRoles_valid, "text/xml"}, + }, + } + ts, iamConn := getMockedAwsIamApi(iamEndpoints) + defer ts() + + id, err := GetAccountId(iamConn, "") + if err != nil { + t.Fatalf("Getting account ID via ListRoles failed: %s", err) + } + + expectedAccountId := "123456789012" + if id != expectedAccountId { + t.Fatalf("Expected account ID: %s, given: %s", expectedAccountId, id) + } +} + +func TestAWSGetAccountId_shouldBeValid_federatedRole(t *testing.T) { + iamEndpoints := []*iamEndpoint{ + &iamEndpoint{ + Request: &iamRequest{"POST", "/", "Action=GetUser&Version=2010-05-08"}, + Response: &iamResponse{400, iamResponse_GetUser_federatedFailure, "text/xml"}, + }, + &iamEndpoint{ + Request: &iamRequest{"POST", "/", "Action=ListRoles&MaxItems=1&Version=2010-05-08"}, + Response: &iamResponse{200, iamResponse_ListRoles_valid, "text/xml"}, + }, + } + ts, iamConn := getMockedAwsIamApi(iamEndpoints) + defer ts() + + id, err := GetAccountId(iamConn, "") + if err != nil { + t.Fatalf("Getting account ID via ListRoles failed: %s", err) + } + + expectedAccountId := "123456789012" + if id != expectedAccountId { + t.Fatalf("Expected account ID: %s, given: %s", expectedAccountId, id) + } +} + +func TestAWSGetAccountId_shouldError_unauthorizedFromIam(t *testing.T) { + iamEndpoints := []*iamEndpoint{ + &iamEndpoint{ + Request: &iamRequest{"POST", "/", "Action=GetUser&Version=2010-05-08"}, + Response: &iamResponse{403, iamResponse_GetUser_unauthorized, "text/xml"}, + }, + &iamEndpoint{ + Request: &iamRequest{"POST", "/", "Action=ListRoles&MaxItems=1&Version=2010-05-08"}, + Response: &iamResponse{403, iamResponse_ListRoles_unauthorized, "text/xml"}, + }, + } + ts, iamConn := getMockedAwsIamApi(iamEndpoints) + defer ts() + + id, err := GetAccountId(iamConn, "") + if err == nil { + t.Fatal("Expected error when getting account ID") + } + + if id != "" { + t.Fatalf("Expected no account ID, given: %s", id) + } +} + +func TestAWSParseAccountIdFromArn(t *testing.T) { + validArn := "arn:aws:iam::101636750127:instance-profile/aws-elasticbeanstalk-ec2-role" + expectedId := "101636750127" + id, err := parseAccountIdFromArn(validArn) + if err != nil { + t.Fatalf("Expected no error when parsing valid ARN: %s", err) + } + if id != expectedId { + t.Fatalf("Parsed id doesn't match with expected (%q != %q)", id, expectedId) + } + + invalidArn := "blablah" + id, err = parseAccountIdFromArn(invalidArn) + if err == nil { + t.Fatalf("Expected error when parsing invalid ARN (%q)", invalidArn) + } +} + +func TestAWSGetCredentials_shouldError(t *testing.T) { + resetEnv := unsetEnv(t) + defer resetEnv() + cfg := Config{} + + c := GetCredentials(cfg.AccessKey, cfg.SecretKey, cfg.Token, cfg.Profile, cfg.CredsFilename) + _, err := c.Get() + if awsErr, ok := err.(awserr.Error); ok { + if awsErr.Code() != "NoCredentialProviders" { + t.Fatalf("Expected NoCredentialProviders error") + } + } + if err == nil { + t.Fatalf("Expected an error with empty env, keys, and IAM in AWS Config") + } +} + +func TestAWSGetCredentials_shouldBeStatic(t *testing.T) { + simple := []struct { + Key, Secret, Token string + }{ + { + Key: "test", + Secret: "secret", + }, { + Key: "test", + Secret: "test", + Token: "test", + }, + } + + for _, c := range simple { + cfg := Config{ + AccessKey: c.Key, + SecretKey: c.Secret, + Token: c.Token, + } + + creds := GetCredentials(cfg.AccessKey, cfg.SecretKey, cfg.Token, cfg.Profile, cfg.CredsFilename) + if creds == nil { + t.Fatalf("Expected a static creds provider to be returned") + } + v, err := creds.Get() + if err != nil { + t.Fatalf("Error gettings creds: %s", err) + } + if v.AccessKeyID != c.Key { + t.Fatalf("AccessKeyID mismatch, expected: (%s), got (%s)", c.Key, v.AccessKeyID) + } + if v.SecretAccessKey != c.Secret { + t.Fatalf("SecretAccessKey mismatch, expected: (%s), got (%s)", c.Secret, v.SecretAccessKey) + } + if v.SessionToken != c.Token { + t.Fatalf("SessionToken mismatch, expected: (%s), got (%s)", c.Token, v.SessionToken) + } + } +} + +// TestAWSGetCredentials_shouldIAM is designed to test the scenario of running Terraform +// from an EC2 instance, without environment variables or manually supplied +// credentials. +func TestAWSGetCredentials_shouldIAM(t *testing.T) { + // clear AWS_* environment variables + resetEnv := unsetEnv(t) + defer resetEnv() + + // capture the test server's close method, to call after the test returns + ts := awsEnv(t) + defer ts() + + // An empty config, no key supplied + cfg := Config{} + + creds := GetCredentials(cfg.AccessKey, cfg.SecretKey, cfg.Token, cfg.Profile, cfg.CredsFilename) + if creds == nil { + t.Fatalf("Expected a static creds provider to be returned") + } + + v, err := creds.Get() + if err != nil { + t.Fatalf("Error gettings creds: %s", err) + } + if v.AccessKeyID != "somekey" { + t.Fatalf("AccessKeyID mismatch, expected: (somekey), got (%s)", v.AccessKeyID) + } + if v.SecretAccessKey != "somesecret" { + t.Fatalf("SecretAccessKey mismatch, expected: (somesecret), got (%s)", v.SecretAccessKey) + } + if v.SessionToken != "sometoken" { + t.Fatalf("SessionToken mismatch, expected: (sometoken), got (%s)", v.SessionToken) + } +} + +// TestAWSGetCredentials_shouldIAM is designed to test the scenario of running Terraform +// from an EC2 instance, without environment variables or manually supplied +// credentials. +func TestAWSGetCredentials_shouldIgnoreIAM(t *testing.T) { + resetEnv := unsetEnv(t) + defer resetEnv() + // capture the test server's close method, to call after the test returns + ts := awsEnv(t) + defer ts() + simple := []struct { + Key, Secret, Token string + }{ + { + Key: "test", + Secret: "secret", + }, { + Key: "test", + Secret: "test", + Token: "test", + }, + } + + for _, c := range simple { + cfg := Config{ + AccessKey: c.Key, + SecretKey: c.Secret, + Token: c.Token, + } + + creds := GetCredentials(cfg.AccessKey, cfg.SecretKey, cfg.Token, cfg.Profile, cfg.CredsFilename) + if creds == nil { + t.Fatalf("Expected a static creds provider to be returned") + } + v, err := creds.Get() + if err != nil { + t.Fatalf("Error gettings creds: %s", err) + } + if v.AccessKeyID != c.Key { + t.Fatalf("AccessKeyID mismatch, expected: (%s), got (%s)", c.Key, v.AccessKeyID) + } + if v.SecretAccessKey != c.Secret { + t.Fatalf("SecretAccessKey mismatch, expected: (%s), got (%s)", c.Secret, v.SecretAccessKey) + } + if v.SessionToken != c.Token { + t.Fatalf("SessionToken mismatch, expected: (%s), got (%s)", c.Token, v.SessionToken) + } + } +} + +func TestAWSGetCredentials_shouldErrorWithInvalidEndpoint(t *testing.T) { + resetEnv := unsetEnv(t) + defer resetEnv() + // capture the test server's close method, to call after the test returns + ts := invalidAwsEnv(t) + defer ts() + + creds := GetCredentials("", "", "", "", "") + v, err := creds.Get() + if err == nil { + t.Fatal("Expected error returned when getting creds w/ invalid EC2 endpoint") + } + + if v.ProviderName != "" { + t.Fatalf("Expected provider name to be empty, %q given", v.ProviderName) + } +} + +func TestAWSGetCredentials_shouldIgnoreInvalidEndpoint(t *testing.T) { + resetEnv := unsetEnv(t) + defer resetEnv() + // capture the test server's close method, to call after the test returns + ts := invalidAwsEnv(t) + defer ts() + + creds := GetCredentials("accessKey", "secretKey", "", "", "") + v, err := creds.Get() + if err != nil { + t.Fatalf("Getting static credentials w/ invalid EC2 endpoint failed: %s", err) + } + + if v.ProviderName != "StaticProvider" { + t.Fatalf("Expected provider name to be %q, %q given", "StaticProvider", v.ProviderName) + } + + if v.AccessKeyID != "accessKey" { + t.Fatalf("Static Access Key %q doesn't match: %s", "accessKey", v.AccessKeyID) + } + + if v.SecretAccessKey != "secretKey" { + t.Fatalf("Static Secret Key %q doesn't match: %s", "secretKey", v.SecretAccessKey) + } +} + +func TestAWSGetCredentials_shouldCatchEC2RoleProvider(t *testing.T) { + resetEnv := unsetEnv(t) + defer resetEnv() + // capture the test server's close method, to call after the test returns + ts := awsEnv(t) + defer ts() + + creds := GetCredentials("", "", "", "", "") + if creds == nil { + t.Fatalf("Expected an EC2Role creds provider to be returned") + } + v, err := creds.Get() + if err != nil { + t.Fatalf("Expected no error when getting creds: %s", err) + } + expectedProvider := "EC2RoleProvider" + if v.ProviderName != expectedProvider { + t.Fatalf("Expected provider name to be %q, %q given", + expectedProvider, v.ProviderName) + } +} + +var credentialsFileContents = `[myprofile] +aws_access_key_id = accesskey +aws_secret_access_key = secretkey +` + +func TestAWSGetCredentials_shouldBeShared(t *testing.T) { + file, err := ioutil.TempFile(os.TempDir(), "terraform_aws_cred") + if err != nil { + t.Fatalf("Error writing temporary credentials file: %s", err) + } + _, err = file.WriteString(credentialsFileContents) + if err != nil { + t.Fatalf("Error writing temporary credentials to file: %s", err) + } + err = file.Close() + if err != nil { + t.Fatalf("Error closing temporary credentials file: %s", err) + } + + defer os.Remove(file.Name()) + + resetEnv := unsetEnv(t) + defer resetEnv() + + if err := os.Setenv("AWS_PROFILE", "myprofile"); err != nil { + t.Fatalf("Error resetting env var AWS_PROFILE: %s", err) + } + if err := os.Setenv("AWS_SHARED_CREDENTIALS_FILE", file.Name()); err != nil { + t.Fatalf("Error resetting env var AWS_SHARED_CREDENTIALS_FILE: %s", err) + } + + creds := GetCredentials("", "", "", "myprofile", file.Name()) + if creds == nil { + t.Fatalf("Expected a provider chain to be returned") + } + v, err := creds.Get() + if err != nil { + t.Fatalf("Error gettings creds: %s", err) + } + + if v.AccessKeyID != "accesskey" { + t.Fatalf("AccessKeyID mismatch, expected (%s), got (%s)", "accesskey", v.AccessKeyID) + } + + if v.SecretAccessKey != "secretkey" { + t.Fatalf("SecretAccessKey mismatch, expected (%s), got (%s)", "accesskey", v.AccessKeyID) + } +} + +func TestAWSGetCredentials_shouldBeENV(t *testing.T) { + // need to set the environment variables to a dummy string, as we don't know + // what they may be at runtime without hardcoding here + s := "some_env" + resetEnv := setEnv(s, t) + + defer resetEnv() + + cfg := Config{} + creds := GetCredentials(cfg.AccessKey, cfg.SecretKey, cfg.Token, cfg.Profile, cfg.CredsFilename) + if creds == nil { + t.Fatalf("Expected a static creds provider to be returned") + } + v, err := creds.Get() + if err != nil { + t.Fatalf("Error gettings creds: %s", err) + } + if v.AccessKeyID != s { + t.Fatalf("AccessKeyID mismatch, expected: (%s), got (%s)", s, v.AccessKeyID) + } + if v.SecretAccessKey != s { + t.Fatalf("SecretAccessKey mismatch, expected: (%s), got (%s)", s, v.SecretAccessKey) + } + if v.SessionToken != s { + t.Fatalf("SessionToken mismatch, expected: (%s), got (%s)", s, v.SessionToken) + } +} + +// unsetEnv unsets enviornment variables for testing a "clean slate" with no +// credentials in the environment +func unsetEnv(t *testing.T) func() { + // Grab any existing AWS keys and preserve. In some tests we'll unset these, so + // we need to have them and restore them after + e := getEnv() + if err := os.Unsetenv("AWS_ACCESS_KEY_ID"); err != nil { + t.Fatalf("Error unsetting env var AWS_ACCESS_KEY_ID: %s", err) + } + if err := os.Unsetenv("AWS_SECRET_ACCESS_KEY"); err != nil { + t.Fatalf("Error unsetting env var AWS_SECRET_ACCESS_KEY: %s", err) + } + if err := os.Unsetenv("AWS_SESSION_TOKEN"); err != nil { + t.Fatalf("Error unsetting env var AWS_SESSION_TOKEN: %s", err) + } + if err := os.Unsetenv("AWS_PROFILE"); err != nil { + t.Fatalf("Error unsetting env var AWS_TOKEN: %s", err) + } + if err := os.Unsetenv("AWS_SHARED_CREDENTIALS_FILE"); err != nil { + t.Fatalf("Error unsetting env var AWS_SHARED_CREDENTIALS_FILE: %s", err) + } + + return func() { + // re-set all the envs we unset above + if err := os.Setenv("AWS_ACCESS_KEY_ID", e.Key); err != nil { + t.Fatalf("Error resetting env var AWS_ACCESS_KEY_ID: %s", err) + } + if err := os.Setenv("AWS_SECRET_ACCESS_KEY", e.Secret); err != nil { + t.Fatalf("Error resetting env var AWS_SECRET_ACCESS_KEY: %s", err) + } + if err := os.Setenv("AWS_SESSION_TOKEN", e.Token); err != nil { + t.Fatalf("Error resetting env var AWS_SESSION_TOKEN: %s", err) + } + if err := os.Setenv("AWS_PROFILE", e.Profile); err != nil { + t.Fatalf("Error resetting env var AWS_PROFILE: %s", err) + } + if err := os.Setenv("AWS_SHARED_CREDENTIALS_FILE", e.CredsFilename); err != nil { + t.Fatalf("Error resetting env var AWS_SHARED_CREDENTIALS_FILE: %s", err) + } + } +} + +func setEnv(s string, t *testing.T) func() { + e := getEnv() + // Set all the envs to a dummy value + if err := os.Setenv("AWS_ACCESS_KEY_ID", s); err != nil { + t.Fatalf("Error setting env var AWS_ACCESS_KEY_ID: %s", err) + } + if err := os.Setenv("AWS_SECRET_ACCESS_KEY", s); err != nil { + t.Fatalf("Error setting env var AWS_SECRET_ACCESS_KEY: %s", err) + } + if err := os.Setenv("AWS_SESSION_TOKEN", s); err != nil { + t.Fatalf("Error setting env var AWS_SESSION_TOKEN: %s", err) + } + if err := os.Setenv("AWS_PROFILE", s); err != nil { + t.Fatalf("Error setting env var AWS_PROFILE: %s", err) + } + if err := os.Setenv("AWS_SHARED_CREDENTIALS_FILE", s); err != nil { + t.Fatalf("Error setting env var AWS_SHARED_CREDENTIALS_FLE: %s", err) + } + + return func() { + // re-set all the envs we unset above + if err := os.Setenv("AWS_ACCESS_KEY_ID", e.Key); err != nil { + t.Fatalf("Error resetting env var AWS_ACCESS_KEY_ID: %s", err) + } + if err := os.Setenv("AWS_SECRET_ACCESS_KEY", e.Secret); err != nil { + t.Fatalf("Error resetting env var AWS_SECRET_ACCESS_KEY: %s", err) + } + if err := os.Setenv("AWS_SESSION_TOKEN", e.Token); err != nil { + t.Fatalf("Error resetting env var AWS_SESSION_TOKEN: %s", err) + } + if err := os.Setenv("AWS_PROFILE", e.Profile); err != nil { + t.Fatalf("Error setting env var AWS_PROFILE: %s", err) + } + if err := os.Setenv("AWS_SHARED_CREDENTIALS_FILE", s); err != nil { + t.Fatalf("Error setting env var AWS_SHARED_CREDENTIALS_FLE: %s", err) + } + } +} + +// awsEnv establishes a httptest server to mock out the internal AWS Metadata +// service. IAM Credentials are retrieved by the EC2RoleProvider, which makes +// API calls to this internal URL. By replacing the server with a test server, +// we can simulate an AWS environment +func awsEnv(t *testing.T) func() { + routes := routes{} + if err := json.Unmarshal([]byte(metadataApiRoutes), &routes); err != nil { + t.Fatalf("Failed to unmarshal JSON in AWS ENV test: %s", err) + } + ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "text/plain") + w.Header().Add("Server", "MockEC2") + log.Printf("[DEBUG] Mocker server received request to %q", r.RequestURI) + for _, e := range routes.Endpoints { + if r.RequestURI == e.Uri { + fmt.Fprintln(w, e.Body) + w.WriteHeader(200) + return + } + } + w.WriteHeader(400) + })) + + os.Setenv("AWS_METADATA_URL", ts.URL+"/latest") + return ts.Close +} + +// invalidAwsEnv establishes a httptest server to simulate behaviour +// when endpoint doesn't respond as expected +func invalidAwsEnv(t *testing.T) func() { + ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(400) + })) + + os.Setenv("AWS_METADATA_URL", ts.URL+"/latest") + return ts.Close +} + +// getMockedAwsIamApi establishes a httptest server to simulate behaviour +// of a real AWS' IAM server +func getMockedAwsIamApi(endpoints []*iamEndpoint) (func(), *iam.IAM) { + ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + buf := new(bytes.Buffer) + buf.ReadFrom(r.Body) + requestBody := buf.String() + + log.Printf("[DEBUG] Received IAM API %q request to %q: %s", + r.Method, r.RequestURI, requestBody) + + for _, e := range endpoints { + if r.Method == e.Request.Method && r.RequestURI == e.Request.Uri && requestBody == e.Request.Body { + log.Printf("[DEBUG] Mock API responding with %d: %s", e.Response.StatusCode, e.Response.Body) + + w.WriteHeader(e.Response.StatusCode) + w.Header().Set("Content-Type", e.Response.ContentType) + w.Header().Set("X-Amzn-Requestid", "1b206dd1-f9a8-11e5-becf-051c60f11c4a") + w.Header().Set("Date", time.Now().Format(time.RFC1123)) + + fmt.Fprintln(w, e.Response.Body) + return + } + } + + w.WriteHeader(400) + return + })) + + sc := awsCredentials.NewStaticCredentials("accessKey", "secretKey", "") + + sess := session.New(&aws.Config{ + Credentials: sc, + Region: aws.String("us-east-1"), + Endpoint: aws.String(ts.URL), + CredentialsChainVerboseErrors: aws.Bool(true), + }) + iamConn := iam.New(sess) + + return ts.Close, iamConn +} + +func getEnv() *currentEnv { + // Grab any existing AWS keys and preserve. In some tests we'll unset these, so + // we need to have them and restore them after + return ¤tEnv{ + Key: os.Getenv("AWS_ACCESS_KEY_ID"), + Secret: os.Getenv("AWS_SECRET_ACCESS_KEY"), + Token: os.Getenv("AWS_SESSION_TOKEN"), + Profile: os.Getenv("AWS_TOKEN"), + CredsFilename: os.Getenv("AWS_SHARED_CREDENTIALS_FILE"), + } +} + +// struct to preserve the current environment +type currentEnv struct { + Key, Secret, Token, Profile, CredsFilename string +} + +type routes struct { + Endpoints []*endpoint `json:"endpoints"` +} +type endpoint struct { + Uri string `json:"uri"` + Body string `json:"body"` +} + +const metadataApiRoutes = ` +{ + "endpoints": [ + { + "uri": "/latest/meta-data/instance-id", + "body": "mock-instance-id" + }, + { + "uri": "/latest/meta-data/iam/info", + "body": "{\"Code\": \"Success\",\"LastUpdated\": \"2016-03-17T12:27:32Z\",\"InstanceProfileArn\": \"arn:aws:iam::123456789013:instance-profile/my-instance-profile\",\"InstanceProfileId\": \"AIPAABCDEFGHIJKLMN123\"}" + }, + { + "uri": "/latest/meta-data/iam/security-credentials", + "body": "test_role" + }, + { + "uri": "/latest/meta-data/iam/security-credentials/test_role", + "body": "{\"Code\":\"Success\",\"LastUpdated\":\"2015-12-11T17:17:25Z\",\"Type\":\"AWS-HMAC\",\"AccessKeyId\":\"somekey\",\"SecretAccessKey\":\"somesecret\",\"Token\":\"sometoken\"}" + } + ] +} +` + +type iamEndpoint struct { + Request *iamRequest + Response *iamResponse +} + +type iamRequest struct { + Method string + Uri string + Body string +} + +type iamResponse struct { + StatusCode int + Body string + ContentType string +} + +const iamResponse_GetUser_valid = ` + + + AIDACKCEVSQ6C2EXAMPLE + /division_abc/subdivision_xyz/ + Bob + arn:aws:iam::123456789012:user/division_abc/subdivision_xyz/Bob + 2013-10-02T17:01:44Z + 2014-10-10T14:37:51Z + + + + 7a62c49f-347e-4fc4-9331-6e8eEXAMPLE + +` + +const iamResponse_GetUser_unauthorized = ` + + Sender + AccessDenied + User: arn:aws:iam::123456789012:user/Bob is not authorized to perform: iam:GetUser on resource: arn:aws:iam::123456789012:user/Bob + + 7a62c49f-347e-4fc4-9331-6e8eEXAMPLE +` + +const iamResponse_GetUser_federatedFailure = ` + + Sender + ValidationError + Must specify userName when calling with non-User credentials + + 7a62c49f-347e-4fc4-9331-6e8eEXAMPLE +` + +const iamResponse_ListRoles_valid = ` + + true + AWceSSsKsazQ4IEplT9o4hURCzBs00iavlEvEXAMPLE + + + / + %7B%22Version%22%3A%222008-10-17%22%2C%22Statement%22%3A%5B%7B%22Sid%22%3A%22%22%2C%22Effect%22%3A%22Allow%22%2C%22Principal%22%3A%7B%22Service%22%3A%22ec2.amazonaws.com%22%7D%2C%22Action%22%3A%22sts%3AAssumeRole%22%7D%5D%7D + AROACKCEVSQ6C2EXAMPLE + elasticbeanstalk-role + arn:aws:iam::123456789012:role/elasticbeanstalk-role + 2013-10-02T17:01:44Z + + + + + 7a62c49f-347e-4fc4-9331-6e8eEXAMPLE + +` + +const iamResponse_ListRoles_unauthorized = ` + + Sender + AccessDenied + User: arn:aws:iam::123456789012:user/Bob is not authorized to perform: iam:ListRoles on resource: arn:aws:iam::123456789012:role/ + + 7a62c49f-347e-4fc4-9331-6e8eEXAMPLE +` diff --git a/builtin/providers/aws/config.go b/builtin/providers/aws/config.go index f521c755a..82a82e016 100644 --- a/builtin/providers/aws/config.go +++ b/builtin/providers/aws/config.go @@ -4,9 +4,7 @@ import ( "fmt" "log" "net/http" - "os" "strings" - "time" "github.com/hashicorp/go-cleanhttp" "github.com/hashicorp/go-multierror" @@ -17,9 +15,6 @@ import ( "github.com/aws/aws-sdk-go/aws" "github.com/aws/aws-sdk-go/aws/awserr" - awsCredentials "github.com/aws/aws-sdk-go/aws/credentials" - "github.com/aws/aws-sdk-go/aws/credentials/ec2rolecreds" - "github.com/aws/aws-sdk-go/aws/ec2metadata" "github.com/aws/aws-sdk-go/aws/request" "github.com/aws/aws-sdk-go/aws/session" "github.com/aws/aws-sdk-go/service/apigateway" @@ -133,10 +128,10 @@ func (c *Config) Client() (interface{}, error) { client.region = c.Region log.Println("[INFO] Building AWS auth structure") - creds := getCreds(c.AccessKey, c.SecretKey, c.Token, c.Profile, c.CredsFilename) + creds := GetCredentials(c.AccessKey, c.SecretKey, c.Token, c.Profile, c.CredsFilename) // Call Get to check for credential provider. If nothing found, we'll get an // error, and we can present it nicely to the user - _, err = creds.Get() + cp, err := creds.Get() if err != nil { if awsErr, ok := err.(awserr.Error); ok && awsErr.Code() == "NoCredentialProviders" { errs = append(errs, fmt.Errorf(`No valid credential sources found for AWS Provider. @@ -147,6 +142,9 @@ func (c *Config) Client() (interface{}, error) { } return nil, &multierror.Error{Errors: errs} } + + log.Printf("[INFO] AWS Auth provider used: %q", cp.ProviderName) + awsConfig := &aws.Config{ Credentials: creds, Region: aws.String(c.Region), @@ -177,6 +175,7 @@ func (c *Config) Client() (interface{}, error) { err = c.ValidateCredentials(client.iamconn) if err != nil { errs = append(errs, err) + return nil, &multierror.Error{Errors: errs} } // Some services exist only in us-east-1, e.g. because they manage @@ -216,7 +215,7 @@ func (c *Config) Client() (interface{}, error) { log.Println("[INFO] Initializing Elastic Beanstalk Connection") client.elasticbeanstalkconn = elasticbeanstalk.New(sess) - authErr := c.ValidateAccountId(client.iamconn) + authErr := c.ValidateAccountId(client.iamconn, cp.ProviderName) if authErr != nil { errs = append(errs, authErr) } @@ -323,7 +322,7 @@ func (c *Config) ValidateCredentials(iamconn *iam.IAM) error { if awsErr, ok := err.(awserr.Error); ok { if awsErr.Code() == "AccessDenied" || awsErr.Code() == "ValidationError" { - log.Printf("[WARN] AccessDenied Error with iam.GetUser, assuming IAM profile") + log.Printf("[WARN] AccessDenied Error with iam.GetUser, assuming IAM role") // User may be an IAM instance profile, or otherwise IAM role without the // GetUser permissions, so fail silently return nil @@ -339,31 +338,17 @@ func (c *Config) ValidateCredentials(iamconn *iam.IAM) error { // ValidateAccountId returns a context-specific error if the configured account // id is explicitly forbidden or not authorised; and nil if it is authorised. -func (c *Config) ValidateAccountId(iamconn *iam.IAM) error { +func (c *Config) ValidateAccountId(iamconn *iam.IAM, authProviderName string) error { if c.AllowedAccountIds == nil && c.ForbiddenAccountIds == nil { return nil } log.Printf("[INFO] Validating account ID") - - out, err := iamconn.GetUser(nil) - + account_id, err := GetAccountId(iamconn, authProviderName) if err != nil { - awsErr, _ := err.(awserr.Error) - if awsErr.Code() == "ValidationError" { - log.Printf("[WARN] ValidationError with iam.GetUser, assuming its an IAM profile") - // User may be an IAM instance profile, so fail silently. - // If it is an IAM instance profile - // validating account might be superfluous - return nil - } else { - return fmt.Errorf("Failed getting account ID from IAM: %s", err) - // return error if the account id is explicitly not authorised - } + return err } - account_id := strings.Split(*out.User.Arn, ":")[4] - if c.ForbiddenAccountIds != nil { for _, id := range c.ForbiddenAccountIds { if id == account_id { @@ -384,59 +369,6 @@ func (c *Config) ValidateAccountId(iamconn *iam.IAM) error { return nil } -// This function is responsible for reading credentials from the -// environment in the case that they're not explicitly specified -// in the Terraform configuration. -func getCreds(key, secret, token, profile, credsfile string) *awsCredentials.Credentials { - // build a chain provider, lazy-evaulated by aws-sdk - providers := []awsCredentials.Provider{ - &awsCredentials.StaticProvider{Value: awsCredentials.Value{ - AccessKeyID: key, - SecretAccessKey: secret, - SessionToken: token, - }}, - &awsCredentials.EnvProvider{}, - &awsCredentials.SharedCredentialsProvider{ - Filename: credsfile, - Profile: profile, - }, - } - - // We only look in the EC2 metadata API if we can connect - // to the metadata service within a reasonable amount of time - metadataURL := os.Getenv("AWS_METADATA_URL") - if metadataURL == "" { - metadataURL = "http://169.254.169.254:80/latest" - } - c := http.Client{ - Timeout: 100 * time.Millisecond, - } - - r, err := c.Get(metadataURL) - // Flag to determine if we should add the EC2Meta data provider. Default false - var useIAM bool - if err == nil { - // AWS will add a "Server: EC2ws" header value for the metadata request. We - // check the headers for this value to ensure something else didn't just - // happent to be listening on that IP:Port - if r.Header["Server"] != nil && strings.Contains(r.Header["Server"][0], "EC2") { - useIAM = true - } - } - - if useIAM { - log.Printf("[DEBUG] EC2 Metadata service found, adding EC2 Role Credential Provider") - providers = append(providers, &ec2rolecreds.EC2RoleProvider{ - Client: ec2metadata.New(session.New(&aws.Config{ - Endpoint: aws.String(metadataURL), - })), - }) - } else { - log.Printf("[DEBUG] EC2 Metadata service not found, not adding EC2 Role Credential Provider") - } - return awsCredentials.NewChainCredentials(providers) -} - // addTerraformVersionToUserAgent is a named handler that will add Terraform's // version information to requests made by the AWS SDK. var addTerraformVersionToUserAgent = request.NamedHandler{ diff --git a/builtin/providers/aws/config_test.go b/builtin/providers/aws/config_test.go deleted file mode 100644 index 5c58a5729..000000000 --- a/builtin/providers/aws/config_test.go +++ /dev/null @@ -1,376 +0,0 @@ -package aws - -import ( - "encoding/json" - "fmt" - "io/ioutil" - "net/http" - "net/http/httptest" - "os" - "testing" - - "github.com/aws/aws-sdk-go/aws/awserr" -) - -func TestAWSConfig_shouldError(t *testing.T) { - resetEnv := unsetEnv(t) - defer resetEnv() - cfg := Config{} - - c := getCreds(cfg.AccessKey, cfg.SecretKey, cfg.Token, cfg.Profile, cfg.CredsFilename) - _, err := c.Get() - if awsErr, ok := err.(awserr.Error); ok { - if awsErr.Code() != "NoCredentialProviders" { - t.Fatalf("Expected NoCredentialProviders error") - } - } - if err == nil { - t.Fatalf("Expected an error with empty env, keys, and IAM in AWS Config") - } -} - -func TestAWSConfig_shouldBeStatic(t *testing.T) { - simple := []struct { - Key, Secret, Token string - }{ - { - Key: "test", - Secret: "secret", - }, { - Key: "test", - Secret: "test", - Token: "test", - }, - } - - for _, c := range simple { - cfg := Config{ - AccessKey: c.Key, - SecretKey: c.Secret, - Token: c.Token, - } - - creds := getCreds(cfg.AccessKey, cfg.SecretKey, cfg.Token, cfg.Profile, cfg.CredsFilename) - if creds == nil { - t.Fatalf("Expected a static creds provider to be returned") - } - v, err := creds.Get() - if err != nil { - t.Fatalf("Error gettings creds: %s", err) - } - if v.AccessKeyID != c.Key { - t.Fatalf("AccessKeyID mismatch, expected: (%s), got (%s)", c.Key, v.AccessKeyID) - } - if v.SecretAccessKey != c.Secret { - t.Fatalf("SecretAccessKey mismatch, expected: (%s), got (%s)", c.Secret, v.SecretAccessKey) - } - if v.SessionToken != c.Token { - t.Fatalf("SessionToken mismatch, expected: (%s), got (%s)", c.Token, v.SessionToken) - } - } -} - -// TestAWSConfig_shouldIAM is designed to test the scenario of running Terraform -// from an EC2 instance, without environment variables or manually supplied -// credentials. -func TestAWSConfig_shouldIAM(t *testing.T) { - // clear AWS_* environment variables - resetEnv := unsetEnv(t) - defer resetEnv() - - // capture the test server's close method, to call after the test returns - ts := awsEnv(t) - defer ts() - - // An empty config, no key supplied - cfg := Config{} - - creds := getCreds(cfg.AccessKey, cfg.SecretKey, cfg.Token, cfg.Profile, cfg.CredsFilename) - if creds == nil { - t.Fatalf("Expected a static creds provider to be returned") - } - - v, err := creds.Get() - if err != nil { - t.Fatalf("Error gettings creds: %s", err) - } - if v.AccessKeyID != "somekey" { - t.Fatalf("AccessKeyID mismatch, expected: (somekey), got (%s)", v.AccessKeyID) - } - if v.SecretAccessKey != "somesecret" { - t.Fatalf("SecretAccessKey mismatch, expected: (somesecret), got (%s)", v.SecretAccessKey) - } - if v.SessionToken != "sometoken" { - t.Fatalf("SessionToken mismatch, expected: (sometoken), got (%s)", v.SessionToken) - } -} - -// TestAWSConfig_shouldIAM is designed to test the scenario of running Terraform -// from an EC2 instance, without environment variables or manually supplied -// credentials. -func TestAWSConfig_shouldIgnoreIAM(t *testing.T) { - resetEnv := unsetEnv(t) - defer resetEnv() - // capture the test server's close method, to call after the test returns - ts := awsEnv(t) - defer ts() - simple := []struct { - Key, Secret, Token string - }{ - { - Key: "test", - Secret: "secret", - }, { - Key: "test", - Secret: "test", - Token: "test", - }, - } - - for _, c := range simple { - cfg := Config{ - AccessKey: c.Key, - SecretKey: c.Secret, - Token: c.Token, - } - - creds := getCreds(cfg.AccessKey, cfg.SecretKey, cfg.Token, cfg.Profile, cfg.CredsFilename) - if creds == nil { - t.Fatalf("Expected a static creds provider to be returned") - } - v, err := creds.Get() - if err != nil { - t.Fatalf("Error gettings creds: %s", err) - } - if v.AccessKeyID != c.Key { - t.Fatalf("AccessKeyID mismatch, expected: (%s), got (%s)", c.Key, v.AccessKeyID) - } - if v.SecretAccessKey != c.Secret { - t.Fatalf("SecretAccessKey mismatch, expected: (%s), got (%s)", c.Secret, v.SecretAccessKey) - } - if v.SessionToken != c.Token { - t.Fatalf("SessionToken mismatch, expected: (%s), got (%s)", c.Token, v.SessionToken) - } - } -} - -var credentialsFileContents = `[myprofile] -aws_access_key_id = accesskey -aws_secret_access_key = secretkey -` - -func TestAWSConfig_shouldBeShared(t *testing.T) { - file, err := ioutil.TempFile(os.TempDir(), "terraform_aws_cred") - if err != nil { - t.Fatalf("Error writing temporary credentials file: %s", err) - } - _, err = file.WriteString(credentialsFileContents) - if err != nil { - t.Fatalf("Error writing temporary credentials to file: %s", err) - } - err = file.Close() - if err != nil { - t.Fatalf("Error closing temporary credentials file: %s", err) - } - - defer os.Remove(file.Name()) - - resetEnv := unsetEnv(t) - defer resetEnv() - - if err := os.Setenv("AWS_PROFILE", "myprofile"); err != nil { - t.Fatalf("Error resetting env var AWS_PROFILE: %s", err) - } - if err := os.Setenv("AWS_SHARED_CREDENTIALS_FILE", file.Name()); err != nil { - t.Fatalf("Error resetting env var AWS_SHARED_CREDENTIALS_FILE: %s", err) - } - - creds := getCreds("", "", "", "myprofile", file.Name()) - if creds == nil { - t.Fatalf("Expected a provider chain to be returned") - } - v, err := creds.Get() - if err != nil { - t.Fatalf("Error gettings creds: %s", err) - } - - if v.AccessKeyID != "accesskey" { - t.Fatalf("AccessKeyID mismatch, expected (%s), got (%s)", "accesskey", v.AccessKeyID) - } - - if v.SecretAccessKey != "secretkey" { - t.Fatalf("SecretAccessKey mismatch, expected (%s), got (%s)", "accesskey", v.AccessKeyID) - } -} - -func TestAWSConfig_shouldBeENV(t *testing.T) { - // need to set the environment variables to a dummy string, as we don't know - // what they may be at runtime without hardcoding here - s := "some_env" - resetEnv := setEnv(s, t) - - defer resetEnv() - - cfg := Config{} - creds := getCreds(cfg.AccessKey, cfg.SecretKey, cfg.Token, cfg.Profile, cfg.CredsFilename) - if creds == nil { - t.Fatalf("Expected a static creds provider to be returned") - } - v, err := creds.Get() - if err != nil { - t.Fatalf("Error gettings creds: %s", err) - } - if v.AccessKeyID != s { - t.Fatalf("AccessKeyID mismatch, expected: (%s), got (%s)", s, v.AccessKeyID) - } - if v.SecretAccessKey != s { - t.Fatalf("SecretAccessKey mismatch, expected: (%s), got (%s)", s, v.SecretAccessKey) - } - if v.SessionToken != s { - t.Fatalf("SessionToken mismatch, expected: (%s), got (%s)", s, v.SessionToken) - } -} - -// unsetEnv unsets enviornment variables for testing a "clean slate" with no -// credentials in the environment -func unsetEnv(t *testing.T) func() { - // Grab any existing AWS keys and preserve. In some tests we'll unset these, so - // we need to have them and restore them after - e := getEnv() - if err := os.Unsetenv("AWS_ACCESS_KEY_ID"); err != nil { - t.Fatalf("Error unsetting env var AWS_ACCESS_KEY_ID: %s", err) - } - if err := os.Unsetenv("AWS_SECRET_ACCESS_KEY"); err != nil { - t.Fatalf("Error unsetting env var AWS_SECRET_ACCESS_KEY: %s", err) - } - if err := os.Unsetenv("AWS_SESSION_TOKEN"); err != nil { - t.Fatalf("Error unsetting env var AWS_SESSION_TOKEN: %s", err) - } - if err := os.Unsetenv("AWS_PROFILE"); err != nil { - t.Fatalf("Error unsetting env var AWS_TOKEN: %s", err) - } - if err := os.Unsetenv("AWS_SHARED_CREDENTIALS_FILE"); err != nil { - t.Fatalf("Error unsetting env var AWS_SHARED_CREDENTIALS_FILE: %s", err) - } - - return func() { - // re-set all the envs we unset above - if err := os.Setenv("AWS_ACCESS_KEY_ID", e.Key); err != nil { - t.Fatalf("Error resetting env var AWS_ACCESS_KEY_ID: %s", err) - } - if err := os.Setenv("AWS_SECRET_ACCESS_KEY", e.Secret); err != nil { - t.Fatalf("Error resetting env var AWS_SECRET_ACCESS_KEY: %s", err) - } - if err := os.Setenv("AWS_SESSION_TOKEN", e.Token); err != nil { - t.Fatalf("Error resetting env var AWS_SESSION_TOKEN: %s", err) - } - if err := os.Setenv("AWS_PROFILE", e.Profile); err != nil { - t.Fatalf("Error resetting env var AWS_PROFILE: %s", err) - } - if err := os.Setenv("AWS_SHARED_CREDENTIALS_FILE", e.CredsFilename); err != nil { - t.Fatalf("Error resetting env var AWS_SHARED_CREDENTIALS_FILE: %s", err) - } - } -} - -func setEnv(s string, t *testing.T) func() { - e := getEnv() - // Set all the envs to a dummy value - if err := os.Setenv("AWS_ACCESS_KEY_ID", s); err != nil { - t.Fatalf("Error setting env var AWS_ACCESS_KEY_ID: %s", err) - } - if err := os.Setenv("AWS_SECRET_ACCESS_KEY", s); err != nil { - t.Fatalf("Error setting env var AWS_SECRET_ACCESS_KEY: %s", err) - } - if err := os.Setenv("AWS_SESSION_TOKEN", s); err != nil { - t.Fatalf("Error setting env var AWS_SESSION_TOKEN: %s", err) - } - if err := os.Setenv("AWS_PROFILE", s); err != nil { - t.Fatalf("Error setting env var AWS_PROFILE: %s", err) - } - if err := os.Setenv("AWS_SHARED_CREDENTIALS_FILE", s); err != nil { - t.Fatalf("Error setting env var AWS_SHARED_CREDENTIALS_FLE: %s", err) - } - - return func() { - // re-set all the envs we unset above - if err := os.Setenv("AWS_ACCESS_KEY_ID", e.Key); err != nil { - t.Fatalf("Error resetting env var AWS_ACCESS_KEY_ID: %s", err) - } - if err := os.Setenv("AWS_SECRET_ACCESS_KEY", e.Secret); err != nil { - t.Fatalf("Error resetting env var AWS_SECRET_ACCESS_KEY: %s", err) - } - if err := os.Setenv("AWS_SESSION_TOKEN", e.Token); err != nil { - t.Fatalf("Error resetting env var AWS_SESSION_TOKEN: %s", err) - } - if err := os.Setenv("AWS_PROFILE", e.Profile); err != nil { - t.Fatalf("Error setting env var AWS_PROFILE: %s", err) - } - if err := os.Setenv("AWS_SHARED_CREDENTIALS_FILE", s); err != nil { - t.Fatalf("Error setting env var AWS_SHARED_CREDENTIALS_FLE: %s", err) - } - } -} - -// awsEnv establishes a httptest server to mock out the internal AWS Metadata -// service. IAM Credentials are retrieved by the EC2RoleProvider, which makes -// API calls to this internal URL. By replacing the server with a test server, -// we can simulate an AWS environment -func awsEnv(t *testing.T) func() { - routes := routes{} - if err := json.Unmarshal([]byte(aws_routes), &routes); err != nil { - t.Fatalf("Failed to unmarshal JSON in AWS ENV test: %s", err) - } - ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - w.Header().Set("Content-Type", "text/plain") - w.Header().Add("Server", "MockEC2") - for _, e := range routes.Endpoints { - if r.RequestURI == e.Uri { - fmt.Fprintln(w, e.Body) - } - } - })) - - os.Setenv("AWS_METADATA_URL", ts.URL+"/latest") - return ts.Close -} - -func getEnv() *currentEnv { - // Grab any existing AWS keys and preserve. In some tests we'll unset these, so - // we need to have them and restore them after - return ¤tEnv{ - Key: os.Getenv("AWS_ACCESS_KEY_ID"), - Secret: os.Getenv("AWS_SECRET_ACCESS_KEY"), - Token: os.Getenv("AWS_SESSION_TOKEN"), - Profile: os.Getenv("AWS_TOKEN"), - CredsFilename: os.Getenv("AWS_SHARED_CREDENTIALS_FILE"), - } -} - -// struct to preserve the current environment -type currentEnv struct { - Key, Secret, Token, Profile, CredsFilename string -} - -type routes struct { - Endpoints []*endpoint `json:"endpoints"` -} -type endpoint struct { - Uri string `json:"uri"` - Body string `json:"body"` -} - -const aws_routes = ` -{ - "endpoints": [ - { - "uri": "/latest/meta-data/iam/security-credentials", - "body": "test_role" - }, - { - "uri": "/latest/meta-data/iam/security-credentials/test_role", - "body": "{\"Code\":\"Success\",\"LastUpdated\":\"2015-12-11T17:17:25Z\",\"Type\":\"AWS-HMAC\",\"AccessKeyId\":\"somekey\",\"SecretAccessKey\":\"somesecret\",\"Token\":\"sometoken\"}" - } - ] -} -` diff --git a/website/source/docs/providers/aws/index.html.markdown b/website/source/docs/providers/aws/index.html.markdown index 7bc328dad..949c67f62 100644 --- a/website/source/docs/providers/aws/index.html.markdown +++ b/website/source/docs/providers/aws/index.html.markdown @@ -39,7 +39,7 @@ explained below: - Static credentials - Environment variables - Shared credentials file - +- EC2 Role ### Static credentials ### @@ -96,6 +96,21 @@ provider "aws" { } ``` +###EC2 Role + +If you're running Terraform from an EC2 instance with IAM Instance Profile +using IAM Role, Terraform will just ask +[the metadata API](http://docs.aws.amazon.com/AWSEC2/latest/UserGuide/iam-roles-for-amazon-ec2.html#instance-metadata-security-credentials) +endpoint for credentials. + +This is a preferred approach over any other when running in EC2 as you can avoid +hardcoding credentials. Instead these are leased on-the-fly by Terraform +which reduces the chance of leakage. + +You can provide custom metadata API endpoint via `AWS_METADATA_ENDPOINT` variable +which expects the endpoint URL including the version +and defaults to `http://169.254.169.254:80/latest`. + ## Argument Reference The following arguments are supported in the `provider` block: @@ -156,4 +171,24 @@ Nested `endpoints` block supports the followings: * `elb` - (Optional) Use this to override the default endpoint URL constructed from the `region`. It's typically used to connect to - custom elb endpoints. \ No newline at end of file + custom elb endpoints. + +## Getting the Account ID + +If you use either `allowed_account_ids` or `forbidden_account_ids`, +Terraform uses several approaches to get the actual account ID +in order to compare it with allowed/forbidden ones. + +Approaches differ per auth providers: + + * EC2 instance w/ IAM Instance Profile - [Metadata API](http://docs.aws.amazon.com/AWSEC2/latest/UserGuide/ec2-instance-metadata.html) + is always used + * All other providers (ENV vars, shared creds file, ...) + will try two approaches in the following order + * `iam:GetUser` - typically useful for IAM Users. It also means + that each user needs to be privileged to call `iam:GetUser` for themselves. + * `iam:ListRoles` - this is specifically useful for IdP-federated profiles + which cannot use `iam:GetUser`. It also means that each federated user + need to be _assuming_ an IAM role which allows `iam:ListRoles`. + There is currently no better clean way to get account ID + out of the API when using federated account unfortunately.