Skip to content

[RFC-0010] Add multi-tenant workload identity support for AWS Bucket #1868

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 5 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 5 additions & 3 deletions api/v1/bucket_types.go
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,8 @@ const (
// BucketProviderGeneric for any S3 API compatible storage Bucket.
BucketProviderGeneric string = "generic"
// BucketProviderAmazon for an AWS S3 object storage Bucket.
// Provides support for retrieving credentials from the AWS EC2 service.
// Provides support for retrieving credentials from the AWS EC2 service
// and workload identity authentication.
BucketProviderAmazon string = "aws"
// BucketProviderGoogle for a Google Cloud Storage Bucket.
// Provides support for authentication using a workload identity.
Expand All @@ -51,7 +52,7 @@ const (
// +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"
// +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"
// +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"
// +kubebuilder:validation:XValidation:rule="self.provider == 'gcp' || !has(self.serviceAccountName)", message="ServiceAccountName is only supported for the 'gcp' Bucket provider"
// +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"
// +kubebuilder:validation:XValidation:rule="!has(self.secretRef) || !has(self.serviceAccountName)", message="cannot set both .spec.secretRef and .spec.serviceAccountName"
type BucketSpec struct {
// Provider of the object storage bucket.
Expand Down Expand Up @@ -96,7 +97,8 @@ type BucketSpec struct {
SecretRef *meta.LocalObjectReference `json:"secretRef,omitempty"`

// ServiceAccountName is the name of the Kubernetes ServiceAccount used to authenticate
// the bucket. For more information about workload identity:
// the bucket. This field is only supported for the 'gcp' and 'aws' providers.
// For more information about workload identity:
// https://fluxcd.io/flux/components/source/buckets/#workload-identity
// +optional
ServiceAccountName string `json:"serviceAccountName,omitempty"`
Expand Down
8 changes: 5 additions & 3 deletions config/crd/bases/source.toolkit.fluxcd.io_buckets.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -145,7 +145,8 @@ spec:
serviceAccountName:
description: |-
ServiceAccountName is the name of the Kubernetes ServiceAccount used to authenticate
the bucket. For more information about workload identity:
the bucket. This field is only supported for the 'gcp' and 'aws' providers.
For more information about workload identity:
https://fluxcd.io/flux/components/source/buckets/#workload-identity
type: string
sts:
Expand Down Expand Up @@ -238,8 +239,9 @@ spec:
rule: '!has(self.sts) || self.sts.provider != ''aws'' || !has(self.sts.secretRef)'
- message: spec.sts.certSecretRef is not required for the 'aws' STS provider
rule: '!has(self.sts) || self.sts.provider != ''aws'' || !has(self.sts.certSecretRef)'
- message: ServiceAccountName is only supported for the 'gcp' Bucket provider
rule: self.provider == 'gcp' || !has(self.serviceAccountName)
- message: ServiceAccountName is only supported for the 'gcp' and 'aws'
Bucket providers
rule: self.provider == 'gcp' || self.provider == 'aws' || !has(self.serviceAccountName)
- message: cannot set both .spec.secretRef and .spec.serviceAccountName
rule: '!has(self.secretRef) || !has(self.serviceAccountName)'
status:
Expand Down
6 changes: 4 additions & 2 deletions docs/api/v1/source.md
Original file line number Diff line number Diff line change
Expand Up @@ -190,7 +190,8 @@ string
<td>
<em>(Optional)</em>
<p>ServiceAccountName is the name of the Kubernetes ServiceAccount used to authenticate
the bucket. For more information about workload identity:
the bucket. This field is only supported for the &lsquo;gcp&rsquo; and &lsquo;aws&rsquo; providers.
For more information about workload identity:
<a href="https://fluxcd.io/flux/components/source/buckets/#workload-identity">https://fluxcd.io/flux/components/source/buckets/#workload-identity</a></p>
</td>
</tr>
Expand Down Expand Up @@ -1646,7 +1647,8 @@ string
<td>
<em>(Optional)</em>
<p>ServiceAccountName is the name of the Kubernetes ServiceAccount used to authenticate
the bucket. For more information about workload identity:
the bucket. This field is only supported for the &lsquo;gcp&rsquo; and &lsquo;aws&rsquo; providers.
For more information about workload identity:
<a href="https://fluxcd.io/flux/components/source/buckets/#workload-identity">https://fluxcd.io/flux/components/source/buckets/#workload-identity</a></p>
</td>
</tr>
Expand Down
149 changes: 45 additions & 104 deletions docs/spec/v1/buckets.md
Original file line number Diff line number Diff line change
Expand Up @@ -186,18 +186,13 @@ data:

#### AWS

When a Bucket's `.spec.provider` field is set to `aws`, the source-controller
will attempt to communicate with the specified [Endpoint](#endpoint) using the
[Minio Client SDK](https://github.com/minio/minio-go).
For detailed setup instructions, see: https://fluxcd.io/flux/integrations/aws/#for-amazon-simple-storage-service

Without a [Secret reference](#secret-reference), authorization using
credentials retrieved from the AWS EC2 service is attempted by default. When
a reference is specified, it expects a Secret with `.data.accesskey` and
`.data.secretkey` values, used to authenticate with static credentials.

The Provider allows for specifying the
[Amazon AWS Region](https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/using-regions-availability-zones.html#concepts-available-regions)
using the [`.spec.region` field](#region).
When using a [Secret reference](#secret-reference), it expects a Secret with
`.data.accesskey` and `.data.secretkey` values. Without a Secret reference,
authorization using credentials retrieved from the AWS EC2 service is attempted
by default. For Workload Identity authentication, use the
[`.spec.serviceAccountName` field](#service-account-reference).

##### AWS EC2 example

Expand Down Expand Up @@ -273,23 +268,49 @@ data:
secretkey: <BASE64>
```

#### Azure
##### AWS Object-Level Workload Identity example

When a Bucket's `.spec.provider` is set to `azure`, the source-controller will
attempt to communicate with the specified [Endpoint](#endpoint) using the
[Azure Blob Storage SDK for Go](https://github.com/Azure/azure-sdk-for-go/tree/main/sdk/storage/azblob).
**Note:** To use Object-Level Workload Identity (`.spec.serviceAccountName` with
cloud providers), the controller feature gate `ObjectLevelWorkloadIdentity` must
be enabled.

```yaml
---
apiVersion: source.toolkit.fluxcd.io/v1
kind: Bucket
metadata:
name: aws-object-level-workload-identity
namespace: default
spec:
interval: 5m0s
provider: aws
bucketName: podinfo
endpoint: s3.amazonaws.com
region: us-east-1
serviceAccountName: aws-workload-identity-sa
timeout: 30s
---
apiVersion: v1
kind: ServiceAccount
metadata:
name: aws-workload-identity-sa
namespace: default
annotations:
eks.amazonaws.com/role-arn: arn:aws:iam::123456789012:role/flux-bucket-role
```

#### Azure

Without a [Secret reference](#secret-reference), authentication using a chain
with:
For detailed setup instructions, see: https://fluxcd.io/flux/integrations/azure/#for-azure-blob-storage

- [Environment credentials](https://pkg.go.dev/github.com/Azure/azure-sdk-for-go/sdk/azidentity#EnvironmentCredential)
- [Workload Identity](https://pkg.go.dev/github.com/Azure/azure-sdk-for-go/sdk/azidentity#WorkloadIdentityCredential)
- [Managed Identity](https://pkg.go.dev/github.com/Azure/azure-sdk-for-go/sdk/azidentity#ManagedIdentityCredential)
with the `AZURE_CLIENT_ID`
- Managed Identity with a system-assigned identity
When using a [Secret reference](#secret-reference), it expects a Secret with
authentication fields (see examples below). Without a Secret reference,
authorization using a credential chain with Environment, Workload Identity,
and Managed Identity is attempted by default. For Workload Identity
authentication, configure the source-controller as described in the integration guide.

is attempted by default. If no chain can be established, the bucket
is assumed to be publicly reachable.
The source-controller uses the [Azure Blob Storage SDK for Go](https://github.com/Azure/azure-sdk-for-go/tree/main/sdk/storage/azblob)
to communicate with the specified [Endpoint](#endpoint).

When a reference is specified, it expects a Secret with one of the following
sets of `.data` fields:
Expand Down Expand Up @@ -440,86 +461,6 @@ data:
accountKey: <BASE64>
```

##### Workload Identity

If you have [Workload Identity](https://azure.github.io/azure-workload-identity/docs/installation/managed-clusters.html)
set up on your cluster, you need to create an Azure Identity and give it
access to Azure Blob Storage.

```shell
export IDENTITY_NAME="blob-access"

az role assignment create --role "Storage Blob Data Reader" \
--assignee-object-id "$(az identity show -n $IDENTITY_NAME -o tsv --query principalId -g $RESOURCE_GROUP)" \
--scope "/subscriptions/<SUBSCRIPTION-ID>/resourceGroups/<RESOURCE_GROUP>/providers/Microsoft.Storage/storageAccounts/<account-name>/blobServices/default/containers/<container-name>"
```

Establish a federated identity between the Identity and the source-controller
ServiceAccount.

```shell
export SERVICE_ACCOUNT_ISSUER="$(az aks show --resource-group <RESOURCE_GROUP> --name <CLUSTER-NAME> --query "oidcIssuerProfile.issuerUrl" -otsv)"

az identity federated-credential create \
--name "kubernetes-federated-credential" \
--identity-name "${IDENTITY_NAME}" \
--resource-group "${RESOURCE_GROUP}" \
--issuer "${SERVICE_ACCOUNT_ISSUER}" \
--subject "system:serviceaccount:flux-system:source-controller"
```

Add a patch to label and annotate the source-controller Deployment and ServiceAccount
correctly so that it can match an identity binding:

```yaml
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
resources:
- gotk-components.yaml
- gotk-sync.yaml
patches:
- patch: |-
apiVersion: v1
kind: ServiceAccount
metadata:
name: source-controller
namespace: flux-system
annotations:
azure.workload.identity/client-id: <AZURE_CLIENT_ID>
labels:
azure.workload.identity/use: "true"
- patch: |-
apiVersion: apps/v1
kind: Deployment
metadata:
name: source-controller
namespace: flux-system
labels:
azure.workload.identity/use: "true"
spec:
template:
metadata:
labels:
azure.workload.identity/use: "true"
```

If you have set up Workload Identity correctly and labeled the source-controller
Deployment and ServiceAccount, then you don't need to reference a Secret. For more information,
please see [documentation](https://azure.github.io/azure-workload-identity/docs/quick-start.html).

```yaml
apiVersion: source.toolkit.fluxcd.io/v1
kind: Bucket
metadata:
name: azure-bucket
namespace: flux-system
spec:
interval: 5m0s
provider: azure
bucketName: testsas
endpoint: https://testfluxsas.blob.core.windows.net
```

##### Deprecated: Managed Identity with AAD Pod Identity

If you are using [aad pod identity](https://azure.github.io/aad-pod-identity/docs),
Expand Down
58 changes: 49 additions & 9 deletions internal/bucket/minio/minio.go
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,9 @@ import (
"github.com/minio/minio-go/v7/pkg/s3utils"
corev1 "k8s.io/api/core/v1"

"github.com/fluxcd/pkg/auth"
awsauth "github.com/fluxcd/pkg/auth/aws"

sourcev1 "github.com/fluxcd/source-controller/api/v1"
)

Expand All @@ -46,6 +49,7 @@ type options struct {
tlsConfig *tls.Config
stsTLSConfig *tls.Config
proxyURL *url.URL
authOpts []auth.Option
}

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

// WithAuth sets the auth options for workload identity authentication.
func WithAuth(authOpts ...auth.Option) Option {
return func(o *options) {
o.authOpts = authOpts
}
}

// NewClient creates a new Minio storage client.
func NewClient(bucket *sourcev1.Bucket, opts ...Option) (*MinioClient, error) {
func NewClient(ctx context.Context, bucket *sourcev1.Bucket, opts ...Option) (*MinioClient, error) {
var o options
for _, opt := range opts {
opt(&o)
Expand All @@ -105,7 +116,11 @@ func NewClient(bucket *sourcev1.Bucket, opts ...Option) (*MinioClient, error) {
case o.secret != nil:
minioOpts.Creds = newCredsFromSecret(o.secret)
case bucketProvider == sourcev1.BucketProviderAmazon:
minioOpts.Creds = newAWSCreds(bucket, o.proxyURL)
creds, err := newAWSCreds(ctx, bucket, &o)
if err != nil {
return nil, err
}
minioOpts.Creds = creds
case bucketProvider == sourcev1.BucketProviderGeneric:
minioOpts.Creds = newGenericCreds(bucket, &o)
}
Expand Down Expand Up @@ -159,23 +174,48 @@ func newCredsFromSecret(secret *corev1.Secret) *credentials.Credentials {
}

// newAWSCreds creates a new Minio credentials object for `aws` bucket provider.
func newAWSCreds(bucket *sourcev1.Bucket, proxyURL *url.URL) *credentials.Credentials {
//
// This function is only called when Secret authentication is not available.
//
// Authentication priority order:
// 1. Object Level Workload Identity (if serviceAccountName is specified)
// 2. AWS credential chain (EC2 instance profiles, environment variables, etc.)
func newAWSCreds(ctx context.Context, bucket *sourcev1.Bucket, o *options) (*credentials.Credentials, error) {
var opts auth.Options
opts.Apply(o.authOpts...)

// 1. Try Object Level Workload Identity if serviceAccountName is specified
if opts.ServiceAccount != nil {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It is our intention to make auth handle also controller-level, it was designed to support both object-level and controller-level authentication across all Flux controllers. As part of implementing RFC-0010 we are actively making this migration, which started in Flux 2.6 for kustomize-controller and for the OCI APIs.

awsCredsProvider := awsauth.NewCredentialsProvider(ctx, o.authOpts...)
awsCreds, err := awsCredsProvider.Retrieve(ctx)
if err != nil {
return nil, fmt.Errorf("Object Level Workload Identity authentication failed: %w", err)
}

return credentials.NewStaticV4(
awsCreds.AccessKeyID,
awsCreds.SecretAccessKey,
awsCreds.SessionToken,
), nil
}

// 2. AWS credential chain
stsEndpoint := ""
if sts := bucket.Spec.STS; sts != nil {
stsEndpoint = sts.Endpoint
}

creds := credentials.NewIAM(stsEndpoint)
if proxyURL != nil {
if o.proxyURL != nil {
transport := http.DefaultTransport.(*http.Transport).Clone()
transport.Proxy = http.ProxyURL(proxyURL)
transport.Proxy = http.ProxyURL(o.proxyURL)
client := &http.Client{Transport: transport}
creds = credentials.New(&credentials.IAM{
return credentials.New(&credentials.IAM{
Client: client,
Endpoint: stsEndpoint,
})
}), nil
} else {
return credentials.NewIAM(stsEndpoint), nil
}
return creds
}

// newGenericCreds creates a new Minio credentials object for the `generic` bucket provider.
Expand Down
Loading