Skip to content

Commit f2b5cae

Browse files
EKS Pod Identity support for driver level credentials (#451)
This PR is a follow-up to #449 because we need the E2E tests to run with the newly added `cluster-name` parameter. This is not possible in a PR created from a fork. *Description of changes:* With this change, we will support the configuration of driver level credentials with EKS Pod Identity. In EKS clusters, this is an alternative to IRSA with an easier configuration process. *Key Changes:* - Credential Provider - In `provider_driver.go` the container credentials used for EKS Pod Identity are fetched from the driver pod and forwarded to the Mountpoint process. This is done similarly to STS Web Identity credentials used for IRSA. - Added unit tests covering different credential setup scenarios (including combinations) - Testing - Added cluster name as parameter for E2E tests - Added E2e tests covering EKS Pod Identity setup scenarios with IAM roles of varying levels of S3 access - Manual testing performed on personal EKS cluster By submitting this pull request, I confirm that you can use, modify, copy, and redistribute this contribution, under the terms of your choice. --------- Signed-off-by: Renan Magagnin <renanmag@amazon.co.uk>
1 parent d629846 commit f2b5cae

10 files changed

Lines changed: 433 additions & 46 deletions

File tree

pkg/driver/node/credentialprovider/provider_driver.go

Lines changed: 36 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,8 @@ import (
1313
)
1414

1515
const (
16-
driverLevelServiceAccountTokenName = "token"
16+
webIdentityServiceAccountTokenName = "token"
17+
eksPodIdentityServiceAccountTokenName = "eks-pod-identity-token"
1718
)
1819

1920
// provideFromDriver provides driver-level AWS credentials.
@@ -26,6 +27,7 @@ func (c *Provider) provideFromDriver(provideCtx ProvideContext) (envprovider.Env
2627
accessKeyID := os.Getenv(envprovider.EnvAccessKeyID)
2728
secretAccessKey := os.Getenv(envprovider.EnvSecretAccessKey)
2829
if accessKeyID != "" && secretAccessKey != "" {
30+
klog.V(4).Infof("Providing credentials from driver with Long-term AWS credentials")
2931
sessionToken := os.Getenv(envprovider.EnvSessionToken)
3032
longTermCredsEnv, err := provideLongTermCredentialsFromDriver(provideCtx, accessKeyID, secretAccessKey, sessionToken)
3133
if err != nil {
@@ -37,6 +39,7 @@ func (c *Provider) provideFromDriver(provideCtx ProvideContext) (envprovider.Env
3739
} else {
3840
// Profile provider
3941
// TODO: This is not officially supported and won't work by default with containerization.
42+
klog.V(4).Infof("Providing credentials from driver with Profile provider")
4043
configFile := os.Getenv(envprovider.EnvConfigFile)
4144
sharedCredentialsFile := os.Getenv(envprovider.EnvSharedCredentialsFile)
4245
if configFile != "" && sharedCredentialsFile != "" {
@@ -45,10 +48,11 @@ func (c *Provider) provideFromDriver(provideCtx ProvideContext) (envprovider.Env
4548
}
4649
}
4750

48-
// STS Web Identity provider
51+
// STS Web Identity provider (IRSA)
4952
webIdentityTokenFile := os.Getenv(envprovider.EnvWebIdentityTokenFile)
5053
roleARN := os.Getenv(envprovider.EnvRoleARN)
5154
if webIdentityTokenFile != "" && roleARN != "" {
55+
klog.V(4).Infof("Providing credentials from driver with STS Web Identity provider (IRSA)")
5256
stsWebIdentityCredsEnv, err := provideStsWebIdentityCredentialsFromDriver(provideCtx)
5357
if err != nil {
5458
klog.V(4).ErrorS(err, "credentialprovider: Failed to provide STS Web Identity credentials from driver")
@@ -58,6 +62,19 @@ func (c *Provider) provideFromDriver(provideCtx ProvideContext) (envprovider.Env
5862
env.Merge(stsWebIdentityCredsEnv)
5963
}
6064

65+
// Container credential provider (EKS Pod Identity)
66+
containerAuthorizationTokenFile := os.Getenv(envprovider.EnvContainerAuthorizationTokenFile)
67+
containerCredentialsFullURI := os.Getenv(envprovider.EnvContainerCredentialsFullURI)
68+
if util.UsePodMounter() && containerAuthorizationTokenFile != "" && containerCredentialsFullURI != "" {
69+
klog.V(4).Infof("Providing credentials from driver with Container credential provider (EKS Pod Identity)")
70+
containerCredsEnv, err := provideContainerCredentialsFromDriver(provideCtx, containerAuthorizationTokenFile, containerCredentialsFullURI)
71+
if err != nil {
72+
klog.V(4).ErrorS(err, "credentialprovider: Failed to provide container credentials from driver")
73+
return nil, err
74+
}
75+
env.Merge(containerCredsEnv)
76+
}
77+
6178
return env, nil
6279
}
6380

@@ -74,15 +91,30 @@ func (c *Provider) cleanupFromDriver(cleanupCtx CleanupContext) error {
7491
// It basically copies driver's injected service account token to [provideCtx.WritePath].
7592
func provideStsWebIdentityCredentialsFromDriver(provideCtx ProvideContext) (envprovider.Environment, error) {
7693
driverServiceAccountTokenFile := os.Getenv(envprovider.EnvWebIdentityTokenFile)
77-
tokenFile := filepath.Join(provideCtx.WritePath, driverLevelServiceAccountTokenName)
94+
tokenFile := filepath.Join(provideCtx.WritePath, webIdentityServiceAccountTokenName)
7895
err := util.ReplaceFile(tokenFile, driverServiceAccountTokenFile, CredentialFilePerm)
7996
if err != nil {
8097
return nil, fmt.Errorf("credentialprovider: sts-web-identity: failed to copy driver's service account token: %w", err)
8198
}
8299

83100
return envprovider.Environment{
84101
envprovider.EnvRoleARN: os.Getenv(envprovider.EnvRoleARN),
85-
envprovider.EnvWebIdentityTokenFile: filepath.Join(provideCtx.EnvPath, driverLevelServiceAccountTokenName),
102+
envprovider.EnvWebIdentityTokenFile: filepath.Join(provideCtx.EnvPath, webIdentityServiceAccountTokenName),
103+
}, nil
104+
}
105+
106+
// provideContainerCredentialsFromDriver provides Container credentials from the driver's service account.
107+
// It basically copies driver's injected service account token to [provideCtx.WritePath].
108+
func provideContainerCredentialsFromDriver(provideCtx ProvideContext, containerAuthorizationTokenFile string, containerCredentialsFullURI string) (envprovider.Environment, error) {
109+
tokenFile := filepath.Join(provideCtx.WritePath, eksPodIdentityServiceAccountTokenName)
110+
err := util.ReplaceFile(tokenFile, containerAuthorizationTokenFile, CredentialFilePerm)
111+
if err != nil {
112+
return nil, fmt.Errorf("credentialprovider: container: failed to copy driver's service account token: %w", err)
113+
}
114+
115+
return envprovider.Environment{
116+
envprovider.EnvContainerAuthorizationTokenFile: filepath.Join(provideCtx.EnvPath, eksPodIdentityServiceAccountTokenName),
117+
envprovider.EnvContainerCredentialsFullURI: containerCredentialsFullURI,
86118
}, nil
87119
}
88120

pkg/driver/node/credentialprovider/provider_test.go

Lines changed: 127 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -28,12 +28,16 @@ const testSessionToken = "test-session-token"
2828
const testRoleARN = "arn:aws:iam::111122223333:role/pod-a-role"
2929
const testWebIdentityToken = "test-web-identity-token"
3030

31+
const testContainerAuthorizationToken = "test-container-authorization-token"
32+
const testContainerCredentialsFullURI = "http://169.254.170.23/v1/credentials"
33+
3134
const testPodID = "2a17db00-0bf3-4052-9b3f-6c89dcee5d79"
3235
const testVolumeID = "test-vol"
3336
const testProfilePrefix = testPodID + "-" + testVolumeID + "-"
3437

3538
const testPodLevelServiceAccountToken = testPodID + "-" + testVolumeID + ".token"
36-
const testDriverLevelServiceAccountToken = "token"
39+
const testWebIdentityServiceAccountToken = "token"
40+
const testEKSPodIdentityServiceAccountToken = "eks-pod-identity-token"
3741

3842
const testPodServiceAccount = "test-sa"
3943
const testPodNamespace = "test-ns"
@@ -98,9 +102,27 @@ func TestProvidingDriverLevelCredentials(t *testing.T) {
98102
assert.Equals(t, credentialprovider.AuthenticationSourceDriver, source)
99103
assert.Equals(t, envprovider.Environment{
100104
"AWS_ROLE_ARN": testRoleARN,
101-
"AWS_WEB_IDENTITY_TOKEN_FILE": filepath.Join(testEnvPath, testDriverLevelServiceAccountToken),
105+
"AWS_WEB_IDENTITY_TOKEN_FILE": filepath.Join(testEnvPath, testWebIdentityServiceAccountToken),
106+
}, env)
107+
assertWebIdentityTokenFile(t, filepath.Join(writePath, testWebIdentityServiceAccountToken))
108+
}
109+
})
110+
111+
t.Run("only container credentials", func(t *testing.T) {
112+
for _, authSource := range authenticationSourceVariants {
113+
setEnvForContainerCredentials(t)
114+
115+
writePath := t.TempDir()
116+
provideCtx := provideCtx(t, writePath, authSource)
117+
118+
env, source, err := provider.Provide(context.Background(), provideCtx)
119+
assert.NoError(t, err)
120+
assert.Equals(t, credentialprovider.AuthenticationSourceDriver, source)
121+
assert.Equals(t, envprovider.Environment{
122+
"AWS_CONTAINER_AUTHORIZATION_TOKEN_FILE": filepath.Join(testEnvPath, testEKSPodIdentityServiceAccountToken),
123+
"AWS_CONTAINER_CREDENTIALS_FULL_URI": testContainerCredentialsFullURI,
102124
}, env)
103-
assertWebIdentityTokenFile(t, filepath.Join(writePath, testDriverLevelServiceAccountToken))
125+
assertContainerTokenFile(t, filepath.Join(writePath, testEKSPodIdentityServiceAccountToken))
104126
}
105127
})
106128

@@ -148,10 +170,55 @@ func TestProvidingDriverLevelCredentials(t *testing.T) {
148170
"AWS_CONFIG_FILE": "/test-env/" + testProfilePrefix + "s3-csi-config",
149171
"AWS_SHARED_CREDENTIALS_FILE": "/test-env/" + testProfilePrefix + "s3-csi-credentials",
150172
"AWS_ROLE_ARN": testRoleARN,
151-
"AWS_WEB_IDENTITY_TOKEN_FILE": filepath.Join(testEnvPath, testDriverLevelServiceAccountToken),
173+
"AWS_WEB_IDENTITY_TOKEN_FILE": filepath.Join(testEnvPath, testWebIdentityServiceAccountToken),
174+
}, env)
175+
assertLongTermCredentials(t, writePath)
176+
assertWebIdentityTokenFile(t, filepath.Join(writePath, testWebIdentityServiceAccountToken))
177+
}
178+
})
179+
180+
t.Run("long-term and container credentials", func(t *testing.T) {
181+
for _, authSource := range authenticationSourceVariants {
182+
setEnvForLongTermCredentials(t)
183+
setEnvForContainerCredentials(t)
184+
185+
writePath := t.TempDir()
186+
provideCtx := provideCtx(t, writePath, authSource)
187+
188+
env, source, err := provider.Provide(context.Background(), provideCtx)
189+
assert.NoError(t, err)
190+
assert.Equals(t, credentialprovider.AuthenticationSourceDriver, source)
191+
assert.Equals(t, envprovider.Environment{
192+
"AWS_PROFILE": testProfilePrefix + "s3-csi",
193+
"AWS_CONFIG_FILE": "/test-env/" + testProfilePrefix + "s3-csi-config",
194+
"AWS_SHARED_CREDENTIALS_FILE": "/test-env/" + testProfilePrefix + "s3-csi-credentials",
195+
"AWS_CONTAINER_AUTHORIZATION_TOKEN_FILE": filepath.Join(testEnvPath, testEKSPodIdentityServiceAccountToken),
196+
"AWS_CONTAINER_CREDENTIALS_FULL_URI": testContainerCredentialsFullURI,
152197
}, env)
153198
assertLongTermCredentials(t, writePath)
154-
assertWebIdentityTokenFile(t, filepath.Join(writePath, testDriverLevelServiceAccountToken))
199+
assertContainerTokenFile(t, filepath.Join(writePath, testEKSPodIdentityServiceAccountToken))
200+
}
201+
})
202+
203+
t.Run("sts web identity credentials and containter credentials", func(t *testing.T) {
204+
for _, authSource := range authenticationSourceVariants {
205+
setEnvForContainerCredentials(t)
206+
setEnvForStsWebIdentityCredentials(t)
207+
208+
writePath := t.TempDir()
209+
provideCtx := provideCtx(t, writePath, authSource)
210+
211+
env, source, err := provider.Provide(context.Background(), provideCtx)
212+
assert.NoError(t, err)
213+
assert.Equals(t, credentialprovider.AuthenticationSourceDriver, source)
214+
assert.Equals(t, envprovider.Environment{
215+
"AWS_ROLE_ARN": testRoleARN,
216+
"AWS_WEB_IDENTITY_TOKEN_FILE": filepath.Join(testEnvPath, testWebIdentityServiceAccountToken),
217+
"AWS_CONTAINER_AUTHORIZATION_TOKEN_FILE": filepath.Join(testEnvPath, testEKSPodIdentityServiceAccountToken),
218+
"AWS_CONTAINER_CREDENTIALS_FULL_URI": testContainerCredentialsFullURI,
219+
}, env)
220+
assertContainerTokenFile(t, filepath.Join(writePath, testEKSPodIdentityServiceAccountToken))
221+
assertWebIdentityTokenFile(t, filepath.Join(writePath, testWebIdentityServiceAccountToken))
155222
}
156223
})
157224

@@ -215,6 +282,31 @@ func TestProvidingDriverLevelCredentials(t *testing.T) {
215282
assert.Equals(t, envprovider.Environment{}, env)
216283
})
217284

