From 1282c53116229888658c57926c758f2e5eba60b7 Mon Sep 17 00:00:00 2001 From: Luis Madrigal <599908+Madrigal@users.noreply.github.com> Date: Mon, 16 Sep 2024 13:22:26 -0400 Subject: [PATCH] Read `AWS_CONTAINER_CREDENTIALS_FULL_URI` env variable if set when reading a profile with `credential_source`. (#2790) * Read `AWS_CONTAINER_CREDENTIALS_FULL_URI` env variable if set when reading a profile with `credential_source`. Also ensure `AWS_CONTAINER_CREDENTIALS_RELATIVE_URI` is always read before it --- .../3230f94ad7814d24b10beaed0739d43c.json | 8 + config/resolve_credentials.go | 15 +- config/resolve_credentials_test.go | 150 +++++++++++++++++- 3 files changed, 165 insertions(+), 8 deletions(-) create mode 100644 .changelog/3230f94ad7814d24b10beaed0739d43c.json diff --git a/.changelog/3230f94ad7814d24b10beaed0739d43c.json b/.changelog/3230f94ad7814d24b10beaed0739d43c.json new file mode 100644 index 00000000000..05950628adb --- /dev/null +++ b/.changelog/3230f94ad7814d24b10beaed0739d43c.json @@ -0,0 +1,8 @@ +{ + "id": "3230f94a-d781-4d24-b10b-eaed0739d43c", + "type": "bugfix", + "description": "Read `AWS_CONTAINER_CREDENTIALS_FULL_URI` env variable if set when reading a profile with `credential_source`. Also ensure `AWS_CONTAINER_CREDENTIALS_RELATIVE_URI` is always read before it", + "modules": [ + "config" + ] +} \ No newline at end of file diff --git a/config/resolve_credentials.go b/config/resolve_credentials.go index 89368520f3f..7ae252e2e87 100644 --- a/config/resolve_credentials.go +++ b/config/resolve_credentials.go @@ -162,12 +162,12 @@ func resolveCredsFromProfile(ctx context.Context, cfg *aws.Config, envConfig *En // Get credentials from CredentialProcess err = processCredentials(ctx, cfg, sharedConfig, configs) - case len(envConfig.ContainerCredentialsEndpoint) != 0: - err = resolveLocalHTTPCredProvider(ctx, cfg, envConfig.ContainerCredentialsEndpoint, envConfig.ContainerAuthorizationToken, configs) - case len(envConfig.ContainerCredentialsRelativePath) != 0: err = resolveHTTPCredProvider(ctx, cfg, ecsContainerURI(envConfig.ContainerCredentialsRelativePath), envConfig.ContainerAuthorizationToken, configs) + case len(envConfig.ContainerCredentialsEndpoint) != 0: + err = resolveLocalHTTPCredProvider(ctx, cfg, envConfig.ContainerCredentialsEndpoint, envConfig.ContainerAuthorizationToken, configs) + default: err = resolveEC2RoleCredentials(ctx, cfg, configs) } @@ -355,10 +355,13 @@ func resolveCredsFromSource(ctx context.Context, cfg *aws.Config, envConfig *Env cfg.Credentials = credentials.StaticCredentialsProvider{Value: envConfig.Credentials} case credSourceECSContainer: - if len(envConfig.ContainerCredentialsRelativePath) == 0 { - return fmt.Errorf("EcsContainer was specified as the credential_source, but 'AWS_CONTAINER_CREDENTIALS_RELATIVE_URI' was not set") + if len(envConfig.ContainerCredentialsRelativePath) != 0 { + return resolveHTTPCredProvider(ctx, cfg, ecsContainerURI(envConfig.ContainerCredentialsRelativePath), envConfig.ContainerAuthorizationToken, configs) + } + if len(envConfig.ContainerCredentialsEndpoint) != 0 { + return resolveLocalHTTPCredProvider(ctx, cfg, envConfig.ContainerCredentialsEndpoint, envConfig.ContainerAuthorizationToken, configs) } - return resolveHTTPCredProvider(ctx, cfg, ecsContainerURI(envConfig.ContainerCredentialsRelativePath), envConfig.ContainerAuthorizationToken, configs) + return fmt.Errorf("EcsContainer was specified as the credential_source, but neither 'AWS_CONTAINER_CREDENTIALS_RELATIVE_URI' or AWS_CONTAINER_CREDENTIALS_FULL_URI' was set") default: return fmt.Errorf("credential_source values must be EcsContainer, Ec2InstanceMetadata, or Environment") diff --git a/config/resolve_credentials_test.go b/config/resolve_credentials_test.go index 503a1ec7be3..839445065a2 100644 --- a/config/resolve_credentials_test.go +++ b/config/resolve_credentials_test.go @@ -33,15 +33,51 @@ func swapECSContainerURI(path string) func() { } } -func setupCredentialsEndpoints(t *testing.T) (aws.EndpointResolverWithOptions, func()) { +const ecsFullPathResponse = `{ + "Code": "Success", + "Type": "AWS-HMAC", + "AccessKeyId": "ecs-full-path-access-key", + "SecretAccessKey": "ecs-full-path-ecs-secret-key", + "Token": "ecs-full-path-token", + "Expiration": "2100-01-01T00:00:00Z", + "LastUpdated": "2009-11-23T00:00:00Z" +}` + +const assumeRoleRespEcsFullPathMsg = ` + + + + arn:aws:sts::account_id:assumed-role/role/session_name + AKID:session_name + + + AKID-Full-Path + SECRET-Full-Path + SESSION_TOKEN-Full-Path + %s + + + + request-id + + +` + +var ecsMetadataServerURL string + +func setupCredentialsEndpoints() (aws.EndpointResolverWithOptions, func()) { ecsMetadataServer := httptest.NewServer(http.HandlerFunc( func(w http.ResponseWriter, r *http.Request) { if r.URL.Path == "/ECS" { w.Write([]byte(ecsResponse)) + // Used when we specify a full path instead of relative path + } else if r.URL.Path == "/ECSFullPath" { + w.Write([]byte(ecsFullPathResponse)) } else { w.Write([]byte("")) } })) + ecsMetadataServerURL = ecsMetadataServer.URL resetECSEndpoint := swapECSContainerURI(ecsMetadataServer.URL) ec2MetadataServer := httptest.NewServer(http.HandlerFunc( @@ -74,6 +110,15 @@ func setupCredentialsEndpoints(t *testing.T) (aws.EndpointResolverWithOptions, f switch form.Get("Action") { case "AssumeRole": + if val, ok := r.Header["X-Amz-Security-Token"]; ok { + if val[0] == "ecs-full-path-token" { + w.Write([]byte(fmt.Sprintf( + assumeRoleRespEcsFullPathMsg, + smithytime.FormatDateTime(time.Now(). + Add(15*time.Minute))))) + return + } + } w.Write([]byte(fmt.Sprintf( assumeRoleRespMsg, smithytime.FormatDateTime(time.Now(). @@ -394,7 +439,7 @@ func TestSharedConfigCredentialSource(t *testing.T) { os.Setenv("AWS_PROFILE", c.envProfile) } - endpointResolver, cleanupFn := setupCredentialsEndpoints(t) + endpointResolver, cleanupFn := setupCredentialsEndpoints() defer cleanupFn() var cleanup func() @@ -604,6 +649,107 @@ func TestResolveCredentialsIMDSClient(t *testing.T) { } } +func TestResolveCredentialsEcsContainer(t *testing.T) { + testCases := map[string]struct { + expectedAccessKey string + expectedSecretKey string + envVar map[string]string + configFile string + }{ + "only relative ECS URI set": { + expectedAccessKey: "ecs-access-key", + expectedSecretKey: "ecs-secret-key", + envVar: map[string]string{ + "AWS_CONTAINER_CREDENTIALS_RELATIVE_URI": "/ECS", + }, + }, + "only full ECS URI set": { + expectedAccessKey: "ecs-full-path-access-key", + expectedSecretKey: "ecs-full-path-ecs-secret-key", + envVar: map[string]string{ + "AWS_CONTAINER_CREDENTIALS_FULL_URI": "placeholder-replaced-at-runtime", + }, + }, + "relative ECS URI has precedence over full": { + expectedAccessKey: "ecs-access-key", + expectedSecretKey: "ecs-secret-key", + envVar: map[string]string{ + "AWS_CONTAINER_CREDENTIALS_RELATIVE_URI": "/ECS", + "AWS_CONTAINER_CREDENTIALS_FULL_URI": "placeholder-replaced-at-runtime", + }, + }, + "credential source only relative ECS URI set": { + expectedAccessKey: "AKID", + expectedSecretKey: "SECRET", + envVar: map[string]string{ + "AWS_PROFILE": "ecscontainer", + "AWS_CONTAINER_CREDENTIALS_RELATIVE_URI": "/ECS", + }, + configFile: filepath.Join("testdata", "config_source_shared"), + }, + "credential source only full ECS URI set": { + expectedAccessKey: "AKID-Full-Path", + expectedSecretKey: "SECRET-Full-Path", + envVar: map[string]string{ + "AWS_CONTAINER_CREDENTIALS_FULL_URI": "placeholder-replaced-at-runtime", + "AWS_PROFILE": "ecscontainer", + }, + configFile: filepath.Join("testdata", "config_source_shared"), + }, + "credential source relative ECS URI has precedence over full": { + expectedAccessKey: "AKID", + expectedSecretKey: "SECRET", + envVar: map[string]string{ + "AWS_CONTAINER_CREDENTIALS_RELATIVE_URI": "/ECS", + "AWS_CONTAINER_CREDENTIALS_FULL_URI": "placeholder-replaced-at-runtime", + "AWS_PROFILE": "ecscontainer", + }, + configFile: filepath.Join("testdata", "config_source_shared"), + }, + } + + for name, tc := range testCases { + t.Run(name, func(t *testing.T) { + endpointResolver, cleanupFn := setupCredentialsEndpoints() + defer cleanupFn() + restoreEnv := awstesting.StashEnv() + defer awstesting.PopEnv(restoreEnv) + var sharedConfigFiles []string + if tc.configFile != "" { + sharedConfigFiles = append(sharedConfigFiles, tc.configFile) + } + opts := []func(*LoadOptions) error{ + WithEndpointResolverWithOptions(endpointResolver), + WithRetryer(func() aws.Retryer { return aws.NopRetryer{} }), + WithSharedConfigFiles(sharedConfigFiles), + WithSharedCredentialsFiles([]string{}), + } + for k, v := range tc.envVar { + // since we don't know the value of this until the server starts + if k == "AWS_CONTAINER_CREDENTIALS_FULL_URI" { + v = ecsMetadataServerURL + "/ECSFullPath" + } + os.Setenv(k, v) + } + cfg, err := LoadDefaultConfig(context.TODO(), opts...) + if err != nil { + t.Fatalf("could not load config: %s", err) + } + actual, err := cfg.Credentials.Retrieve(context.TODO()) + if err != nil { + t.Fatalf("could not retrieve credentials: %s", err) + } + if actual.AccessKeyID != tc.expectedAccessKey { + t.Errorf("expected access key to be %s, got %s", tc.expectedAccessKey, actual.AccessKeyID) + } + if actual.SecretAccessKey != tc.expectedSecretKey { + t.Errorf("expected secret key to be %s, got %s", tc.expectedSecretKey, actual.SecretAccessKey) + } + }) + } + +} + type stubErrorClient struct { err error }