From bd5755625ccb7eaa79613461db238cace19e1f1f Mon Sep 17 00:00:00 2001 From: Matheus Pimenta Date: Wed, 21 Oct 2026 17:37:54 +0100 Subject: [PATCH] [RFC-0014] Introduce SPIFFE Broker API Signed-off-by: Matheus Pimenta --- AGENTS.md | 1 - docs/README.md | 18 +- docs/spec/v1/buckets.md | 21 +- docs/spec/v1/gitrepositories.md | 9 +- docs/spec/v1/ocirepositories.md | 16 +- go.mod | 15 +- go.sum | 30 +- hack/ci/oci-zot-workload-identity-e2e.sh | 613 +++++++++++++++++- internal/bucket/gcp/gcp_test.go | 2 +- internal/controller/bucket_controller.go | 19 +- internal/controller/bucket_controller_test.go | 53 +- .../controller/gitrepository_controller.go | 26 +- .../gitrepository_controller_test.go | 79 +-- .../controller/helmchart_controller_test.go | 2 +- .../controller/ocirepository_controller.go | 18 +- .../ocirepository_controller_test.go | 86 +-- internal/features/features.go | 7 +- main.go | 88 +-- 18 files changed, 782 insertions(+), 321 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index 211ac7010..8abbd4c22 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -135,7 +135,6 @@ Bump `fluxcd/pkg/*` modules as a set — version skew breaks `go.sum`. Run `make - `make verify` is the CI gate — a dirty diff means you forgot to run codegen or tidy. - The `replace` directives in `go.mod` (semver and go-digest) exist for correctness. Leave them alone. - `Storage` serializes writes per object via `fluxcd/pkg/lockedfile` and expects `--storage-path` to be a real local directory. Never write artifacts outside `Storage.BasePath` — the file server exposes that path verbatim at `--storage-addr`. -- Workload Identity is feature-gated by `auth.FeatureGateObjectLevelWorkloadIdentity` (from `fluxcd/pkg/auth`). Token caching is opt-in via `--token-cache-max-size`. - `CacheSecretsAndConfigMaps` (in `internal/features`) is off by default; `Secret`/`ConfigMap` lookups bypass the cache and hit the API server directly. Mind that before adding new secret reads to a hot path. - The controller watches a label-selected subset of its CRs — see `Cache.ByObject` in `mustSetupManager`. Adding a new kind requires updating both `main.go` and the scheme. - `pkg/azure`, `pkg/gcp`, `pkg/minio` are importable by external consumers. Treat their exported surface as semi-public API. diff --git a/docs/README.md b/docs/README.md index b86716adb..92e1b8954 100644 --- a/docs/README.md +++ b/docs/README.md @@ -36,6 +36,8 @@ | `--storage-adv-addr` | string | The advertised address of the static file server. | | `--storage-path` | string | The local storage path. | | `--token-cache-max-size` | int | The maximum amount of entries in the LRU cache used for tokens. (default 100, enabled) | +| `--spiffe-broker-endpoint` | string | The gRPC URL of the SPIFFE Broker endpoint. | +| `--spiffe-broker-id` | strings | SPIFFE ID of the SPIFFE Broker endpoint. Can be repeated. | | `--token-cache-max-duration` | duration | The maximum duration for which a token would be considered unexpired. This is capped at 1h. (default 1h) | | `--watch-all-namespaces` | boolean | Watch for custom resources in all namespaces, if set to false it will only watch the runtime namespace. (default true) | | `--watch-label-selector` | string | Watch for resources with matching labels e.g. 'sharding.fluxcd.io/key=shard1'. | @@ -71,10 +73,24 @@ understands it as the trust domain of the controller's own X509-SVID. Invalid values cause source-controller to fail at startup. +### SPIFFE Broker API + +When `--spiffe-broker-endpoint` and at least one `--spiffe-broker-id` are set, +source-controller acquires an X.509-SVID from the SPIFFE Workload API and uses it to +authenticate to the SPIFFE Broker API. The `SPIFFE_ENDPOINT_SOCKET` environment +variable must point to the Workload API socket. + +The Broker is used to acquire JWT-SVIDs for object-level workload identity when +a ServiceAccount sets `credential.auth.fluxcd.io/provider` to `spiffe`. The +Broker endpoint is a TCP gRPC target, e.g. +`dns:///spire-server.spire.svc.cluster.local:8443`. + +Each `--spiffe-broker-id` value is interpreted like the SPIFFE IDs used by the +mTLS flags. Invalid values cause source-controller to fail at startup. + ### Feature Gates | Name | Default Value | Description | |-------------------------------|---------------|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| | `CacheSecretsAndConfigMaps` | `false` | Configures the caching of Secrets and ConfigMaps by the controller-runtime client. When enabled, it will cache both object types, resulting in increased memory usage and cluster-wide RBAC permissions (list and watch). | -| `ObjectLevelWorkloadIdentity` | `false` | Enables the use of object-level workload identity for the controller. | | `OptimizedGitClones` | `true` | Optimises Git resource usage by only cloning repositories when the HEAD commit changed since last reconciliation. | diff --git a/docs/spec/v1/buckets.md b/docs/spec/v1/buckets.md index eb02f5114..6735f66fa 100644 --- a/docs/spec/v1/buckets.md +++ b/docs/spec/v1/buckets.md @@ -295,10 +295,6 @@ spec: ##### AWS Object-Level Workload Identity example -**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 @@ -573,10 +569,6 @@ spec: ##### Azure Object-Level Workload Identity example -**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 @@ -676,10 +668,6 @@ spec: ##### GCP Object-Level Workload Identity example -**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 @@ -1000,9 +988,12 @@ the `.spec.provider` field: - When `.spec.provider` is set to `generic`, the controller will fetch the image pull secrets attached to the Service Account and use them for authentication. - When `.spec.provider` is set to `aws`, `azure`, or `gcp`, the Service Account - will be used for Workload Identity authentication. In this case, the controller - feature gate `ObjectLevelWorkloadIdentity` must be enabled, otherwise the - controller will error out. + will be used for Workload Identity authentication. + +For a complete guide to the ServiceAccount annotations used for object-level +workload identity, including the `spiffe` credential provider, see the +[ServiceAccount annotations](/flux/integrations/serviceaccount-annotations) +docs. **Note:** that for a publicly accessible object storage, you don't need to provide a `secretRef` nor `serviceAccountName`. diff --git a/docs/spec/v1/gitrepositories.md b/docs/spec/v1/gitrepositories.md index a700c460a..f873fa7ec 100644 --- a/docs/spec/v1/gitrepositories.md +++ b/docs/spec/v1/gitrepositories.md @@ -465,11 +465,14 @@ in the same namespace as GitRepository with purpose depending on the value of the `.spec.provider` field: - When `.spec.provider` is set to `azure`, the Service Account - will be used for Workload Identity authentication. In this case, the controller - feature gate `ObjectLevelWorkloadIdentity` must be enabled, otherwise the - controller will error out. For Azure DevOps specific setup, see the + will be used for Workload Identity authentication. For Azure DevOps specific setup, see the [Azure DevOps integration guide](https://fluxcd.io/flux/integrations/azure/#for-azure-devops). +For a complete guide to the ServiceAccount annotations used for object-level +workload identity, including the `spiffe` credential provider, see the +[ServiceAccount annotations](/flux/integrations/serviceaccount-annotations) +docs. + **Note:** that for a publicly accessible git repository, you don't need to provide a `secretRef` nor `serviceAccountName`. diff --git a/docs/spec/v1/ocirepositories.md b/docs/spec/v1/ocirepositories.md index 9d2b7df72..169606b61 100644 --- a/docs/spec/v1/ocirepositories.md +++ b/docs/spec/v1/ocirepositories.md @@ -281,9 +281,12 @@ the `.spec.provider` field: use the Service Account token for object-level workload identity when the Service Account opts in with the credential annotations described below. - When `.spec.provider` is set to `aws`, `azure`, or `gcp`, the Service Account - will be used for Workload Identity authentication. In this case, the controller - feature gate `ObjectLevelWorkloadIdentity` must be enabled, otherwise the - controller will error out. + will be used for Workload Identity authentication. + +For a complete guide to the ServiceAccount annotations used for object-level +workload identity, including the `spiffe` credential provider, see the +[ServiceAccount annotations](/flux/integrations/serviceaccount-annotations) +docs. **Note:** that for a publicly accessible image repository, you don't need to provide a `secretRef` nor `serviceAccountName`. @@ -314,13 +317,6 @@ and continues to use its `imagePullSecrets`, or anonymous access if it has none. Setting only one of the two required annotations is a configuration error. The credential annotations cannot be combined with `imagePullSecrets`. -Object-level workload identity for the `generic` provider requires the -`ObjectLevelWorkloadIdentity` feature gate to be enabled: - -```bash ---feature-gates=ObjectLevelWorkloadIdentity=true -``` - Example: ```yaml diff --git a/go.mod b/go.mod index 6bb8f77d6..5ca93ef93 100644 --- a/go.mod +++ b/go.mod @@ -23,21 +23,21 @@ require ( github.com/docker/go-units v0.5.0 github.com/elazarl/goproxy v1.8.4 github.com/fluxcd/cli-utils v1.4.0 - github.com/fluxcd/pkg/apis/crypto v0.4.1-0.20261022125904-05e84321fa52 + github.com/fluxcd/pkg/apis/crypto v0.5.0 github.com/fluxcd/pkg/apis/event v0.32.0 - github.com/fluxcd/pkg/apis/meta v1.36.1-0.20261022125904-05e84321fa52 - github.com/fluxcd/pkg/artifact v0.25.1-0.20261022125904-05e84321fa52 - github.com/fluxcd/pkg/auth v0.63.1-0.20261022125904-05e84321fa52 - github.com/fluxcd/pkg/cache v0.17.1-0.20261022125904-05e84321fa52 + github.com/fluxcd/pkg/apis/meta v1.37.0 + github.com/fluxcd/pkg/artifact v0.26.0 + github.com/fluxcd/pkg/auth v0.65.0 + github.com/fluxcd/pkg/cache v0.18.0 github.com/fluxcd/pkg/git v0.55.0 github.com/fluxcd/pkg/gittestserver v0.32.0 github.com/fluxcd/pkg/helmtestserver v0.44.0 github.com/fluxcd/pkg/http/transport v0.9.0 github.com/fluxcd/pkg/masktoken v0.10.0 github.com/fluxcd/pkg/oci v0.72.0 - github.com/fluxcd/pkg/runtime v0.120.1-0.20261022125904-05e84321fa52 + github.com/fluxcd/pkg/runtime v0.121.0 github.com/fluxcd/pkg/sourceignore v0.21.0 - github.com/fluxcd/pkg/spiffe v0.0.0-20261022125904-05e84321fa52 + github.com/fluxcd/pkg/spiffe v0.1.0 github.com/fluxcd/pkg/ssh v0.28.0 github.com/fluxcd/pkg/tar v1.3.0 github.com/fluxcd/pkg/testserver v0.15.0 @@ -224,6 +224,7 @@ require ( github.com/go-openapi/validate v0.26.1 // indirect github.com/go-piv/piv-go/v2 v2.6.0 // indirect github.com/go-viper/mapstructure/v2 v2.5.0 // indirect + github.com/gobuffalo/flect v1.0.3 // indirect github.com/gobwas/glob v0.2.3 // indirect github.com/gofrs/flock v0.13.0 // indirect github.com/gofrs/uuid v4.4.0+incompatible // indirect diff --git a/go.sum b/go.sum index dfbdc4469..46e7f139b 100644 --- a/go.sum +++ b/go.sum @@ -350,18 +350,18 @@ github.com/fluxcd/gitkit v0.6.0 h1:iNg5LTx6ePo+Pl0ZwqHTAkhbUHxGVSY3YCxCdw7VIFg= github.com/fluxcd/gitkit v0.6.0/go.mod h1:svOHuKi0fO9HoawdK4HfHAJJseZDHHjk7I3ihnCIqNo= github.com/fluxcd/pkg/apis/acl v0.12.0 h1:UWpZDqKxpKDW38wdXIMHV8Uv3POPu2vUrra0JH1frH0= github.com/fluxcd/pkg/apis/acl v0.12.0/go.mod h1:PJrGIu5Bw/6/siO95uhS+pi5oKSU+gVJqHqsPHb8Kok= -github.com/fluxcd/pkg/apis/crypto v0.4.1-0.20261022125904-05e84321fa52 h1:YkDjmLFe0+RhzlFTwHgoO6xLpPYuhoiqQlMo+Qj2tW0= -github.com/fluxcd/pkg/apis/crypto v0.4.1-0.20261022125904-05e84321fa52/go.mod h1:bEwkDgBjLXMVJ2BwmVbbj32PkirmNSicabgl2Jvw7Dk= +github.com/fluxcd/pkg/apis/crypto v0.5.0 h1:buy9YCvrD8IpKqOVCSV9k2wlF2iVPaETLOZBqsbyfVs= +github.com/fluxcd/pkg/apis/crypto v0.5.0/go.mod h1:bEwkDgBjLXMVJ2BwmVbbj32PkirmNSicabgl2Jvw7Dk= github.com/fluxcd/pkg/apis/event v0.32.0 h1:3zyjdUspwQN+EQJPlUXwCvCMbeqtB+azlNmeenAcbuE= github.com/fluxcd/pkg/apis/event v0.32.0/go.mod h1:ALma8dTqo+NbI+z4BawSzt0pAPjetCSNNUSSS/9p0VU= -github.com/fluxcd/pkg/apis/meta v1.36.1-0.20261022125904-05e84321fa52 h1:vKtcpfELWz1l2eOQjWeEbCJZMNH29W4Fwa3EPM37zW0= -github.com/fluxcd/pkg/apis/meta v1.36.1-0.20261022125904-05e84321fa52/go.mod h1:1MLvK2Xu3UdTKBzg+Q1Oj7H9PHeiKsUB1UW3uwxAwiM= -github.com/fluxcd/pkg/artifact v0.25.1-0.20261022125904-05e84321fa52 h1:xUbaB4s9PtJ771fZirtqH8GDg3tnZ8qtTYQup322K4k= -github.com/fluxcd/pkg/artifact v0.25.1-0.20261022125904-05e84321fa52/go.mod h1:9wKz9vfH7zuhteNxQkGw9l7sCN1xW8YZttEWNN8M/L0= -github.com/fluxcd/pkg/auth v0.63.1-0.20261022125904-05e84321fa52 h1:QoZGEf9JonMNcEAacH8EGtSq55dplp18TTSH//M2r7o= -github.com/fluxcd/pkg/auth v0.63.1-0.20261022125904-05e84321fa52/go.mod h1:gWbB4MzuHWalVyI0o4lE84aINidor9+S63ithckm+94= -github.com/fluxcd/pkg/cache v0.17.1-0.20261022125904-05e84321fa52 h1:9ZSGSkmi4OxVCSj9Uzrt6E1Ph0ypPUajsc76pYqUg2s= -github.com/fluxcd/pkg/cache v0.17.1-0.20261022125904-05e84321fa52/go.mod h1:OW3fncHhPnPZFZav5FdEa//cyMHSndI0JS+IM69LXOU= +github.com/fluxcd/pkg/apis/meta v1.37.0 h1:gVwadRI8BrRrl4Rw0ZBJ/VSWh7qNpf7ShmTjDhcVf30= +github.com/fluxcd/pkg/apis/meta v1.37.0/go.mod h1:1MLvK2Xu3UdTKBzg+Q1Oj7H9PHeiKsUB1UW3uwxAwiM= +github.com/fluxcd/pkg/artifact v0.26.0 h1:gqBI9pFOkCCuwxky3rTkwGGKOGokkSUQ5GuNtUdfiJA= +github.com/fluxcd/pkg/artifact v0.26.0/go.mod h1:4P7avVbWbMgqZhckfP9y+wLa01l/4kRLXflp6zU6kVc= +github.com/fluxcd/pkg/auth v0.65.0 h1:AIA+RM4MOyc+iGCMgfuohH1TQsGMBB7Msay7yBLg7IE= +github.com/fluxcd/pkg/auth v0.65.0/go.mod h1:ozzC9/AiqvfeC8BVyFRsUTQAn34gasyElX4aCBHV83c= +github.com/fluxcd/pkg/cache v0.18.0 h1:MllbYkQYJ5zJ5nK52mDWpSov4kSXiCsLJoPeV43y6os= +github.com/fluxcd/pkg/cache v0.18.0/go.mod h1:OW3fncHhPnPZFZav5FdEa//cyMHSndI0JS+IM69LXOU= github.com/fluxcd/pkg/git v0.55.0 h1:jMdW0uAYuzcwIL5rJq5pmDd+jSrgOe6eIOZfdrcAvO8= github.com/fluxcd/pkg/git v0.55.0/go.mod h1:s9AK9ZEG9CtAgkcX4QhQphLsaDBTje32tT5V3knpj1I= github.com/fluxcd/pkg/gittestserver v0.32.0 h1:y1wbnEaOXpEYqAxGMxcsXZTH6W4QjFyTj6m1d1ytjY0= @@ -376,12 +376,12 @@ github.com/fluxcd/pkg/masktoken v0.10.0 h1:jY9tuBIS1T7z9F8nJc9ozT0QrI391Se9vMu0e github.com/fluxcd/pkg/masktoken v0.10.0/go.mod h1:zOCZp3trYmD5/csLOizvIGTi4gn76nnkTFu+vKOJufo= github.com/fluxcd/pkg/oci v0.72.0 h1:nUTGHTd/2qq2yT66anHfXiwE19uJl6VL36zm74iFwbU= github.com/fluxcd/pkg/oci v0.72.0/go.mod h1:5ngbB6SDvo6daD29HZtBbC6CcnHxuI+5JJR/YqtZhKM= -github.com/fluxcd/pkg/runtime v0.120.1-0.20261022125904-05e84321fa52 h1:UD7MGD096C02MqQFDujvSGktyuDhYUnvQhjBePw3bLk= -github.com/fluxcd/pkg/runtime v0.120.1-0.20261022125904-05e84321fa52/go.mod h1:Cw0CQ/tVKS9lgJoyKVouZr1qVnoJcYnSHw3QDzIOOjA= +github.com/fluxcd/pkg/runtime v0.121.0 h1:xFBJ+8To7YEaenBvFZ843MmKST0fQTaALsA+qH0ZzVE= +github.com/fluxcd/pkg/runtime v0.121.0/go.mod h1:8JWIvsBXvHzSOYfuiPNDB6cmHTjWF5m2WyCB1Tiy8Ek= github.com/fluxcd/pkg/sourceignore v0.21.0 h1:p65j81RzXAeL4KJKgbkFRou+xiA0841CUdAsOCSAb5o= github.com/fluxcd/pkg/sourceignore v0.21.0/go.mod h1:N9NFuxbEMm+UPtP9t+Oyx5LEs0uHu7W+WVPmdlIbCeY= -github.com/fluxcd/pkg/spiffe v0.0.0-20261022125904-05e84321fa52 h1:nl8eRotsfHEZS7m7SwZbZidq8F/MGj3yxyH5Vh6bas4= -github.com/fluxcd/pkg/spiffe v0.0.0-20261022125904-05e84321fa52/go.mod h1:tZ+T8ljAM9ZE0VriHSWM4vfvhlq5zv5bNcPKqwZK1lk= +github.com/fluxcd/pkg/spiffe v0.1.0 h1:BpfZHrine91A/tcGjSs6oejT0VyQpDx4nal2KLfE1Fc= +github.com/fluxcd/pkg/spiffe v0.1.0/go.mod h1:IKBOcFXvk4PwdaE86+d25iB3m+l7iYKUxWl2yVDX/Co= github.com/fluxcd/pkg/ssh v0.28.0 h1:DbqDtvJGKzrSfXfB8Hi9T2mbTnNBodijpRZRfxx59wE= github.com/fluxcd/pkg/ssh v0.28.0/go.mod h1:FyaNc49GQcFIHdyZzwmG/9UApGaD5nKXT6CtzWAsaE0= github.com/fluxcd/pkg/tar v1.3.0 h1:PbL5jNN5q/F6nXA8aCH+msM75BS6lSnPX2RNd48f8ls= @@ -497,6 +497,8 @@ github.com/go-test/deep v1.1.1 h1:0r/53hagsehfO4bzD2Pgr/+RgHqhmf+k1Bpse2cTu1U= github.com/go-test/deep v1.1.1/go.mod h1:5C2ZWiW0ErCdrYzpqxLbTX7MG14M9iiw8DgHncVwcsE= github.com/go-viper/mapstructure/v2 v2.5.0 h1:vM5IJoUAy3d7zRSVtIwQgBj7BiWtMPfmPEgAXnvj1Ro= github.com/go-viper/mapstructure/v2 v2.5.0/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM= +github.com/gobuffalo/flect v1.0.3 h1:xeWBM2nui+qnVvNM4S3foBhCAL2XgPU+a7FdpelbTq4= +github.com/gobuffalo/flect v1.0.3/go.mod h1:A5msMlrHtLqh9umBSnvabjsMrCcCpAyzglnDvkbYKHs= github.com/gobwas/glob v0.2.3 h1:A4xDbljILXROh+kObIiy5kIaPYD8e96x1tgBhUI5J+Y= github.com/gobwas/glob v0.2.3/go.mod h1:d3Ez4x06l9bZtSvzIay5+Yzi0fmZzPgnTbPcKjJAkT8= github.com/godbus/dbus/v5 v5.2.2 h1:TUR3TgtSVDmjiXOgAAyaZbYmIeP3DPkld3jgKGV8mXQ= diff --git a/hack/ci/oci-zot-workload-identity-e2e.sh b/hack/ci/oci-zot-workload-identity-e2e.sh index cd88a944a..5c69ce75b 100755 --- a/hack/ci/oci-zot-workload-identity-e2e.sh +++ b/hack/ci/oci-zot-workload-identity-e2e.sh @@ -5,7 +5,8 @@ # It creates a dedicated kind cluster with a ServiceAccount issuer reachable # from a Zot container on the kind network, deploys source-controller, and # verifies that an OCIRepository using the generic provider authenticates to -# Zot with a projected ServiceAccount token. +# Zot with a projected ServiceAccount token. It also installs SPIRE with the +# SPIFFE Broker API and verifies the same using JWT-SVIDs. set -euo pipefail IMG="${IMG:-test/source-controller:latest}" @@ -26,6 +27,25 @@ OCIREPO_A="zot-oidc-a" OCIREPO_B="zot-oidc-b" REV="test" +# SPIFFE Broker API test setup. +SPIRE_NAMESPACE="spire" +SPIRE_TRUST_DOMAIN="example.org" +SPIRE_SERVER_IMAGE="${SPIRE_SERVER_IMAGE:-ghcr.io/spiffe/spire-server:1.15.5}" +SPIRE_AGENT_IMAGE="${SPIRE_AGENT_IMAGE:-ghcr.io/spiffe/spire-agent:1.15.5}" +SPIRE_OIDC_IMAGE="${SPIRE_OIDC_IMAGE:-ghcr.io/spiffe/oidc-discovery-provider:1.15.5}" +BROKER_SA="default" +SPIRE_BROKER_ENDPOINT="dns:///spire-broker.spire:8443" +SPIRE_BROKER_ID="spiffe://${SPIRE_TRUST_DOMAIN}/spire/agent/k8s_psat/broker-cluster/" +SPIRE_OIDC_ISSUER="http://${CLUSTER}-control-plane:8082" +JWT_SA_NAME="zot-puller-jwt" +JWT_SA_NAME_B="zot-puller-jwt-b" +JWT_OBJ_TYPE="ocirepositories.source.toolkit.fluxcd.io" +JWT_OCIREPO_A="zot-jwt-a" +JWT_OCIREPO_B="zot-jwt-b" +JWT_OCIREPO_A_CROSS="zot-jwt-a-cross" +JWT_SPIFFE_A="spiffe://${SPIRE_TRUST_DOMAIN}/zot/ocirepositories/${JWT_OCIREPO_A}" +JWT_SPIFFE_B="spiffe://${SPIRE_TRUST_DOMAIN}/zot/ocirepositories/${JWT_OCIREPO_B}" + ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)" WORKDIR="$(mktemp -d)" CREATED_CLUSTER=false @@ -153,6 +173,477 @@ flux push artifact "oci://localhost:${ZOT_HOST_PORT}/${REPO_B}:${REV}" \ --revision="${REV}" docker rm -f "${ZOT_CONTAINER}" >/dev/null + +############################################################################## +log "Installing SPIRE with the SPIFFE Broker API and OIDC discovery" +############################################################################## +kubectl create namespace "${SPIRE_NAMESPACE}" --dry-run=client -o yaml | kubectl apply -f - >/dev/null + +kubectl apply -f - >/dev/null </dev/null +} + +create_entry "spiffe://${SPIRE_TRUST_DOMAIN}/oidc-discovery-provider" \ + -selector "k8s:ns:${SPIRE_NAMESPACE}" \ + -selector "k8s:sa:spire-agent" \ + -selector "k8s:container-name:oidc-discovery-provider" +create_entry "spiffe://${SPIRE_TRUST_DOMAIN}/ns/${NS}/sa/${BROKER_SA}" \ + -selector "k8s:ns:${NS}" \ + -selector "k8s:sa:${BROKER_SA}" +create_entry "${JWT_SPIFFE_A}" \ + -selector "k8s:resource:${JWT_OBJ_TYPE}" \ + -selector "k8s:namespace:${APP_NS}" \ + -selector "k8s:name:${JWT_OCIREPO_A}" +create_entry "${JWT_SPIFFE_B}" \ + -selector "k8s:resource:${JWT_OBJ_TYPE}" \ + -selector "k8s:namespace:${APP_NS}" \ + -selector "k8s:name:${JWT_OCIREPO_B}" + +kubectl apply -f - >/dev/null < "${WORKDIR}/zot-auth.json" < "${WORKDIR}/zot-auth.json" < "${WORKDIR}/zot-auth.json" </dev/null make -C "${ROOT_DIR}" dev-deploy IMG="${IMG%:*}" TAG="${IMG##*:}" >/dev/null -kubectl -n "${NS}" patch deploy/source-controller --type=json \ - -p='[{"op":"add","path":"/spec/template/spec/containers/0/args/-","value":"--feature-gates=ObjectLevelWorkloadIdentity=true"}]' >/dev/null +kubectl -n "${NS}" patch deploy/source-controller --type=json -p "$(cat </dev/null +kubectl -n "${NS}" patch deploy/source-controller --type=strategic -p "$(cat </dev/null kubectl -n "${NS}" rollout status deploy/source-controller --timeout=2m +for _ in $(seq 1 30); do + logs="$(kubectl -n "${NS}" logs deploy/source-controller)" + if grep -qF "object-level workload identity will use the SPIFFE Broker API" <<<"${logs}"; then + break + fi + sleep 1 +done +grep -qF "object-level workload identity will use the SPIFFE Broker API" <<<"${logs}" \ + || fail "source-controller did not log SPIFFE Broker API usage" ############################################################################## log "Creating ServiceAccounts and OCIRepositories" @@ -262,6 +790,24 @@ metadata: credential.auth.fluxcd.io/provider: kubernetes credential.auth.fluxcd.io/type: jwt credential.auth.fluxcd.io/audiences: wrong-audience +--- +apiVersion: v1 +kind: ServiceAccount +metadata: + name: ${JWT_SA_NAME} + annotations: + credential.auth.fluxcd.io/provider: spiffe + credential.auth.fluxcd.io/type: jwt + credential.auth.fluxcd.io/audiences: ${AUDIENCE} +--- +apiVersion: v1 +kind: ServiceAccount +metadata: + name: ${JWT_SA_NAME_B} + annotations: + credential.auth.fluxcd.io/provider: spiffe + credential.auth.fluxcd.io/type: jwt + credential.auth.fluxcd.io/audiences: ${AUDIENCE} EOF kubectl -n "${APP_NS}" apply -f - >/dev/null < 0 || len(opts.ClientKey) > 0 || len(opts.CAFile) > 0 - - // Check the object-level workload identity feature gate for the providers - // that use the ServiceAccount for authentication. - switch provider { - case sourcev1.GitProviderAzure, sourcev1.GitProviderAWS: - if obj.Spec.ServiceAccountName != "" && !r.ObjectLevelWorkloadIdentity { - const gate = auth.FeatureGateObjectLevelWorkloadIdentity - const msgFmt = "to use spec.serviceAccountName for provider authentication please enable the %s feature gate in the controller" - err := serror.NewStalling(fmt.Errorf(msgFmt, gate), meta.FeatureGateDisabledReason) - conditions.MarkTrue(obj, sourcev1.FetchFailedCondition, meta.FeatureGateDisabledReason, "%s", err) - return nil, err - } - } + withAuth := obj.Spec.SecretRef != nil && + (opts.Username != "" || opts.Password != "" || opts.BearerToken != "" || len(opts.Identity) > 0 || + len(authData[githubapp.KeyAppID]) > 0 || len(authData[githubapp.KeyAppClientID]) > 0) // Resolve the ServiceAccount once, applying the default ServiceAccount when // the object does not specify one. @@ -755,6 +745,9 @@ func (r *GitRepositoryReconciler) getAuthOpts(ctx context.Context, obj *sourcev1 if withCerts { buildOpts = append(buildOpts, cryptov1.WithSecretRefCerts()) } + if withAuth { + buildOpts = append(buildOpts, cryptov1.WithSecretRefAuth()) + } if provider == sourcev1.GitProviderGitHub && len(authData[githubapp.KeyAppBaseURL]) > 0 { buildOpts = append(buildOpts, cryptov1.WithGitHubEnterpriseServer()) } @@ -779,7 +772,8 @@ func (r *GitRepositoryReconciler) getAuthOpts(ctx context.Context, obj *sourcev1 authOpts = append(authOpts, auth.WithClient(r.Client), auth.WithGitURL(u), - auth.WithObject(sourcev1.GroupVersion.Group, sourcev1.GitRepositoryKind, obj)) + auth.WithObject(sourcev1.GroupVersion.Group, sourcev1.GitRepositoryKind, obj), + auth.WithSPIFFEBroker(r.SPIFFEBroker)) if sa != nil { authOpts = append(authOpts, auth.WithServiceAccountName(sa.Name)) } diff --git a/internal/controller/gitrepository_controller_test.go b/internal/controller/gitrepository_controller_test.go index 252c3b919..38aadca4d 100644 --- a/internal/controller/gitrepository_controller_test.go +++ b/internal/controller/gitrepository_controller_test.go @@ -48,7 +48,6 @@ import ( kstatus "github.com/fluxcd/cli-utils/pkg/kstatus/status" "github.com/fluxcd/pkg/apis/meta" "github.com/fluxcd/pkg/artifact/storage" - "github.com/fluxcd/pkg/auth" "github.com/fluxcd/pkg/auth/githubapp" "github.com/fluxcd/pkg/git" "github.com/fluxcd/pkg/gittestserver" @@ -953,15 +952,6 @@ func TestGitRepositoryReconciler_getAuthOpts_provider(t *testing.T) { }, wantErr: "ManagedIdentityCredential", }, - { - name: "azure provider with service account and feature gate for object-level identity disabled", - url: "https://dev.azure.com/foo/bar/_git/baz", - beforeFunc: func(obj *sourcev1.GitRepository) { - obj.Spec.Provider = sourcev1.GitProviderAzure - obj.Spec.ServiceAccountName = "azure-sa" - }, - wantErr: auth.FeatureGateObjectLevelWorkloadIdentity, - }, { name: "aws provider with non codecommit URL", url: "https://github.com/org/repo.git", @@ -970,15 +960,6 @@ func TestGitRepositoryReconciler_getAuthOpts_provider(t *testing.T) { }, wantErr: "invalid AWS CodeCommit Git URL: github.com", }, - { - name: "aws provider with service account and feature gate for object-level identity disabled", - url: "https://git-codecommit.us-east-1.amazonaws.com/v1/repos/my-repo", - beforeFunc: func(obj *sourcev1.GitRepository) { - obj.Spec.Provider = sourcev1.GitProviderAWS - obj.Spec.ServiceAccountName = "aws-sa" - }, - wantErr: auth.FeatureGateObjectLevelWorkloadIdentity, - }, { name: "github provider with no secret ref", url: "https://github.com/org/repo.git", @@ -1102,10 +1083,9 @@ func TestGitRepositoryReconciler_getAuthOpts_provider(t *testing.T) { obj := &sourcev1.GitRepository{} r := &GitRepositoryReconciler{ - Recorder: events.NewFakeRecorder(32, false), - Client: clientBuilder.Build(), - ObjectLevelWorkloadIdentity: false, - patchOptions: getPatchOptions(gitRepositoryReadyCondition.Owned, "sc"), + Recorder: events.NewFakeRecorder(32, false), + Client: clientBuilder.Build(), + patchOptions: getPatchOptions(gitRepositoryReadyCondition.Owned, "sc"), } url, err := url.Parse(tt.url) @@ -1527,10 +1507,9 @@ func TestGitRepositoryReconciler_reconcileArtifact(t *testing.T) { resetChmod(tt.dir, 0o750, 0o600) r := &GitRepositoryReconciler{ - Recorder: events.NewFakeRecorder(32, false), - Storage: testStorage, - ObjectLevelWorkloadIdentity: true, - patchOptions: getPatchOptions(gitRepositoryReadyCondition.Owned, "sc"), + Recorder: events.NewFakeRecorder(32, false), + Storage: testStorage, + patchOptions: getPatchOptions(gitRepositoryReadyCondition.Owned, "sc"), } obj := &sourcev1.GitRepository{ @@ -1677,12 +1656,11 @@ func TestGitRepositoryReconciler_reconcileInclude(t *testing.T) { } r := &GitRepositoryReconciler{ - Client: clientBuilder.Build(), - Recorder: events.NewFakeRecorder(32, false), - Storage: storage, - requeueDependency: dependencyInterval, - ObjectLevelWorkloadIdentity: true, - patchOptions: getPatchOptions(gitRepositoryReadyCondition.Owned, "sc"), + Client: clientBuilder.Build(), + Recorder: events.NewFakeRecorder(32, false), + Storage: storage, + requeueDependency: dependencyInterval, + patchOptions: getPatchOptions(gitRepositoryReadyCondition.Owned, "sc"), } obj := &sourcev1.GitRepository{ @@ -1935,10 +1913,9 @@ func TestGitRepositoryReconciler_reconcileStorage(t *testing.T) { WithScheme(testEnv.GetScheme()). WithStatusSubresource(&sourcev1.GitRepository{}). Build(), - Recorder: events.NewFakeRecorder(32, false), - Storage: testStorage, - ObjectLevelWorkloadIdentity: true, - patchOptions: getPatchOptions(gitRepositoryReadyCondition.Owned, "sc"), + Recorder: events.NewFakeRecorder(32, false), + Storage: testStorage, + patchOptions: getPatchOptions(gitRepositoryReadyCondition.Owned, "sc"), } obj := &sourcev1.GitRepository{ @@ -1989,10 +1966,9 @@ func TestGitRepositoryReconciler_reconcileDelete(t *testing.T) { g := NewWithT(t) r := &GitRepositoryReconciler{ - Recorder: events.NewFakeRecorder(32, false), - Storage: testStorage, - ObjectLevelWorkloadIdentity: true, - patchOptions: getPatchOptions(gitRepositoryReadyCondition.Owned, "sc"), + Recorder: events.NewFakeRecorder(32, false), + Storage: testStorage, + patchOptions: getPatchOptions(gitRepositoryReadyCondition.Owned, "sc"), } obj := &sourcev1.GitRepository{ @@ -2781,10 +2757,9 @@ func TestGitRepositoryReconciler_verifySignature(t *testing.T) { } r := &GitRepositoryReconciler{ - Recorder: events.NewFakeRecorder(32, false), - Client: clientBuilder.Build(), - ObjectLevelWorkloadIdentity: true, - patchOptions: getPatchOptions(gitRepositoryReadyCondition.Owned, "sc"), + Recorder: events.NewFakeRecorder(32, false), + Client: clientBuilder.Build(), + patchOptions: getPatchOptions(gitRepositoryReadyCondition.Owned, "sc"), } obj := &sourcev1.GitRepository{ @@ -2937,11 +2912,10 @@ func TestGitRepositoryReconciler_ConditionsUpdate(t *testing.T) { WithStatusSubresource(&sourcev1.GitRepository{}) r := &GitRepositoryReconciler{ - Client: clientBuilder.Build(), - Recorder: events.NewFakeRecorder(32, false), - Storage: testStorage, - ObjectLevelWorkloadIdentity: true, - patchOptions: getPatchOptions(gitRepositoryReadyCondition.Owned, "sc"), + Client: clientBuilder.Build(), + Recorder: events.NewFakeRecorder(32, false), + Storage: testStorage, + patchOptions: getPatchOptions(gitRepositoryReadyCondition.Owned, "sc"), } key := client.ObjectKeyFromObject(obj) @@ -3345,9 +3319,8 @@ func TestGitRepositoryReconciler_notify(t *testing.T) { } reconciler := &GitRepositoryReconciler{ - Recorder: recorder, - ObjectLevelWorkloadIdentity: true, - patchOptions: getPatchOptions(gitRepositoryReadyCondition.Owned, "sc"), + Recorder: recorder, + patchOptions: getPatchOptions(gitRepositoryReadyCondition.Owned, "sc"), } reconciler.notify(ctx, oldObj, newObj, tt.commit, tt.res, tt.resErr) diff --git a/internal/controller/helmchart_controller_test.go b/internal/controller/helmchart_controller_test.go index 762dce5c6..94c6f7de4 100644 --- a/internal/controller/helmchart_controller_test.go +++ b/internal/controller/helmchart_controller_test.go @@ -2534,7 +2534,7 @@ func TestHelmChartReconciler_reconcileSourceFromOCI_authStrategy(t *testing.T) { provider: "aws", providerImg: "oci://123456789000.dkr.ecr.us-east-2.amazonaws.com/test", assertConditions: []metav1.Condition{ - *conditions.TrueCondition(sourcev1.FetchFailedCondition, "Unknown", "unknown build error: unable to get authentication data from OIDC: failed to get access token for artifact registry"), + *conditions.TrueCondition(sourcev1.FetchFailedCondition, "Unknown", "unknown build error: unable to get authentication data from OIDC: failed to get access credential for artifact registry"), }, }, { diff --git a/internal/controller/ocirepository_controller.go b/internal/controller/ocirepository_controller.go index b97dd25ad..5329432e3 100644 --- a/internal/controller/ocirepository_controller.go +++ b/internal/controller/ocirepository_controller.go @@ -63,6 +63,7 @@ import ( rreconcile "github.com/fluxcd/pkg/runtime/reconcile" "github.com/fluxcd/pkg/runtime/secrets" "github.com/fluxcd/pkg/sourceignore" + fluxbroker "github.com/fluxcd/pkg/spiffe/broker" "github.com/fluxcd/pkg/tar" "github.com/fluxcd/pkg/version" ctrl "sigs.k8s.io/controller-runtime" @@ -146,8 +147,7 @@ type OCIRepositoryReconciler struct { TokenCache *cache.TokenCache CosignVerifierFactory *scosign.CosignVerifierFactory DefaultServiceAccount string - - ObjectLevelWorkloadIdentity bool + SPIFFEBroker fluxbroker.Interface requeueDependency time.Duration @@ -376,6 +376,9 @@ func (r *OCIRepositoryReconciler) reconcileSource(ctx context.Context, sp *patch cryptov1.WithServiceProvider(provider), cryptov1.WithOCI(), } + if obj.Spec.SecretRef != nil { + buildOpts = append(buildOpts, cryptov1.WithSecretRefAuth()) + } if sa != nil { buildOpts = append(buildOpts, cryptov1.WithAnnotations(sa.Annotations)) if len(sa.ImagePullSecrets) > 0 { @@ -432,18 +435,11 @@ func (r *OCIRepositoryReconciler) reconcileSource(ctx context.Context, sp *patch // Workload identity kicks in when the provider is not generic or // the SA has opted into object-level workload identity. if _, ok := keychain.(soci.Anonymous); ok && (provider != sourcev1.GenericOCIProvider || cryptoSpec.Auth.OptedIn()) { - // Check the object-level workload identity feature gate. - if sa != nil && !r.ObjectLevelWorkloadIdentity { - const gate = auth.FeatureGateObjectLevelWorkloadIdentity - const msgFmt = "to use spec.serviceAccountName for provider authentication please enable the %s feature gate in the controller" - err := fmt.Errorf(msgFmt, gate) - return sreconcile.ResultEmpty, serror.NewStalling(err, meta.FeatureGateDisabledReason) - } - opts := auth.OptionsFromSpec(cryptoSpec.Auth) opts = append(opts, auth.WithClient(r.Client), - auth.WithObject(sourcev1.GroupVersion.Group, sourcev1.OCIRepositoryKind, obj)) + auth.WithObject(sourcev1.GroupVersion.Group, sourcev1.OCIRepositoryKind, obj), + auth.WithSPIFFEBroker(r.SPIFFEBroker)) if sa != nil { opts = append(opts, auth.WithServiceAccountName(sa.Name)) } diff --git a/internal/controller/ocirepository_controller_test.go b/internal/controller/ocirepository_controller_test.go index 1a206fc42..8907d84d0 100644 --- a/internal/controller/ocirepository_controller_test.go +++ b/internal/controller/ocirepository_controller_test.go @@ -769,7 +769,7 @@ func TestOCIRepository_reconcileSource_authStrategy(t *testing.T) { crane.Insecure, }, assertConditions: []metav1.Condition{ - *conditions.TrueCondition(sourcev1.FetchFailedCondition, sourcev1.OCIPullFailedReason, "%s", "failed to get access token for artifact registry"), + *conditions.TrueCondition(sourcev1.FetchFailedCondition, sourcev1.OCIPullFailedReason, "%s", "failed to get access credential for artifact registry"), }, }, { @@ -3186,75 +3186,7 @@ func TestOCIRepository_invalidURL(t *testing.T) { g.Expect(stalledCondition.Reason).Should(Equal(sourcev1.URLInvalidReason)) } -func TestOCIRepository_objectLevelWorkloadIdentityFeatureGate(t *testing.T) { - g := NewWithT(t) - - ns, err := testEnv.CreateNamespace(ctx, "ocirepository-olwifg-test") - g.Expect(err).ToNot(HaveOccurred()) - defer func() { g.Expect(testEnv.Delete(ctx, ns)).To(Succeed()) }() - - err = testEnv.Create(ctx, &corev1.ServiceAccount{ - ObjectMeta: metav1.ObjectMeta{ - Namespace: ns.Name, - Name: "test", - }, - }) - g.Expect(err).NotTo(HaveOccurred()) - - obj := &sourcev1.OCIRepository{ - ObjectMeta: metav1.ObjectMeta{ - GenerateName: "ocirepository-reconcile", - Namespace: ns.Name, - }, - Spec: sourcev1.OCIRepositorySpec{ - URL: "oci://ghcr.io/stefanprodan/manifests/podinfo", - Interval: &metav1.Duration{Duration: 60 * time.Minute}, - Provider: "aws", - ServiceAccountName: "test", - }, - } - - g.Expect(testEnv.Create(ctx, obj)).To(Succeed()) - - key := client.ObjectKey{Name: obj.Name, Namespace: obj.Namespace} - resultobj := &sourcev1.OCIRepository{} - - g.Eventually(func() bool { - if err := testEnv.Get(ctx, key, resultobj); err != nil { - return false - } - return conditions.IsStalled(resultobj) - }, timeout).Should(BeTrue()) - - stalledCondition := conditions.Get(resultobj, meta.StalledCondition) - g.Expect(stalledCondition).ToNot(BeNil()) - g.Expect(stalledCondition.Reason).Should(Equal(meta.FeatureGateDisabledReason)) - g.Expect(stalledCondition.Message).Should(Equal("to use spec.serviceAccountName for provider authentication please enable the ObjectLevelWorkloadIdentity feature gate in the controller")) - - testOCIRepositoryReconciler.ObjectLevelWorkloadIdentity = true - t.Cleanup(func() { testOCIRepositoryReconciler.ObjectLevelWorkloadIdentity = false }) - - g.Eventually(func() bool { - if err := testEnv.Get(ctx, key, resultobj); err != nil { - return false - } - resultobj.Annotations = map[string]string{ - meta.ReconcileRequestAnnotation: time.Now().Format(time.RFC3339), - } - return testEnv.Update(ctx, resultobj) == nil - }).Should(BeTrue()) - - g.Eventually(func() bool { - if err := testEnv.Get(ctx, key, resultobj); err != nil { - return false - } - logOCIRepoStatus(t, resultobj) - return !conditions.IsReady(resultobj) && - conditions.GetReason(resultobj, meta.ReadyCondition) == sourcev1.OCIPullFailedReason - }, timeout).Should(BeTrue()) -} - -func TestOCIRepository_genericObjectLevelWorkloadIdentity(t *testing.T) { +func TestOCIRepository_genericWorkloadIdentity(t *testing.T) { g := NewWithT(t) ns, err := testEnv.CreateNamespace(ctx, "ocirepository-generic-olwi-test") @@ -3291,20 +3223,6 @@ func TestOCIRepository_genericObjectLevelWorkloadIdentity(t *testing.T) { key := client.ObjectKey{Name: obj.Name, Namespace: obj.Namespace} resultobj := &sourcev1.OCIRepository{} - g.Eventually(func() bool { - if err := testEnv.Get(ctx, key, resultobj); err != nil { - return false - } - return conditions.IsStalled(resultobj) - }, timeout).Should(BeTrue()) - - stalledCondition := conditions.Get(resultobj, meta.StalledCondition) - g.Expect(stalledCondition).ToNot(BeNil()) - g.Expect(stalledCondition.Reason).Should(Equal(meta.FeatureGateDisabledReason)) - - testOCIRepositoryReconciler.ObjectLevelWorkloadIdentity = true - t.Cleanup(func() { testOCIRepositoryReconciler.ObjectLevelWorkloadIdentity = false }) - g.Eventually(func() bool { if err := testEnv.Get(ctx, key, resultobj); err != nil { return false diff --git a/internal/features/features.go b/internal/features/features.go index 661d3eaa1..bc4855075 100644 --- a/internal/features/features.go +++ b/internal/features/features.go @@ -36,14 +36,11 @@ var features = map[string]bool{ // CacheSecretsAndConfigMaps // opt-in from v0.34 CacheSecretsAndConfigMaps: false, - - // ObjectLevelWorkloadIdentity - // opt-in from Flux v2.6. - auth.FeatureGateObjectLevelWorkloadIdentity: false, } var retiredFeatures = map[string]string{ - // TODO: retire ObjectLevelWorkloadIdentity in Flux 2.11. + // Retired in Flux 2.10. + auth.FeatureGateObjectLevelWorkloadIdentity: auth.FeatureGateObjectLevelWorkloadIdentityRetirementReason, } // FeatureGates contains a list of all supported feature gates and diff --git a/main.go b/main.go index df069b33c..fb685eb98 100644 --- a/main.go +++ b/main.go @@ -18,7 +18,6 @@ package main import ( "crypto/tls" - "errors" "fmt" "os" "time" @@ -57,6 +56,7 @@ import ( "github.com/fluxcd/pkg/runtime/pprof" "github.com/fluxcd/pkg/runtime/probes" fluxspiffe "github.com/fluxcd/pkg/spiffe" + fluxbroker "github.com/fluxcd/pkg/spiffe/broker" sourcev1 "github.com/fluxcd/source-controller/api/v1" @@ -122,6 +122,8 @@ func main() { defaultServiceAccount string authorizedArtifactClients []string authorizedEventServers []string + spiffeBrokerEndpoint string + spiffeBrokerIDs []string ) flag.StringVar(&metricsAddr, "metrics-addr", envOrDefault("METRICS_ADDR", ":8080"), @@ -154,6 +156,10 @@ func main() { "SPIFFE ID authorized to fetch artifacts from the artifact server. Can be repeated.") flag.StringArrayVar(&authorizedEventServers, fluxspiffe.ControllerFlagAuthorizedEventServerSPIFFEID, nil, "SPIFFE ID of an event server authorized to receive events. Can be repeated.") + flag.StringVar(&spiffeBrokerEndpoint, fluxspiffe.ControllerFlagSPIFFEBrokerEndpoint, "", + "The gRPC URL of the SPIFFE Broker endpoint.") + flag.StringArrayVar(&spiffeBrokerIDs, fluxspiffe.ControllerFlagSPIFFEBrokerID, nil, + "SPIFFE ID of the SPIFFE Broker endpoint. Can be repeated.") artifactOptions.BindFlags(flag.CommandLine) clientOptions.BindFlags(flag.CommandLine) @@ -176,17 +182,6 @@ func main() { os.Exit(1) } - objectLevelWorkloadIdentity, err := features.Enabled(auth.FeatureGateObjectLevelWorkloadIdentity) - if err != nil { - setupLog.Error(err, "unable to check feature gate "+auth.FeatureGateObjectLevelWorkloadIdentity) - os.Exit(1) - } - if defaultServiceAccount != "" && !objectLevelWorkloadIdentity { - setupLog.Error(errors.New("cannot set default service accounts when the "+ - auth.FeatureGateObjectLevelWorkloadIdentity+" feature gate is not enabled"), "invalid configuration") - os.Exit(1) - } - if err := intervalJitterOptions.SetGlobalJitter(nil); err != nil { setupLog.Error(err, "unable to set global jitter") os.Exit(1) @@ -206,7 +201,9 @@ func main() { cacheRecorder := cache.MustMakeMetrics() var eventTLSConfig *tls.Config - if len(authorizedArtifactClients) > 0 || len(authorizedEventServers) > 0 { + var spiffeBroker fluxbroker.Client + if len(authorizedArtifactClients) > 0 || len(authorizedEventServers) > 0 || + spiffeBrokerEndpoint != "" || len(spiffeBrokerIDs) > 0 { source, err := workloadapi.NewX509Source(ctx) if err != nil { setupLog.Error(err, "unable to create SPIFFE X.509 source") @@ -236,6 +233,21 @@ func main() { } setupLog.Info("event server will use inter-controller mTLS via SPIFFE", "spiffeIDs", authorizedEventServers) } + + if spiffeBrokerEndpoint != "" || len(spiffeBrokerIDs) > 0 { + spiffeBroker, err = fluxbroker.New(spiffeBrokerEndpoint, spiffeBrokerIDs, source) + if err != nil { + setupLog.Error(err, "unable to create SPIFFE Broker client") + os.Exit(1) + } + defer func() { + if err := spiffeBroker.Close(); err != nil { + setupLog.Error(err, "unable to close SPIFFE Broker client") + } + }() + setupLog.Info("object-level workload identity will use the SPIFFE Broker API", + "endpoint", spiffeBrokerEndpoint, "spiffeIDs", spiffeBrokerIDs) + } } eventRecorderOpts := []events.RecorderOption{events.WithManager(mgr)} @@ -278,14 +290,14 @@ func main() { } if err := (&controller.GitRepositoryReconciler{ - Client: mgr.GetClient(), - Recorder: eventRecorder, - Metrics: metrics, - Storage: storage, - ControllerName: controllerName, - TokenCache: tokenCache, - DefaultServiceAccount: defaultServiceAccount, - ObjectLevelWorkloadIdentity: objectLevelWorkloadIdentity, + Client: mgr.GetClient(), + Recorder: eventRecorder, + Metrics: metrics, + Storage: storage, + ControllerName: controllerName, + TokenCache: tokenCache, + DefaultServiceAccount: defaultServiceAccount, + SPIFFEBroker: spiffeBroker, }).SetupWithManager(mgr, controller.GitRepositoryReconcilerOptions{ DependencyRequeueInterval: requeueDependency, RateLimiter: helper.GetRateLimiter(rateLimiterOptions), @@ -330,14 +342,14 @@ func main() { } if err := (&controller.BucketReconciler{ - Client: mgr.GetClient(), - Recorder: eventRecorder, - Metrics: metrics, - Storage: storage, - ControllerName: controllerName, - TokenCache: tokenCache, - DefaultServiceAccount: defaultServiceAccount, - ObjectLevelWorkloadIdentity: objectLevelWorkloadIdentity, + Client: mgr.GetClient(), + Recorder: eventRecorder, + Metrics: metrics, + Storage: storage, + ControllerName: controllerName, + TokenCache: tokenCache, + DefaultServiceAccount: defaultServiceAccount, + SPIFFEBroker: spiffeBroker, }).SetupWithManager(mgr, controller.BucketReconcilerOptions{ RateLimiter: helper.GetRateLimiter(rateLimiterOptions), }); err != nil { @@ -346,15 +358,15 @@ func main() { } if err := (&controller.OCIRepositoryReconciler{ - Client: mgr.GetClient(), - Storage: storage, - Recorder: eventRecorder, - ControllerName: controllerName, - TokenCache: tokenCache, - CosignVerifierFactory: CosignVerifierFactory, - Metrics: metrics, - DefaultServiceAccount: defaultServiceAccount, - ObjectLevelWorkloadIdentity: objectLevelWorkloadIdentity, + Client: mgr.GetClient(), + Storage: storage, + Recorder: eventRecorder, + ControllerName: controllerName, + TokenCache: tokenCache, + CosignVerifierFactory: CosignVerifierFactory, + Metrics: metrics, + DefaultServiceAccount: defaultServiceAccount, + SPIFFEBroker: spiffeBroker, }).SetupWithManager(mgr, controller.OCIRepositoryReconcilerOptions{ RateLimiter: helper.GetRateLimiter(rateLimiterOptions), }); err != nil {