285+
t.Run("incomplete container credentials", func(t *testing.T) {
286+
// Only set container credentials full URI without token file
287+
t.Setenv("AWS_CONTAINER_CREDENTIALS_FULL_URI", testContainerCredentialsFullURI)
288+
289+
provider := credentialprovider.New(nil, dummyRegionProvider)
290+
291+
provideCtx := provideCtx(t, t.TempDir(), credentialprovider.AuthenticationSourceDriver)
292+
293+
env, source, err := provider.Provide(context.Background(), provideCtx)
294+
assert.NoError(t, err)
295+
assert.Equals(t, credentialprovider.AuthenticationSourceDriver, source)
296+
assert.Equals(t, envprovider.Environment{}, env)
297+
298+
// Only set token file without role ARN
299+
tokenPath := filepath.Join(t.TempDir(), "token")
300+
assert.NoError(t, os.WriteFile(tokenPath, []byte(testContainerAuthorizationToken), 0600))
301+
t.Setenv("AWS_ROLE_ARN", "")
302+
t.Setenv("AWS_WEB_IDENTITY_TOKEN_FILE", tokenPath)
303+
304+
env, source, err = provider.Provide(context.Background(), provideCtx)
305+
assert.NoError(t, err)
306+
assert.Equals(t, credentialprovider.AuthenticationSourceDriver, source)
307+
assert.Equals(t, envprovider.Environment{}, env)
308+
})
309+
218310
t.Run("no credentials", func(t *testing.T) {
219311
for _, authSource := range authenticationSourceVariants {
220312
writePath := t.TempDir()
@@ -817,6 +909,16 @@ func TestCleanup(t *testing.T) {
817909

818910
//-- Utilities for tests
819911

912+
func provideCtx(t *testing.T, writePath string, authSource string) credentialprovider.ProvideContext {
913+
return credentialprovider.ProvideContext{
914+
AuthenticationSource: authSource,
915+
WritePath: writePath,
916+
EnvPath: testEnvPath,
917+
PodID: testPodID,
918+
VolumeID: testVolumeID,
919+
}
920+
}
921+
820922
func setEnvForLongTermCredentials(t *testing.T) {
821923
t.Setenv("AWS_ACCESS_KEY_ID", testAccessKeyID)
822924
t.Setenv("AWS_SECRET_ACCESS_KEY", testSecretAccessKey)
@@ -848,6 +950,18 @@ func setEnvForStsWebIdentityCredentials(t *testing.T) {
848950
t.Setenv("AWS_WEB_IDENTITY_TOKEN_FILE", tokenPath)
849951
}
850952

953+
func setEnvForContainerCredentials(t *testing.T) {
954+
t.Helper()
955+
956+
t.Setenv("MOUNTER_KIND", "pod")
957+
958+
tokenPath := filepath.Join(t.TempDir(), "token")
959+
assert.NoError(t, os.WriteFile(tokenPath, []byte(testContainerAuthorizationToken), 0600))
960+
961+
t.Setenv("AWS_CONTAINER_AUTHORIZATION_TOKEN_FILE", tokenPath)
962+
t.Setenv("AWS_CONTAINER_CREDENTIALS_FULL_URI", testContainerCredentialsFullURI)
963+
}
964+
851965
func assertWebIdentityTokenFile(t *testing.T, path string) {
852966
t.Helper()
853967

@@ -856,6 +970,14 @@ func assertWebIdentityTokenFile(t *testing.T, path string) {
856970
assert.Equals(t, []byte(testWebIdentityToken), got)
857971
}
858972

973+
func assertContainerTokenFile(t *testing.T, path string) {
974+
t.Helper()
975+
976+
got, err := os.ReadFile(path)
977+
assert.NoError(t, err)
978+
assert.Equals(t, []byte(testContainerAuthorizationToken), got)
979+
}
980+
859981
type tokens = map[string]struct {
860982
Token string `json:"token"`
861983
ExpirationTimestamp time.Time

pkg/driver/node/envprovider/provider.go

Lines changed: 16 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -9,20 +9,22 @@ import (
99
)
1010

1111
const (
12-
EnvRegion = "AWS_REGION"
13-
EnvDefaultRegion = "AWS_DEFAULT_REGION"
14-
EnvSTSRegionalEndpoints = "AWS_STS_REGIONAL_ENDPOINTS"
15-
EnvMaxAttempts = "AWS_MAX_ATTEMPTS"
16-
EnvProfile = "AWS_PROFILE"
17-
EnvConfigFile = "AWS_CONFIG_FILE"
18-
EnvSharedCredentialsFile = "AWS_SHARED_CREDENTIALS_FILE"
19-
EnvRoleARN = "AWS_ROLE_ARN"
20-
EnvWebIdentityTokenFile = "AWS_WEB_IDENTITY_TOKEN_FILE"
21-
EnvEC2MetadataDisabled = "AWS_EC2_METADATA_DISABLED"
22-
EnvAccessKeyID = "AWS_ACCESS_KEY_ID"
23-
EnvSecretAccessKey = "AWS_SECRET_ACCESS_KEY"
24-
EnvSessionToken = "AWS_SESSION_TOKEN"
25-
EnvMountpointCacheKey = "UNSTABLE_MOUNTPOINT_CACHE_KEY"
12+
EnvRegion = "AWS_REGION"
13+
EnvDefaultRegion = "AWS_DEFAULT_REGION"
14+
EnvSTSRegionalEndpoints = "AWS_STS_REGIONAL_ENDPOINTS"
15+
EnvMaxAttempts = "AWS_MAX_ATTEMPTS"
16+
EnvProfile = "AWS_PROFILE"
17+
EnvConfigFile = "AWS_CONFIG_FILE"
18+
EnvSharedCredentialsFile = "AWS_SHARED_CREDENTIALS_FILE"
19+
EnvRoleARN = "AWS_ROLE_ARN"
20+
EnvWebIdentityTokenFile = "AWS_WEB_IDENTITY_TOKEN_FILE"
21+
EnvContainerAuthorizationTokenFile = "AWS_CONTAINER_AUTHORIZATION_TOKEN_FILE"
22+
EnvContainerCredentialsFullURI = "AWS_CONTAINER_CREDENTIALS_FULL_URI"
23+
EnvEC2MetadataDisabled = "AWS_EC2_METADATA_DISABLED"
24+
EnvAccessKeyID = "AWS_ACCESS_KEY_ID"
25+
EnvSecretAccessKey = "AWS_SECRET_ACCESS_KEY"
26+
EnvSessionToken = "AWS_SESSION_TOKEN"
27+
EnvMountpointCacheKey = "UNSTABLE_MOUNTPOINT_CACHE_KEY"
2628
)
2729

2830
// Key represents an environment variable name.

tests/e2e-kubernetes/e2e_test.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ func init() {
2323

2424
flag.StringVar(&CommitId, "commit-id", "local", "commit id will be used to name buckets")
2525
flag.StringVar(&BucketRegion, "bucket-region", "us-east-1", "region where temporary buckets will be created")
26+
flag.StringVar(&ClusterName, "cluster-name", "", "name of the cluster")
2627
flag.StringVar(&BucketPrefix, "bucket-prefix", "local", "prefix for temporary buckets")
2728
flag.BoolVar(&Performance, "performance", false, "run performance tests")
2829
flag.BoolVar(&IMDSAvailable, "imds-available", false, "indicates whether instance metadata service is available")
@@ -31,6 +32,7 @@ func init() {
3132

3233
s3client.DefaultRegion = BucketRegion
3334
custom_testsuites.DefaultRegion = BucketRegion
35+
custom_testsuites.ClusterName = ClusterName
3436
custom_testsuites.IMDSAvailable = IMDSAvailable
3537
custom_testsuites.IsPodMounter = IsPodMounter
3638
}

tests/e2e-kubernetes/go.mod

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ module github.com/awslabs/aws-s3-csi-driver/tests/e2e-kubernetes
33
go 1.24
44

55
require (
6-
github.com/aws/aws-sdk-go-v2 v1.30.5
6+
github.com/aws/aws-sdk-go-v2 v1.36.3
77
github.com/aws/aws-sdk-go-v2/config v1.27.33
88
github.com/aws/aws-sdk-go-v2/service/iam v1.34.3
99
github.com/aws/aws-sdk-go-v2/service/s3 v1.47.3
@@ -29,17 +29,19 @@ require (
2929
github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.5.3 // indirect
3030
github.com/aws/aws-sdk-go-v2/credentials v1.17.32 // indirect
3131
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.13 // indirect
32-
github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.17 // indirect
33-
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.17 // indirect
32+
github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.34 // indirect
33+
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.34 // indirect
3434
github.com/aws/aws-sdk-go-v2/internal/ini v1.8.1 // indirect
3535
github.com/aws/aws-sdk-go-v2/internal/v4a v1.2.8 // indirect
36+
github.com/aws/aws-sdk-go-v2/service/eks v1.64.0
37+
github.com/aws/aws-sdk-go-v2/service/eksauth v1.8.2
3638
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.11.4 // indirect
3739
github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.2.8 // indirect
3840
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.11.19 // indirect
3941
github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.16.8 // indirect
4042
github.com/aws/aws-sdk-go-v2/service/sso v1.22.7 // indirect
4143
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.26.7 // indirect
42-
github.com/aws/smithy-go v1.20.4 // indirect
44+
github.com/aws/smithy-go v1.22.2 // indirect
4345
github.com/beorn7/perks v1.0.1 // indirect
4446
github.com/blang/semver/v4 v4.0.0 // indirect
4547
github.com/cenkalti/backoff/v4 v4.3.0 // indirect

0 commit comments

Comments
 (0)