Skip to content

Commit 17dd424

Browse files
committed
add profile override and account id to imds creds
1 parent 56be5e3 commit 17dd424

14 files changed

+589
-33
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
{
2+
"id": "3a4c3951-c250-4554-a64b-14ce2dddf6ef",
3+
"type": "feature",
4+
"description": "Support account ID retrieval in IMDS credentials provider, and support new IMDS profile name config:\n\n1. environment: `AWS_EC2_INSTANCE_PROFILE_NAME`\n2. shared config: `ec2_instance_profile_name`",
5+
"modules": [
6+
"config",
7+
"credentials"
8+
]
9+
}

config/config_source_test.go

+2-2
Original file line numberDiff line numberDiff line change
@@ -128,10 +128,10 @@ func (f imdsForwarder) Do(r *http.Request) (*http.Response, error) {
128128
header.Set(ttlHeader, r.Header.Get(ttlHeader))
129129
return &http.Response{StatusCode: 200, Header: header, Body: io.NopCloser(strings.NewReader("validToken"))}, nil
130130
}
131-
if r.URL.Path == "/latest/meta-data/iam/security-credentials/" {
131+
if r.URL.Path == "/latest/meta-data/iam/security-credentials-extended/" {
132132
return &http.Response{StatusCode: 200, Body: io.NopCloser(strings.NewReader("RoleName"))}, nil
133133
}
134-
if r.URL.Path == "/latest/meta-data/iam/security-credentials/RoleName" {
134+
if r.URL.Path == "/latest/meta-data/iam/security-credentials-extended/RoleName" {
135135
return &http.Response{StatusCode: 200, Body: io.NopCloser(strings.NewReader(ecsResponse))}, nil
136136
}
137137
return f.innerClient.Do(r)

config/env_config.go

+15
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,8 @@ const (
6060
awsEc2MetadataDisabledEnv = "AWS_EC2_METADATA_DISABLED"
6161
awsEc2MetadataV1DisabledEnv = "AWS_EC2_METADATA_V1_DISABLED"
6262

63+
awsEc2InstanceProfileNameEnv = "AWS_EC2_INSTANCE_PROFILE_NAME"
64+
6365
awsS3DisableMultiRegionAccessPointsEnv = "AWS_S3_DISABLE_MULTIREGION_ACCESS_POINTS"
6466

6567
awsUseDualStackEndpointEnv = "AWS_USE_DUALSTACK_ENDPOINT"
@@ -304,6 +306,9 @@ type EnvConfig struct {
304306

305307
// Indicates whether response checksum should be validated
306308
ResponseChecksumValidation aws.ResponseChecksumValidation
309+
310+
// Profile name used for fetching IMDS credentials.
311+
EC2InstanceProfileName string
307312
}
308313

309314
// loadEnvConfig reads configuration values from the OS's environment variables.
@@ -347,6 +352,8 @@ func NewEnvConfig() (EnvConfig, error) {
347352

348353
cfg.AppID = os.Getenv(awsSdkUaAppIDEnv)
349354

355+
cfg.EC2InstanceProfileName = os.Getenv(awsEc2InstanceProfileNameEnv)
356+
350357
if err := setBoolPtrFromEnvVal(&cfg.DisableRequestCompression, []string{awsDisableRequestCompressionEnv}); err != nil {
351358
return cfg, err
352359
}
@@ -916,3 +923,11 @@ func (c EnvConfig) GetS3DisableExpressAuth() (value, ok bool) {
916923

917924
return *c.S3DisableExpressAuth, true
918925
}
926+
927+
func (c EnvConfig) getEC2InstanceProfileName() (string, bool, error) {
928+
if len(c.EC2InstanceProfileName) == 0 {
929+
return "", false, nil
930+
}
931+
932+
return c.EC2InstanceProfileName, true, nil
933+
}

config/env_config_test.go

+8
Original file line numberDiff line numberDiff line change
@@ -568,6 +568,14 @@ func TestNewEnvConfig(t *testing.T) {
568568
Config: EnvConfig{},
569569
WantErr: true,
570570
},
571+
54: {
572+
Env: map[string]string{
573+
"AWS_EC2_INSTANCE_PROFILE_NAME": "ProfileName",
574+
},
575+
Config: EnvConfig{
576+
EC2InstanceProfileName: "ProfileName",
577+
},
578+
},
571579
}
572580

573581
for i, c := range cases {

config/provider.go

+16
Original file line numberDiff line numberDiff line change
@@ -445,6 +445,22 @@ func getEC2RoleCredentialProviderOptions(ctx context.Context, configs configs) (
445445
return
446446
}
447447

448+
type ec2InstanceProfileNameProvider interface {
449+
getEC2InstanceProfileName() (string, bool, error)
450+
}
451+
452+
func getEC2InstanceProfileName(ctx context.Context, configs configs) (v string, found bool, err error) {
453+
for _, config := range configs {
454+
if p, ok := config.(ec2InstanceProfileNameProvider); ok {
455+
v, found, err = p.getEC2InstanceProfileName()
456+
if err != nil || found {
457+
break
458+
}
459+
}
460+
}
461+
return
462+
}
463+
448464
// defaultRegionProvider is an interface for retrieving a default region if a region was not resolved from other sources
449465
type defaultRegionProvider interface {
450466
getDefaultRegion(ctx context.Context) (string, bool, error)

config/resolve_credentials.go

+17-4
Original file line numberDiff line numberDiff line change
@@ -189,7 +189,7 @@ func resolveCredsFromProfile(ctx context.Context, cfg *aws.Config, envConfig *En
189189

190190
default:
191191
ctx = addCredentialSource(ctx, aws.CredentialSourceIMDS)
192-
err = resolveEC2RoleCredentials(ctx, cfg, configs)
192+
err = resolveEC2RoleCredentials(ctx, cfg, envConfig, sharedConfig, configs)
193193
}
194194
if err != nil {
195195
return ctx, err
@@ -379,7 +379,7 @@ func resolveCredsFromSource(ctx context.Context, cfg *aws.Config, envConfig *Env
379379
switch sharedCfg.CredentialSource {
380380
case credSourceEc2Metadata:
381381
ctx = addCredentialSource(ctx, aws.CredentialSourceIMDS)
382-
return ctx, resolveEC2RoleCredentials(ctx, cfg, configs)
382+
return ctx, resolveEC2RoleCredentials(ctx, cfg, envConfig, sharedCfg, configs)
383383

384384
case credSourceEnvironment:
385385
ctx = addCredentialSource(ctx, aws.CredentialSourceHTTP)
@@ -402,8 +402,21 @@ func resolveCredsFromSource(ctx context.Context, cfg *aws.Config, envConfig *Env
402402
return ctx, nil
403403
}
404404

405-
func resolveEC2RoleCredentials(ctx context.Context, cfg *aws.Config, configs configs) error {
406-
optFns := make([]func(*ec2rolecreds.Options), 0, 2)
405+
func resolveEC2RoleCredentials(ctx context.Context, cfg *aws.Config, envCfg *EnvConfig, sharedCfg *SharedConfig, configs configs) error {
406+
optFns := make([]func(*ec2rolecreds.Options), 0, 3)
407+
408+
var profile string
409+
if sharedCfg != nil && sharedCfg.EC2InstanceProfileName != "" {
410+
profile = sharedCfg.EC2InstanceProfileName
411+
}
412+
if envCfg != nil && envCfg.EC2InstanceProfileName != "" {
413+
profile = envCfg.EC2InstanceProfileName
414+
}
415+
if profile != "" {
416+
optFns = append(optFns, func(o *ec2rolecreds.Options) {
417+
o.ProfileName = profile // caller options will override
418+
})
419+
}
407420

408421
optFn, found, err := getEC2RoleCredentialProviderOptions(ctx, configs)
409422
if err != nil {

config/resolve_credentials_test.go

+106-2
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ import (
1616
"time"
1717

1818
"github.com/aws/aws-sdk-go-v2/aws"
19+
"github.com/aws/aws-sdk-go-v2/credentials/ec2rolecreds"
1920
"github.com/aws/aws-sdk-go-v2/feature/ec2/imds"
2021
"github.com/aws/aws-sdk-go-v2/internal/awstesting"
2122
"github.com/aws/aws-sdk-go-v2/service/sso"
@@ -82,9 +83,15 @@ func setupCredentialsEndpoints() (aws.EndpointResolverWithOptions, func()) {
8283

8384
ec2MetadataServer := httptest.NewServer(http.HandlerFunc(
8485
func(w http.ResponseWriter, r *http.Request) {
85-
if r.URL.Path == "/latest/meta-data/iam/security-credentials/RoleName" {
86+
if r.URL.Path == "/latest/meta-data/iam/security-credentials-extended/RoleName" {
8687
w.Write([]byte(ec2MetadataResponse))
87-
} else if r.URL.Path == "/latest/meta-data/iam/security-credentials/" {
88+
} else if r.URL.Path == "/latest/meta-data/iam/security-credentials-extended/LoadOptions" {
89+
w.Write([]byte(ec2MetadataResponseLoadOptions))
90+
} else if r.URL.Path == "/latest/meta-data/iam/security-credentials-extended/EnvCfg" {
91+
w.Write([]byte(ec2MetadataResponseEnvCfg))
92+
} else if r.URL.Path == "/latest/meta-data/iam/security-credentials-extended/SharedCfg" {
93+
w.Write([]byte(ec2MetadataResponseSharedCfg))
94+
} else if r.URL.Path == "/latest/meta-data/iam/security-credentials-extended/" {
8895
w.Write([]byte("RoleName"))
8996
} else if r.URL.Path == "/latest/api/token" {
9097
header := w.Header()
@@ -750,6 +757,103 @@ func TestResolveCredentialsEcsContainer(t *testing.T) {
750757

751758
}
752759

760+
func TestResolveCredentialsEC2RoleCreds(t *testing.T) {
761+
testCases := map[string]struct {
762+
expectedAccessKey string
763+
expectedSecretKey string
764+
envVar map[string]string
765+
configFile string
766+
configProfile string
767+
loadOptions func(*LoadOptions) error
768+
}{
769+
"no config whatsoever": {
770+
expectedAccessKey: "ec2-access-key",
771+
expectedSecretKey: "ec2-secret-key",
772+
envVar: map[string]string{},
773+
configFile: "",
774+
},
775+
"env cfg": {
776+
expectedAccessKey: "ec2-access-key-envcfg",
777+
expectedSecretKey: "ec2-secret-key-envcfg",
778+
envVar: map[string]string{
779+
"AWS_EC2_INSTANCE_PROFILE_NAME": "EnvCfg",
780+
},
781+
configFile: "",
782+
},
783+
"shared cfg": {
784+
expectedAccessKey: "ec2-access-key-sharedcfg",
785+
expectedSecretKey: "ec2-secret-key-sharedcfg",
786+
envVar: map[string]string{},
787+
configFile: filepath.Join("testdata", "config_source_shared"),
788+
configProfile: "ec2metadata-profilename",
789+
},
790+
"loadopts + env cfg + shared cfg": {
791+
expectedAccessKey: "ec2-access-key-loadopts",
792+
expectedSecretKey: "ec2-secret-key-loadopts",
793+
envVar: map[string]string{
794+
"AWS_EC2_INSTANCE_PROFILE_NAME": "EnvCfg",
795+
},
796+
configFile: filepath.Join("testdata", "config_source_shared"),
797+
configProfile: "ec2metadata-profilename",
798+
loadOptions: WithEC2RoleCredentialOptions(func(o *ec2rolecreds.Options) {
799+
o.ProfileName = "LoadOptions"
800+
}),
801+
},
802+
}
803+
804+
for name, tc := range testCases {
805+
t.Run(name, func(t *testing.T) {
806+
endpointResolver, cleanupFn := setupCredentialsEndpoints()
807+
defer cleanupFn()
808+
809+
// setupCredentialsEndpoints sets this above and then we hold onto
810+
// it for this test
811+
ec2MetadataURL := os.Getenv("AWS_EC2_METADATA_SERVICE_ENDPOINT")
812+
813+
restoreEnv := awstesting.StashEnv()
814+
defer awstesting.PopEnv(restoreEnv)
815+
816+
os.Setenv("AWS_EC2_METADATA_SERVICE_ENDPOINT", ec2MetadataURL)
817+
for k, v := range tc.envVar {
818+
os.Setenv(k, v)
819+
}
820+
var sharedConfigFiles []string
821+
if tc.configFile != "" {
822+
sharedConfigFiles = append(sharedConfigFiles, tc.configFile)
823+
}
824+
opts := []func(*LoadOptions) error{
825+
WithEndpointResolverWithOptions(endpointResolver),
826+
WithRetryer(func() aws.Retryer { return aws.NopRetryer{} }),
827+
WithSharedConfigFiles(sharedConfigFiles),
828+
WithSharedCredentialsFiles([]string{}),
829+
}
830+
if len(tc.configProfile) != 0 {
831+
opts = append(opts, WithSharedConfigProfile(tc.configProfile))
832+
}
833+
834+
if tc.loadOptions != nil {
835+
opts = append(opts, tc.loadOptions)
836+
}
837+
838+
cfg, err := LoadDefaultConfig(context.TODO(), opts...)
839+
if err != nil {
840+
t.Fatalf("could not load config: %s", err)
841+
}
842+
actual, err := cfg.Credentials.Retrieve(context.TODO())
843+
if err != nil {
844+
t.Fatalf("could not retrieve credentials: %s", err)
845+
}
846+
if actual.AccessKeyID != tc.expectedAccessKey {
847+
t.Errorf("expected access key to be %s, got %s", tc.expectedAccessKey, actual.AccessKeyID)
848+
}
849+
if actual.SecretAccessKey != tc.expectedSecretKey {
850+
t.Errorf("expected secret key to be %s, got %s", tc.expectedSecretKey, actual.SecretAccessKey)
851+
}
852+
})
853+
}
854+
855+
}
856+
753857
type stubErrorClient struct {
754858
err error
755859
}

config/shared_config.go

+16
Original file line numberDiff line numberDiff line change
@@ -82,6 +82,8 @@ const (
8282

8383
ec2MetadataV1DisabledKey = "ec2_metadata_v1_disabled"
8484

85+
ec2InstanceProfileNameKey = "ec2_instance_profile_name"
86+
8587
// Use DualStack Endpoint Resolution
8688
useDualStackEndpoint = "use_dualstack_endpoint"
8789

@@ -357,6 +359,9 @@ type SharedConfig struct {
357359

358360
// ResponseChecksumValidation indicates if the response checksum should be validated
359361
ResponseChecksumValidation aws.ResponseChecksumValidation
362+
363+
// Profile name used for fetching IMDS credentials.
364+
EC2InstanceProfileName string
360365
}
361366

362367
func (c SharedConfig) getDefaultsMode(ctx context.Context) (value aws.DefaultsMode, ok bool, err error) {
@@ -877,6 +882,7 @@ func mergeSections(dst *ini.Sections, src ini.Sections) error {
877882
ec2MetadataServiceEndpointModeKey,
878883
ec2MetadataServiceEndpointKey,
879884
ec2MetadataV1DisabledKey,
885+
ec2InstanceProfileNameKey,
880886
useDualStackEndpoint,
881887
useFIPSEndpointKey,
882888
defaultsModeKey,
@@ -1110,6 +1116,8 @@ func (c *SharedConfig) setFromIniSection(profile string, section ini.Section) er
11101116
updateString(&c.EC2IMDSEndpoint, section, ec2MetadataServiceEndpointKey)
11111117
updateBoolPtr(&c.EC2IMDSv1Disabled, section, ec2MetadataV1DisabledKey)
11121118

1119+
updateString(&c.EC2InstanceProfileName, section, ec2InstanceProfileNameKey)
1120+
11131121
updateUseDualStackEndpoint(&c.UseDualStackEndpoint, section, useDualStackEndpoint)
11141122
updateUseFIPSEndpoint(&c.UseFIPSEndpoint, section, useFIPSEndpointKey)
11151123

@@ -1678,3 +1686,11 @@ func updateUseFIPSEndpoint(dst *aws.FIPSEndpointState, section ini.Section, key
16781686

16791687
return
16801688
}
1689+
1690+
func (c SharedConfig) getEC2InstanceProfileName() (string, bool, error) {
1691+
if len(c.EC2InstanceProfileName) == 0 {
1692+
return "", false, nil
1693+
}
1694+
1695+
return c.EC2InstanceProfileName, true, nil
1696+
}

config/shared_config_test.go

+9
Original file line numberDiff line numberDiff line change
@@ -806,6 +806,15 @@ func TestNewSharedConfig(t *testing.T) {
806806
},
807807
Err: fmt.Errorf("invalid value for shared config profile field, response_checksum_validation=blabla, must be when_supported/when_required"),
808808
},
809+
810+
"profile with ec2 instance profile name": {
811+
ConfigFilenames: []string{testConfigFilename},
812+
Profile: "ec2_instance_profile_name",
813+
Expected: SharedConfig{
814+
Profile: "ec2_instance_profile_name",
815+
EC2InstanceProfileName: "ProfileName",
816+
},
817+
},
809818
}
810819

811820
for name, c := range cases {

config/shared_test.go

+30
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,36 @@ const ec2MetadataResponse = `{
2727
"LastUpdated": "2009-11-23T00:00:00Z"
2828
}`
2929

30+
const ec2MetadataResponseLoadOptions = `{
31+
"Code": "Success",
32+
"Type": "AWS-HMAC",
33+
"AccessKeyId": "ec2-access-key-loadopts",
34+
"SecretAccessKey": "ec2-secret-key-loadopts",
35+
"Token": "token",
36+
"Expiration": "2100-01-01T00:00:00Z",
37+
"LastUpdated": "2009-11-23T00:00:00Z"
38+
}`
39+
40+
const ec2MetadataResponseEnvCfg = `{
41+
"Code": "Success",
42+
"Type": "AWS-HMAC",
43+
"AccessKeyId": "ec2-access-key-envcfg",
44+
"SecretAccessKey": "ec2-secret-key-envcfg",
45+
"Token": "token",
46+
"Expiration": "2100-01-01T00:00:00Z",
47+
"LastUpdated": "2009-11-23T00:00:00Z"
48+
}`
49+
50+
const ec2MetadataResponseSharedCfg = `{
51+
"Code": "Success",
52+
"Type": "AWS-HMAC",
53+
"AccessKeyId": "ec2-access-key-sharedcfg",
54+
"SecretAccessKey": "ec2-secret-key-sharedcfg",
55+
"Token": "token",
56+
"Expiration": "2100-01-01T00:00:00Z",
57+
"LastUpdated": "2009-11-23T00:00:00Z"
58+
}`
59+
3060
const assumeRoleRespMsg = `
3161
<AssumeRoleResponse xmlns="https://sts.amazonaws.com/doc/2011-06-15/">
3262
<AssumeRoleResult>

config/testdata/config_source_shared

+3
Original file line numberDiff line numberDiff line change
@@ -101,3 +101,6 @@ role_arn = webident_arn
101101

102102
[profile webident-partial]
103103
web_identity_token_file = ./testdata/wit.txt
104+
105+
[profile ec2metadata-profilename]
106+
ec2_instance_profile_name = SharedCfg

config/testdata/shared_config

+2
Original file line numberDiff line numberDiff line change
@@ -347,3 +347,5 @@ response_checksum_validation = when_required
347347
[profile response_checksum_validation_error]
348348
response_checksum_validation = blabla
349349

350+
[profile ec2_instance_profile_name]
351+
ec2_instance_profile_name = ProfileName

0 commit comments

Comments
 (0)