Skip to content

Commit 3300705

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

File tree

7 files changed

+157
-116
lines changed

7 files changed

+157
-116
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: 46 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -23,13 +23,17 @@ import (
2323
"fmt"
2424
"net/http"
2525
"net/url"
26+
"os"
2627
"strings"
2728

2829
"github.com/minio/minio-go/v7"
2930
"github.com/minio/minio-go/v7/pkg/credentials"
3031
"github.com/minio/minio-go/v7/pkg/s3utils"
3132
corev1 "k8s.io/api/core/v1"
3233

34+
"github.com/fluxcd/pkg/auth"
35+
awsauth "github.com/fluxcd/pkg/auth/aws"
36+
3337
sourcev1 "github.com/fluxcd/source-controller/api/v1"
3438
)
3539

@@ -46,6 +50,7 @@ type options struct {
4650
tlsConfig *tls.Config
4751
stsTLSConfig *tls.Config
4852
proxyURL *url.URL
53+
authOpts []auth.Option
4954
}
5055

5156
// Option is a function that configures the Minio client.
@@ -86,8 +91,15 @@ func WithSTSTLSConfig(tlsConfig *tls.Config) Option {
8691
}
8792
}
8893

94+
// WithAuth sets the auth options for workload identity authentication.
95+
func WithAuth(authOpts ...auth.Option) Option {
96+
return func(o *options) {
97+
o.authOpts = authOpts
98+
}
99+
}
100+
89101
// NewClient creates a new Minio storage client.
90-
func NewClient(bucket *sourcev1.Bucket, opts ...Option) (*MinioClient, error) {
102+
func NewClient(ctx context.Context, bucket *sourcev1.Bucket, opts ...Option) (*MinioClient, error) {
91103
var o options
92104
for _, opt := range opts {
93105
opt(&o)
@@ -105,7 +117,11 @@ func NewClient(bucket *sourcev1.Bucket, opts ...Option) (*MinioClient, error) {
105117
case o.secret != nil:
106118
minioOpts.Creds = newCredsFromSecret(o.secret)
107119
case bucketProvider == sourcev1.BucketProviderAmazon:
108-
minioOpts.Creds = newAWSCreds(bucket, o.proxyURL)
120+
creds, err := newAWSCreds(ctx, &o)
121+
if err != nil {
122+
return nil, err
123+
}
124+
minioOpts.Creds = creds
109125
case bucketProvider == sourcev1.BucketProviderGeneric:
110126
minioOpts.Creds = newGenericCreds(bucket, &o)
111127
}
@@ -159,23 +175,35 @@ func newCredsFromSecret(secret *corev1.Secret) *credentials.Credentials {
159175
}
160176

161177
// 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-
})
178+
//
179+
// This function is only called when Secret authentication is not available.
180+
//
181+
// Uses AWS SDK's config.LoadDefaultConfig() which supports:
182+
// - Workload Identity (IRSA/EKS Pod Identity)
183+
// - EC2 instance profiles
184+
// - Environment variables
185+
// - Shared credentials files
186+
// - All other AWS SDK authentication methods
187+
func newAWSCreds(ctx context.Context, o *options) (*credentials.Credentials, error) {
188+
var opts auth.Options
189+
opts.Apply(o.authOpts...)
190+
191+
// Set AWS_REGION environment variable from bucket.Spec.Region for pkg/auth/aws
192+
if opts.STSRegion != "" {
193+
os.Setenv("AWS_REGION", opts.STSRegion)
194+
}
195+
196+
awsCredsProvider := awsauth.NewCredentialsProvider(ctx, o.authOpts...)
197+
awsCreds, err := awsCredsProvider.Retrieve(ctx)
198+
if err != nil {
199+
return nil, fmt.Errorf("AWS authentication failed: %w", err)
177200
}
178-
return creds
201+
202+
return credentials.NewStaticV4(
203+
awsCreds.AccessKeyID,
204+
awsCreds.SecretAccessKey,
205+
awsCreds.SessionToken,
206+
), nil
179207
}
180208

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

0 commit comments

Comments
 (0)