Skip to content

Commit d69d743

Browse files
authored
Merge pull request #1862 from cappyzawa/feat/bucket-workload-identity-gcp
[RFC-0010] Add multi-tenant workload identity support for GCP Bucket
2 parents 1469073 + 3733163 commit d69d743

File tree

10 files changed

+326
-59
lines changed

10 files changed

+326
-59
lines changed

api/v1/bucket_types.go

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,8 @@ const (
5151
// +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"
5252
// +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"
5353
// +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="!has(self.secretRef) || !has(self.serviceAccountName)", message="cannot set both .spec.secretRef and .spec.serviceAccountName"
5456
type BucketSpec struct {
5557
// Provider of the object storage bucket.
5658
// Defaults to 'generic', which expects an S3 (API) compatible object
@@ -93,6 +95,12 @@ type BucketSpec struct {
9395
// +optional
9496
SecretRef *meta.LocalObjectReference `json:"secretRef,omitempty"`
9597

98+
// ServiceAccountName is the name of the Kubernetes ServiceAccount used to authenticate
99+
// the bucket. For more information about workload identity:
100+
// https://fluxcd.io/flux/components/source/buckets/#workload-identity
101+
// +optional
102+
ServiceAccountName string `json:"serviceAccountName,omitempty"`
103+
96104
// CertSecretRef can be given the name of a Secret containing
97105
// either or both of
98106
//

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

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -142,6 +142,12 @@ spec:
142142
required:
143143
- name
144144
type: object
145+
serviceAccountName:
146+
description: |-
147+
ServiceAccountName is the name of the Kubernetes ServiceAccount used to authenticate
148+
the bucket. For more information about workload identity:
149+
https://fluxcd.io/flux/components/source/buckets/#workload-identity
150+
type: string
145151
sts:
146152
description: |-
147153
STS specifies the required configuration to use a Security Token
@@ -232,6 +238,10 @@ spec:
232238
rule: '!has(self.sts) || self.sts.provider != ''aws'' || !has(self.sts.secretRef)'
233239
- message: spec.sts.certSecretRef is not required for the 'aws' STS provider
234240
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)
243+
- message: cannot set both .spec.secretRef and .spec.serviceAccountName
244+
rule: '!has(self.secretRef) || !has(self.serviceAccountName)'
235245
status:
236246
default:
237247
observedGeneration: -1

config/rbac/role.yaml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ rules:
1515
- ""
1616
resources:
1717
- secrets
18+
- serviceaccounts
1819
verbs:
1920
- get
2021
- list

docs/api/v1/source.md

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -182,6 +182,20 @@ for the Bucket.</p>
182182
</tr>
183183
<tr>
184184
<td>
185+
<code>serviceAccountName</code><br>
186+
<em>
187+
string
188+
</em>
189+
</td>
190+
<td>
191+
<em>(Optional)</em>
192+
<p>ServiceAccountName is the name of the Kubernetes ServiceAccount used to authenticate
193+
the bucket. For more information about workload identity:
194+
<a href="https://fluxcd.io/flux/components/source/buckets/#workload-identity">https://fluxcd.io/flux/components/source/buckets/#workload-identity</a></p>
195+
</td>
196+
</tr>
197+
<tr>
198+
<td>
185199
<code>certSecretRef</code><br>
186200
<em>
187201
<a href="https://pkg.go.dev/github.com/fluxcd/pkg/apis/meta#LocalObjectReference">
@@ -1624,6 +1638,20 @@ for the Bucket.</p>
16241638
</tr>
16251639
<tr>
16261640
<td>
1641+
<code>serviceAccountName</code><br>
1642+
<em>
1643+
string
1644+
</em>
1645+
</td>
1646+
<td>
1647+
<em>(Optional)</em>
1648+
<p>ServiceAccountName is the name of the Kubernetes ServiceAccount used to authenticate
1649+
the bucket. For more information about workload identity:
1650+
<a href="https://fluxcd.io/flux/components/source/buckets/#workload-identity">https://fluxcd.io/flux/components/source/buckets/#workload-identity</a></p>
1651+
</td>
1652+
</tr>
1653+
<tr>
1654+
<td>
16271655
<code>certSecretRef</code><br>
16281656
<em>
16291657
<a href="https://pkg.go.dev/github.com/fluxcd/pkg/apis/meta#LocalObjectReference">

docs/spec/v1/buckets.md

Lines changed: 55 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -647,37 +647,55 @@ Refer to the [Azure documentation](https://learn.microsoft.com/en-us/rest/api/st
647647

648648
#### GCP
649649

650-
When a Bucket's `.spec.provider` is set to `gcp`, the source-controller will
651-
attempt to communicate with the specified [Endpoint](#endpoint) using the
652-
[Google Client SDK](https://github.com/googleapis/google-api-go-client).
650+
For detailed setup instructions, see: https://fluxcd.io/flux/integrations/gcp/#for-google-cloud-storage
653651

654-
Without a [Secret reference](#secret-reference), authorization using a
655-
workload identity is attempted by default. The workload identity is obtained
656-
using the `GOOGLE_APPLICATION_CREDENTIALS` environment variable, falling back
657-
to the Google Application Credential file in the config directory.
658-
When a reference is specified, it expects a Secret with a `.data.serviceaccount`
659-
value with a GCP service account JSON file.
652+
##### GCP Controller-Level Workload Identity example
660653

661-
The Provider allows for specifying the
662-
[Bucket location](https://cloud.google.com/storage/docs/locations) using the
663-
[`.spec.region` field](#region).
654+
```yaml
655+
---
656+
apiVersion: source.toolkit.fluxcd.io/v1
657+
kind: Bucket
658+
metadata:
659+
name: gcp-controller-level-workload-identity
660+
namespace: default
661+
spec:
662+
interval: 5m0s
663+
provider: gcp
664+
bucketName: podinfo
665+
endpoint: storage.googleapis.com
666+
region: us-east-1
667+
timeout: 30s
668+
```
669+
670+
##### GCP Object-Level Workload Identity example
664671

665-
##### GCP example
672+
**Note:** To use Object-Level Workload Identity (`.spec.serviceAccountName` with
673+
cloud providers), the controller feature gate `ObjectLevelWorkloadIdentity` must
674+
be enabled.
666675

667676
```yaml
668677
---
669678
apiVersion: source.toolkit.fluxcd.io/v1
670679
kind: Bucket
671680
metadata:
672-
name: gcp-workload-identity
681+
name: gcp-object-level-workload-identity
673682
namespace: default
674683
spec:
675684
interval: 5m0s
676685
provider: gcp
677686
bucketName: podinfo
678687
endpoint: storage.googleapis.com
679688
region: us-east-1
689+
serviceAccountName: gcp-workload-identity-sa
680690
timeout: 30s
691+
---
692+
apiVersion: v1
693+
kind: ServiceAccount
694+
metadata:
695+
name: gcp-workload-identity-sa
696+
namespace: default
697+
annotations:
698+
iam.gke.io/gcp-service-account: <identity-name>
681699
```
682700

683701
##### GCP static auth example
@@ -959,6 +977,29 @@ credentials for the object storage. For some `.spec.provider` implementations
959977
the presence of the field is required, see [Provider](#provider) for more
960978
details and examples.
961979

980+
### Service Account reference
981+
982+
`.spec.serviceAccountName` is an optional field to specify a Service Account
983+
in the same namespace as Bucket with purpose depending on the value of
984+
the `.spec.provider` field:
985+
986+
- When `.spec.provider` is set to `generic`, the controller will fetch the image
987+
pull secrets attached to the Service Account and use them for authentication.
988+
- When `.spec.provider` is set to `aws`, `azure`, or `gcp`, the Service Account
989+
will be used for Workload Identity authentication. In this case, the controller
990+
feature gate `ObjectLevelWorkloadIdentity` must be enabled, otherwise the
991+
controller will error out.
992+
993+
**Note:** that for a publicly accessible object storage, you don't need to
994+
provide a `secretRef` nor `serviceAccountName`.
995+
996+
**Important:** `.spec.secretRef` and `.spec.serviceAccountName` are mutually
997+
exclusive and cannot be set at the same time. This constraint is enforced
998+
at the CRD level.
999+
1000+
For a complete guide on how to set up authentication for cloud providers,
1001+
see the integration [docs](/flux/integrations/).
1002+
9621003
### Prefix
9631004

9641005
`.spec.prefix` is an optional field to enable server-side filtering

internal/controller/bucket_controller.go

Lines changed: 56 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,8 @@ import (
4444

4545
eventv1 "github.com/fluxcd/pkg/apis/event/v1beta1"
4646
"github.com/fluxcd/pkg/apis/meta"
47+
"github.com/fluxcd/pkg/auth"
48+
"github.com/fluxcd/pkg/cache"
4749
"github.com/fluxcd/pkg/runtime/conditions"
4850
helper "github.com/fluxcd/pkg/runtime/controller"
4951
"github.com/fluxcd/pkg/runtime/jitter"
@@ -116,6 +118,8 @@ var bucketFailConditions = []string{
116118
// +kubebuilder:rbac:groups=source.toolkit.fluxcd.io,resources=buckets/status,verbs=get;update;patch
117119
// +kubebuilder:rbac:groups=source.toolkit.fluxcd.io,resources=buckets/finalizers,verbs=get;create;update;patch;delete
118120
// +kubebuilder:rbac:groups="",resources=secrets,verbs=get;list;watch
121+
// +kubebuilder:rbac:groups="",resources=serviceaccounts,verbs=get;list;watch
122+
// +kubebuilder:rbac:groups="",resources=serviceaccounts/token,verbs=create
119123

120124
// BucketReconciler reconciles a v1.Bucket object.
121125
type BucketReconciler struct {
@@ -125,6 +129,7 @@ type BucketReconciler struct {
125129

126130
Storage *Storage
127131
ControllerName string
132+
TokenCache *cache.TokenCache
128133

129134
patchOptions []patch.Option
130135
}
@@ -430,6 +435,18 @@ func (r *BucketReconciler) reconcileStorage(ctx context.Context, sp *patch.Seria
430435
// the provider. If this fails, it records v1.FetchFailedCondition=True on
431436
// the object and returns early.
432437
func (r *BucketReconciler) reconcileSource(ctx context.Context, sp *patch.SerialPatcher, obj *sourcev1.Bucket, index *index.Digester, dir string) (sreconcile.Result, error) {
438+
usesObjectLevelWorkloadIdentity := obj.Spec.Provider != "" && obj.Spec.Provider != sourcev1.BucketProviderGeneric && obj.Spec.ServiceAccountName != ""
439+
if usesObjectLevelWorkloadIdentity {
440+
if !auth.IsObjectLevelWorkloadIdentityEnabled() {
441+
const gate = auth.FeatureGateObjectLevelWorkloadIdentity
442+
const msgFmt = "to use spec.serviceAccountName for provider authentication please enable the %s feature gate in the controller"
443+
err := fmt.Errorf(msgFmt, gate)
444+
e := serror.NewStalling(err, meta.FeatureGateDisabledReason)
445+
conditions.MarkTrue(obj, sourcev1.FetchFailedCondition, e.Reason, "%s", e)
446+
return sreconcile.ResultEmpty, e
447+
}
448+
}
449+
433450
creds, err := r.setupCredentials(ctx, obj)
434451
if err != nil {
435452
e := serror.NewGeneric(err, sourcev1.AuthenticationFailedReason)
@@ -590,6 +607,10 @@ func (r *BucketReconciler) reconcileDelete(ctx context.Context, obj *sourcev1.Bu
590607
// Remove our finalizer from the list
591608
controllerutil.RemoveFinalizer(obj, sourcev1.SourceFinalizer)
592609

610+
// Cleanup caches.
611+
r.TokenCache.DeleteEventsForObject(sourcev1.BucketKind,
612+
obj.GetName(), obj.GetNamespace(), cache.OperationReconcile)
613+
593614
// Stop reconciliation as the object is being deleted
594615
return sreconcile.ResultEmpty, nil
595616
}
@@ -838,19 +859,47 @@ func (r *BucketReconciler) setupCredentials(ctx context.Context, obj *sourcev1.B
838859
// createBucketProvider creates a provider-specific bucket client using the given credentials and configuration.
839860
// It handles different bucket providers (AWS, GCP, Azure, generic) and returns the appropriate client.
840861
func (r *BucketReconciler) createBucketProvider(ctx context.Context, obj *sourcev1.Bucket, creds *bucketCredentials) (BucketProvider, error) {
862+
var authOpts []auth.Option
863+
864+
if obj.Spec.ServiceAccountName != "" {
865+
serviceAccount := client.ObjectKey{
866+
Name: obj.Spec.ServiceAccountName,
867+
Namespace: obj.GetNamespace(),
868+
}
869+
authOpts = append(authOpts, auth.WithServiceAccount(serviceAccount, r.Client))
870+
}
871+
872+
if r.TokenCache != nil {
873+
involvedObject := cache.InvolvedObject{
874+
Kind: sourcev1.BucketKind,
875+
Name: obj.GetName(),
876+
Namespace: obj.GetNamespace(),
877+
Operation: cache.OperationReconcile,
878+
}
879+
authOpts = append(authOpts, auth.WithCache(*r.TokenCache, involvedObject))
880+
}
881+
882+
if creds.proxyURL != nil {
883+
authOpts = append(authOpts, auth.WithProxyURL(*creds.proxyURL))
884+
}
885+
841886
switch obj.Spec.Provider {
842887
case sourcev1.BucketProviderGoogle:
843-
if err := gcp.ValidateSecret(creds.secret); err != nil {
844-
return nil, err
845-
}
846888
var opts []gcp.Option
847-
if creds.secret != nil {
848-
opts = append(opts, gcp.WithSecret(creds.secret))
849-
}
850889
if creds.proxyURL != nil {
851890
opts = append(opts, gcp.WithProxyURL(creds.proxyURL))
852891
}
853-
return gcp.NewClient(ctx, opts...)
892+
893+
if creds.secret != nil {
894+
if err := gcp.ValidateSecret(creds.secret); err != nil {
895+
return nil, err
896+
}
897+
opts = append(opts, gcp.WithSecret(creds.secret))
898+
} else {
899+
opts = append(opts, gcp.WithAuth(authOpts...))
900+
}
901+
902+
return gcp.NewClient(ctx, obj, opts...)
854903

855904
case sourcev1.BucketProviderAzure:
856905
if err := azure.ValidateSecret(creds.secret); err != nil {

0 commit comments

Comments
 (0)