Skip to content

Commit

Permalink
Read AWS_CONTAINER_CREDENTIALS_FULL_URI env variable if set when re…
Browse files Browse the repository at this point in the history
…ading 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
  • Loading branch information
Madrigal authored Sep 16, 2024
1 parent 171151b commit 1282c53
Show file tree
Hide file tree
Showing 3 changed files with 165 additions and 8 deletions.
8 changes: 8 additions & 0 deletions .changelog/3230f94ad7814d24b10beaed0739d43c.json
Original file line number Diff line number Diff line change
@@ -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"
]
}
15 changes: 9 additions & 6 deletions config/resolve_credentials.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
Expand Down Expand Up @@ -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")
Expand Down
150 changes: 148 additions & 2 deletions config/resolve_credentials_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 = `
<AssumeRoleResponse xmlns="https://sts.amazonaws.com/doc/2011-06-15/">
<AssumeRoleResult>
<AssumedRoleUser>
<Arn>arn:aws:sts::account_id:assumed-role/role/session_name</Arn>
<AssumedRoleId>AKID:session_name</AssumedRoleId>
</AssumedRoleUser>
<Credentials>
<AccessKeyId>AKID-Full-Path</AccessKeyId>
<SecretAccessKey>SECRET-Full-Path</SecretAccessKey>
<SessionToken>SESSION_TOKEN-Full-Path</SessionToken>
<Expiration>%s</Expiration>
</Credentials>
</AssumeRoleResult>
<ResponseMetadata>
<RequestId>request-id</RequestId>
</ResponseMetadata>
</AssumeRoleResponse>
`

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(
Expand Down Expand Up @@ -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().
Expand Down Expand Up @@ -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()
Expand Down Expand Up @@ -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
}
Expand Down

0 comments on commit 1282c53

Please sign in to comment.