Skip to content

Commit d550312

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

File tree

7 files changed

+223
-40
lines changed

7 files changed

+223
-40
lines changed

api/v1/bucket_types.go

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,8 @@ const (
3333
// BucketProviderGeneric for any S3 API compatible storage Bucket.
3434
BucketProviderGeneric string = "generic"
3535
// BucketProviderAmazon for an AWS S3 object storage Bucket.
36-
// Provides support for retrieving credentials from the AWS EC2 service.
36+
// Provides support for retrieving credentials from the AWS EC2 service
37+
// and workload identity authentication.
3738
BucketProviderAmazon string = "aws"
3839
// BucketProviderGoogle for a Google Cloud Storage Bucket.
3940
// Provides support for authentication using a workload identity.
@@ -51,7 +52,7 @@ const (
5152
// +kubebuilder:validation:XValidation:rule="self.provider != 'generic' || !has(self.sts) || self.sts.provider == 'ldap'", message="'ldap' is the only supported STS provider for the 'generic' Bucket provider"
5253
// +kubebuilder:validation:XValidation:rule="!has(self.sts) || self.sts.provider != 'aws' || !has(self.sts.secretRef)", message="spec.sts.secretRef is not required for the 'aws' STS provider"
5354
// +kubebuilder:validation:XValidation:rule="!has(self.sts) || self.sts.provider != 'aws' || !has(self.sts.certSecretRef)", message="spec.sts.certSecretRef is not required for the 'aws' STS provider"
54-
// +kubebuilder:validation:XValidation:rule="self.provider == 'gcp' || !has(self.serviceAccountName)", message="ServiceAccountName is only supported for the 'gcp' Bucket provider"
55+
// +kubebuilder:validation:XValidation:rule="self.provider == 'gcp' || self.provider == 'aws' || !has(self.serviceAccountName)", message="ServiceAccountName is only supported for the 'gcp' and 'aws' Bucket providers"
5556
// +kubebuilder:validation:XValidation:rule="!has(self.secretRef) || !has(self.serviceAccountName)", message="cannot set both .spec.secretRef and .spec.serviceAccountName"
5657
type BucketSpec struct {
5758
// Provider of the object storage bucket.
@@ -96,7 +97,8 @@ type BucketSpec struct {
9697
SecretRef *meta.LocalObjectReference `json:"secretRef,omitempty"`
9798

9899
// ServiceAccountName is the name of the Kubernetes ServiceAccount used to authenticate
99-
// the bucket. For more information about workload identity:
100+
// the bucket. This field is only supported for the 'gcp' and 'aws' providers.
101+
// For more information about workload identity:
100102
// https://fluxcd.io/flux/components/source/buckets/#workload-identity
101103
// +optional
102104
ServiceAccountName string `json:"serviceAccountName,omitempty"`

config/crd/bases/source.toolkit.fluxcd.io_buckets.yaml

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -145,7 +145,8 @@ spec:
145145
serviceAccountName:
146146
description: |-
147147
ServiceAccountName is the name of the Kubernetes ServiceAccount used to authenticate
148-
the bucket. For more information about workload identity:
148+
the bucket. This field is only supported for the 'gcp' and 'aws' providers.
149+
For more information about workload identity:
149150
https://fluxcd.io/flux/components/source/buckets/#workload-identity
150151
type: string
151152
sts:
@@ -238,8 +239,9 @@ spec:
238239
rule: '!has(self.sts) || self.sts.provider != ''aws'' || !has(self.sts.secretRef)'
239240
- message: spec.sts.certSecretRef is not required for the 'aws' STS provider
240241
rule: '!has(self.sts) || self.sts.provider != ''aws'' || !has(self.sts.certSecretRef)'
241-
- message: ServiceAccountName is only supported for the 'gcp' Bucket provider
242-
rule: self.provider == 'gcp' || !has(self.serviceAccountName)
242+
- message: ServiceAccountName is only supported for the 'gcp' and 'aws'
243+
Bucket providers
244+
rule: self.provider == 'gcp' || self.provider == 'aws' || !has(self.serviceAccountName)
243245
- message: cannot set both .spec.secretRef and .spec.serviceAccountName
244246
rule: '!has(self.secretRef) || !has(self.serviceAccountName)'
245247
status:

docs/api/v1/source.md

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -190,7 +190,8 @@ string
190190
<td>
191191
<em>(Optional)</em>
192192
<p>ServiceAccountName is the name of the Kubernetes ServiceAccount used to authenticate
193-
the bucket. For more information about workload identity:
193+
the bucket. This field is only supported for the &lsquo;gcp&rsquo; and &lsquo;aws&rsquo; providers.
194+
For more information about workload identity:
194195
<a href="https://fluxcd.io/flux/components/source/buckets/#workload-identity">https://fluxcd.io/flux/components/source/buckets/#workload-identity</a></p>
195196
</td>
196197
</tr>
@@ -1646,7 +1647,8 @@ string
16461647
<td>
16471648
<em>(Optional)</em>
16481649
<p>ServiceAccountName is the name of the Kubernetes ServiceAccount used to authenticate
1649-
the bucket. For more information about workload identity:
1650+
the bucket. This field is only supported for the &lsquo;gcp&rsquo; and &lsquo;aws&rsquo; providers.
1651+
For more information about workload identity:
16501652
<a href="https://fluxcd.io/flux/components/source/buckets/#workload-identity">https://fluxcd.io/flux/components/source/buckets/#workload-identity</a></p>
16511653
</td>
16521654
</tr>

docs/spec/v1/buckets.md

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -273,6 +273,55 @@ data:
273273
secretkey: <BASE64>
274274
```
275275

276+
##### AWS Controller-Level Workload Identity example
277+
278+
```yaml
279+
---
280+
apiVersion: source.toolkit.fluxcd.io/v1
281+
kind: Bucket
282+
metadata:
283+
name: aws-controller-level-workload-identity
284+
namespace: default
285+
spec:
286+
interval: 5m0s
287+
provider: aws
288+
bucketName: podinfo
289+
endpoint: s3.amazonaws.com
290+
region: us-east-1
291+
timeout: 30s
292+
```
293+
294+
##### AWS Object-Level Workload Identity example
295+
296+
**Note:** To use Object-Level Workload Identity (`.spec.serviceAccountName` with
297+
cloud providers), the controller feature gate `ObjectLevelWorkloadIdentity` must
298+
be enabled.
299+
300+
```yaml
301+
---
302+
apiVersion: source.toolkit.fluxcd.io/v1
303+
kind: Bucket
304+
metadata:
305+
name: aws-object-level-workload-identity
306+
namespace: default
307+
spec:
308+
interval: 5m0s
309+
provider: aws
310+
bucketName: podinfo
311+
endpoint: s3.amazonaws.com
312+
region: us-east-1
313+
serviceAccountName: aws-workload-identity-sa
314+
timeout: 30s
315+
---
316+
apiVersion: v1
317+
kind: ServiceAccount
318+
metadata:
319+
name: aws-workload-identity-sa
320+
namespace: default
321+
annotations:
322+
eks.amazonaws.com/role-arn: arn:aws:iam::123456789012:role/flux-bucket-role
323+
```
324+
276325
#### Azure
277326

278327
When a Bucket's `.spec.provider` is set to `azure`, the source-controller will

internal/bucket/minio/minio.go

Lines changed: 52 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,9 @@ import (
3030
"github.com/minio/minio-go/v7/pkg/s3utils"
3131
corev1 "k8s.io/api/core/v1"
3232

33+
"github.com/fluxcd/pkg/auth"
34+
awsauth "github.com/fluxcd/pkg/auth/aws"
35+
3336
sourcev1 "github.com/fluxcd/source-controller/api/v1"
3437
)
3538

@@ -46,6 +49,7 @@ type options struct {
4649
tlsConfig *tls.Config
4750
stsTLSConfig *tls.Config
4851
proxyURL *url.URL
52+
authOpts []auth.Option
4953
}
5054

5155
// Option is a function that configures the Minio client.
@@ -86,8 +90,15 @@ func WithSTSTLSConfig(tlsConfig *tls.Config) Option {
8690
}
8791
}
8892

93+
// WithAuth sets the auth options for workload identity authentication.
94+
func WithAuth(authOpts ...auth.Option) Option {
95+
return func(o *options) {
96+
o.authOpts = authOpts
97+
}
98+
}
99+
89100
// NewClient creates a new Minio storage client.
90-
func NewClient(bucket *sourcev1.Bucket, opts ...Option) (*MinioClient, error) {
101+
func NewClient(ctx context.Context, bucket *sourcev1.Bucket, opts ...Option) (*MinioClient, error) {
91102
var o options
92103
for _, opt := range opts {
93104
opt(&o)
@@ -105,7 +116,11 @@ func NewClient(bucket *sourcev1.Bucket, opts ...Option) (*MinioClient, error) {
105116
case o.secret != nil:
106117
minioOpts.Creds = newCredsFromSecret(o.secret)
107118
case bucketProvider == sourcev1.BucketProviderAmazon:
108-
minioOpts.Creds = newAWSCreds(bucket, o.proxyURL)
119+
creds, err := newAWSCreds(ctx, &o)
120+
if err != nil {
121+
return nil, err
122+
}
123+
minioOpts.Creds = creds
109124
case bucketProvider == sourcev1.BucketProviderGeneric:
110125
minioOpts.Creds = newGenericCreds(bucket, &o)
111126
}
@@ -159,23 +174,48 @@ func newCredsFromSecret(secret *corev1.Secret) *credentials.Credentials {
159174
}
160175

161176
// newAWSCreds creates a new Minio credentials object for `aws` bucket provider.
162-
func newAWSCreds(bucket *sourcev1.Bucket, proxyURL *url.URL) *credentials.Credentials {
163-
stsEndpoint := ""
164-
if sts := bucket.Spec.STS; sts != nil {
165-
stsEndpoint = sts.Endpoint
177+
//
178+
// This function is only called when Secret authentication is not available.
179+
//
180+
// Authentication flow:
181+
// 1. Try Workload Identity authentication (IRSA/EKS Pod Identity)
182+
// - Object-Level: When ServiceAccount is specified (fails with error if unsuccessful)
183+
// - Controller-Level: When no ServiceAccount specified (fallback to IAM if fails)
184+
//
185+
// 2. IAM Fallback: Only for Controller-Level failures
186+
func newAWSCreds(ctx context.Context, o *options) (*credentials.Credentials, error) {
187+
var opts auth.Options
188+
opts.Apply(o.authOpts...)
189+
isObjectLevel := opts.ServiceAccount != nil
190+
191+
awsCredsProvider := awsauth.NewCredentialsProvider(ctx, o.authOpts...)
192+
awsCreds, err := awsCredsProvider.Retrieve(ctx)
193+
if err != nil {
194+
if isObjectLevel {
195+
return nil, fmt.Errorf("object-level workload identity authentication failed: %w", err)
196+
}
197+
return newAWSIAMCreds(opts), nil
166198
}
167199

168-
creds := credentials.NewIAM(stsEndpoint)
169-
if proxyURL != nil {
200+
return credentials.NewStaticV4(
201+
awsCreds.AccessKeyID,
202+
awsCreds.SecretAccessKey,
203+
awsCreds.SessionToken,
204+
), nil
205+
}
206+
207+
// newAWSIAMCreds creates IAM credentials with optional STS endpoint and proxy support.
208+
func newAWSIAMCreds(opts auth.Options) *credentials.Credentials {
209+
if opts.ProxyURL != nil {
170210
transport := http.DefaultTransport.(*http.Transport).Clone()
171-
transport.Proxy = http.ProxyURL(proxyURL)
211+
transport.Proxy = http.ProxyURL(opts.ProxyURL)
172212
client := &http.Client{Transport: transport}
173-
creds = credentials.New(&credentials.IAM{
213+
return credentials.New(&credentials.IAM{
174214
Client: client,
175-
Endpoint: stsEndpoint,
215+
Endpoint: opts.STSEndpoint,
176216
})
177217
}
178-
return creds
218+
return credentials.NewIAM(opts.STSEndpoint)
179219
}
180220

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

0 commit comments

Comments
 (0)