Skip to content

Commit f30c076

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

File tree

6 files changed

+166
-46
lines changed

6 files changed

+166
-46
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>

internal/bucket/minio/minio.go

Lines changed: 52 additions & 18 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.authOpts)
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,42 @@ 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
166-
}
167-
168-
creds := credentials.NewIAM(stsEndpoint)
169-
if proxyURL != nil {
170-
transport := http.DefaultTransport.(*http.Transport).Clone()
171-
transport.Proxy = http.ProxyURL(proxyURL)
172-
client := &http.Client{Transport: transport}
173-
creds = credentials.New(&credentials.IAM{
174-
Client: client,
175-
Endpoint: stsEndpoint,
176-
})
177+
//
178+
// This function is only called when Secret authentication is not available.
179+
//
180+
// Authentication flow:
181+
// 1. Try Workload Identity authentication first
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+
// - Uses MinIO's credentials.NewIAM("") for EC2 instance profiles,
187+
// environment variables, or other AWS credential chain methods
188+
func newAWSCreds(ctx context.Context, authOpts []auth.Option) (*credentials.Credentials, error) {
189+
// Check if this is Object-Level (ServiceAccount specified) or Controller-Level
190+
var opts auth.Options
191+
opts.Apply(authOpts...)
192+
isObjectLevel := opts.ServiceAccount != nil
193+
194+
// Try Workload Identity authentication
195+
awsCredsProvider := awsauth.NewCredentialsProvider(ctx, authOpts...)
196+
197+
awsCreds, err := awsCredsProvider.Retrieve(ctx)
198+
if err != nil {
199+
// Object-Level: Don't fallback, fail with wrapped error
200+
if isObjectLevel {
201+
return nil, fmt.Errorf("object-level workload identity authentication failed: %w", err)
202+
}
203+
// Controller-Level: Fallback to traditional IAM authentication
204+
return credentials.NewIAM(""), nil
177205
}
178-
return creds
206+
207+
// Use retrieved Workload Identity credentials
208+
return credentials.NewStaticV4(
209+
awsCreds.AccessKeyID,
210+
awsCreds.SecretAccessKey,
211+
awsCreds.SessionToken,
212+
), nil
179213
}
180214

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

0 commit comments

Comments
 (0)