Skip to content

Commit ef755b2

Browse files
committed
fixup! [RFC-0010] Add multi-tenant workload identity support for AWS Bucket
Signed-off-by: cappyzawa <[email protected]>
1 parent 1b1fc21 commit ef755b2

File tree

3 files changed

+109
-37
lines changed

3 files changed

+109
-37
lines changed

docs/spec/v1/buckets.md

Lines changed: 0 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -268,24 +268,6 @@ data:
268268
secretkey: <BASE64>
269269
```
270270

271-
##### AWS Controller-Level Workload Identity example
272-
273-
```yaml
274-
---
275-
apiVersion: source.toolkit.fluxcd.io/v1
276-
kind: Bucket
277-
metadata:
278-
name: aws-controller-level-workload-identity
279-
namespace: default
280-
spec:
281-
interval: 5m0s
282-
provider: aws
283-
bucketName: podinfo
284-
endpoint: s3.amazonaws.com
285-
region: us-east-1
286-
timeout: 30s
287-
```
288-
289271
##### AWS Object-Level Workload Identity example
290272

291273
**Note:** To use Object-Level Workload Identity (`.spec.serviceAccountName` with

internal/bucket/minio/minio.go

Lines changed: 35 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -116,7 +116,7 @@ func NewClient(ctx context.Context, bucket *sourcev1.Bucket, opts ...Option) (*M
116116
case o.secret != nil:
117117
minioOpts.Creds = newCredsFromSecret(o.secret)
118118
case bucketProvider == sourcev1.BucketProviderAmazon:
119-
creds, err := newAWSCreds(ctx, &o)
119+
creds, err := newAWSCreds(ctx, bucket, &o)
120120
if err != nil {
121121
return nil, err
122122
}
@@ -177,27 +177,45 @@ func newCredsFromSecret(secret *corev1.Secret) *credentials.Credentials {
177177
//
178178
// This function is only called when Secret authentication is not available.
179179
//
180-
// Uses AWS SDK's config.LoadDefaultConfig() which supports:
181-
// - Workload Identity (IRSA/EKS Pod Identity)
182-
// - EC2 instance profiles
183-
// - Environment variables
184-
// - Shared credentials files
185-
// - All other AWS SDK authentication methods
186-
func newAWSCreds(ctx context.Context, o *options) (*credentials.Credentials, error) {
180+
// Authentication priority order:
181+
// 1. Object Level Workload Identity (if serviceAccountName is specified)
182+
// 2. AWS credential chain (EC2 instance profiles, environment variables, etc.)
183+
func newAWSCreds(ctx context.Context, bucket *sourcev1.Bucket, o *options) (*credentials.Credentials, error) {
187184
var opts auth.Options
188185
opts.Apply(o.authOpts...)
189186

190-
awsCredsProvider := awsauth.NewCredentialsProvider(ctx, o.authOpts...)
191-
awsCreds, err := awsCredsProvider.Retrieve(ctx)
192-
if err != nil {
193-
return nil, fmt.Errorf("AWS authentication failed: %w", err)
187+
// 1. Try Object Level Workload Identity if serviceAccountName is specified
188+
if opts.ServiceAccount != nil {
189+
awsCredsProvider := awsauth.NewCredentialsProvider(ctx, o.authOpts...)
190+
awsCreds, err := awsCredsProvider.Retrieve(ctx)
191+
if err != nil {
192+
return nil, fmt.Errorf("Object Level Workload Identity authentication failed: %w", err)
193+
}
194+
195+
return credentials.NewStaticV4(
196+
awsCreds.AccessKeyID,
197+
awsCreds.SecretAccessKey,
198+
awsCreds.SessionToken,
199+
), nil
194200
}
195201

196-
return credentials.NewStaticV4(
197-
awsCreds.AccessKeyID,
198-
awsCreds.SecretAccessKey,
199-
awsCreds.SessionToken,
200-
), nil
202+
// 2. AWS credential chain
203+
stsEndpoint := ""
204+
if sts := bucket.Spec.STS; sts != nil {
205+
stsEndpoint = sts.Endpoint
206+
}
207+
208+
if o.proxyURL != nil {
209+
transport := http.DefaultTransport.(*http.Transport).Clone()
210+
transport.Proxy = http.ProxyURL(o.proxyURL)
211+
client := &http.Client{Transport: transport}
212+
return credentials.New(&credentials.IAM{
213+
Client: client,
214+
Endpoint: stsEndpoint,
215+
}), nil
216+
} else {
217+
return credentials.NewIAM(stsEndpoint), nil
218+
}
201219
}
202220

203221
// newGenericCreds creates a new Minio credentials object for the `generic` bucket provider.

internal/bucket/minio/minio_test.go

Lines changed: 74 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ import (
2020
"context"
2121
"crypto/tls"
2222
"crypto/x509"
23+
"encoding/json"
2324
"encoding/xml"
2425
"errors"
2526
"fmt"
@@ -249,8 +250,8 @@ func TestNewClientAWSProvider(t *testing.T) {
249250
t.Run("without secret", func(t *testing.T) {
250251
bucket := bucketStub(bucketAwsProvider, testMinioAddress)
251252
minioClient, err := NewClient(ctx, bucket)
252-
assert.ErrorContains(t, err, "AWS authentication failed")
253-
assert.Assert(t, minioClient == nil)
253+
assert.NilError(t, err)
254+
assert.Assert(t, minioClient != nil)
254255
})
255256
}
256257

@@ -274,8 +275,43 @@ func TestFGetObject(t *testing.T) {
274275
}
275276

276277
func TestNewClientAndFGetObjectWithSTSEndpoint(t *testing.T) {
278+
// start a mock AWS STS server
279+
awsSTSListener, awsSTSAddr, awsSTSPort := testlistener.New(t)
280+
awsSTSEndpoint := fmt.Sprintf("http://%s", awsSTSAddr)
281+
awsSTSHandler := http.NewServeMux()
282+
awsSTSHandler.HandleFunc("PUT "+credentials.TokenPath,
283+
func(w http.ResponseWriter, r *http.Request) {
284+
_, err := w.Write([]byte("mock-token"))
285+
assert.NilError(t, err)
286+
})
287+
awsSTSHandler.HandleFunc("GET "+credentials.DefaultIAMSecurityCredsPath,
288+
func(w http.ResponseWriter, r *http.Request) {
289+
token := r.Header.Get(credentials.TokenRequestHeader)
290+
assert.Equal(t, token, "mock-token")
291+
_, err := w.Write([]byte("mock-role"))
292+
assert.NilError(t, err)
293+
})
277294
var credsRetrieved bool
278295

296+
awsSTSHandler.HandleFunc("GET "+credentials.DefaultIAMSecurityCredsPath+"mock-role",
297+
func(w http.ResponseWriter, r *http.Request) {
298+
token := r.Header.Get(credentials.TokenRequestHeader)
299+
assert.Equal(t, token, "mock-token")
300+
err := json.NewEncoder(w).Encode(map[string]any{
301+
"Code": "Success",
302+
"AccessKeyID": testMinioRootUser,
303+
"SecretAccessKey": testMinioRootPassword,
304+
})
305+
assert.NilError(t, err)
306+
credsRetrieved = true
307+
})
308+
awsSTSServer := &http.Server{
309+
Addr: awsSTSAddr,
310+
Handler: awsSTSHandler,
311+
}
312+
go awsSTSServer.Serve(awsSTSListener)
313+
defer awsSTSServer.Shutdown(context.Background())
314+
279315
// start a mock LDAP STS server
280316
ldapSTSListener, ldapSTSAddr, ldapSTSPort := testlistener.New(t)
281317
ldapSTSEndpoint := fmt.Sprintf("https://%s", ldapSTSAddr)
@@ -315,6 +351,42 @@ func TestNewClientAndFGetObjectWithSTSEndpoint(t *testing.T) {
315351
ldapPassword string
316352
err string
317353
}{
354+
{
355+
name: "with correct aws endpoint",
356+
provider: "aws",
357+
stsSpec: &sourcev1.BucketSTSSpec{
358+
Provider: "aws",
359+
Endpoint: awsSTSEndpoint,
360+
},
361+
},
362+
{
363+
name: "with incorrect aws endpoint",
364+
provider: "aws",
365+
stsSpec: &sourcev1.BucketSTSSpec{
366+
Provider: "aws",
367+
Endpoint: fmt.Sprintf("http://localhost:%d", awsSTSPort+1),
368+
},
369+
err: "connection refused",
370+
},
371+
{
372+
name: "with correct aws endpoint and proxy",
373+
provider: "aws",
374+
stsSpec: &sourcev1.BucketSTSSpec{
375+
Provider: "aws",
376+
Endpoint: awsSTSEndpoint,
377+
},
378+
opts: []Option{WithProxyURL(&url.URL{Scheme: "http", Host: proxyAddr})},
379+
},
380+
{
381+
name: "with correct aws endpoint and incorrect proxy",
382+
provider: "aws",
383+
stsSpec: &sourcev1.BucketSTSSpec{
384+
Provider: "aws",
385+
Endpoint: awsSTSEndpoint,
386+
},
387+
opts: []Option{WithProxyURL(&url.URL{Scheme: "http", Host: fmt.Sprintf("localhost:%d", proxyPort+1)})},
388+
err: "connection refused",
389+
},
318390
{
319391
name: "with correct ldap endpoint",
320392
provider: "generic",

0 commit comments

Comments
 (0)