DevSecOps

FluxCD Security Model in Production

A production-focused look at FluxCD's security model, covering multi-tenancy isolation, source verification, image automation risks, and the CVE history behind the current defaults.

Shadab Khan
Security Engineer
7 min read

FluxCD is the other major GitOps controller, and its security model is noticeably different from ArgoCD's. Where ArgoCD concentrates authority in a single controller with a global view, Flux is composed of smaller controllers, each with a narrower responsibility, and the security story is largely about how those controllers interact. In a production deployment with dozens of tenants and thousands of Kustomizations, the distinction matters.

This post covers the FluxCD security model as it exists in Flux v2.3 and v2.4, with notes on what has changed since the original v2.0 GA in 2023. It focuses on multi-tenancy isolation, source verification, the image automation controller risks, and the CVE history that has shaped the current defaults. It assumes you are running Flux in production or about to, not that you are evaluating it for the first time.

The controller decomposition and why it matters

Flux v2 is made up of source-controller, kustomize-controller, helm-controller, notification-controller, image-reflector-controller, and image-automation-controller. Each is a separate deployment with its own service account, and the interactions between them happen through Kubernetes custom resources rather than internal function calls. This design is a security feature. A bug in the helm-controller's chart rendering logic, like CVE-2024-25989 in v2.2.2, is scoped to the helm-controller's service account permissions, which can be independently restricted from the kustomize-controller's.

The practical implication is that you should configure the service account for each controller to only have the Kubernetes permissions it actually needs. The default install grants all controllers a broadly permissive cluster role. Flux provides a --cluster-domain and --no-cross-namespace-refs flag combination that, when set on every controller, prevents a Kustomization in tenant-A from referencing a source or a secret in tenant-B's namespace. This is the foundation of multi-tenancy isolation in Flux, and turning it on is the single most impactful hardening step for any multi-tenant installation.

Flux's documented multi-tenancy lockdown configuration looks like this in the controller deployment:

args:
  - --watch-all-namespaces
  - --log-level=info
  - --log-encoding=json
  - --enable-leader-election
  - --no-cross-namespace-refs=true
  - --default-service-account=flux-tenant-sa
  - --cluster-domain=cluster.local

The --default-service-account flag is the subtle one. When set, every Kustomization and HelmRelease that does not specify its own spec.serviceAccountName is treated as if it ran under the named service account. Create a low-privilege service account per tenant namespace with exactly the permissions that tenant's workloads need, set it as the default, and a Kustomization that tries to create a ClusterRoleBinding will be rejected because the default service account cannot write cluster-scoped resources.

Source verification is opt-in and should not be

Flux's source-controller can reconcile from git repositories, OCI registries, Helm repositories, and S3-compatible buckets. For each source type, there is a corresponding verification mechanism: git signed commits, OCI image signatures via cosign, Helm chart provenance, and bucket object signatures. All of them are opt-in, and most Flux installations I have reviewed have none of them enabled.

The right default for a production installation is to require signed sources for every GitRepository, OCIRepository, and HelmChart that targets a production namespace. For git, this means setting spec.verify.mode: HEAD and spec.verify.secretRef pointing to a secret with the public GPG key of each trusted signer. For OCI, it means setting spec.verify.provider: cosign and configuring a keyless verification policy that checks the image was signed by a Fulcio-issued certificate matching your organization's Sigstore identity.

The CVE that made this urgent was CVE-2023-28108 in source-controller v1.0.0, which allowed a specifically crafted Helm chart to cause the controller to load arbitrary files from the filesystem during rendering. The fix shipped quickly, but the lesson was that an unverified source is an attack surface, and treating chart verification as optional is a mistake. With cosign verification enforced, a supply chain attack that substitutes a malicious chart at the OCI registry cannot succeed unless the attacker also controls the signing key.

Image automation is where credentials leak

The image-automation-controller is the Flux component that watches OCI registries for new image tags and writes updated manifests back to git. It holds both registry pull credentials and git push credentials, which makes it the highest-value target in the Flux controller set.

Two mistakes are common. First, configuring the controller with a single broadly-scoped git push token that can write to every repository Flux manages. This is the default path in tutorials and it means one compromised image-automation-controller gives an attacker write access to your entire GitOps configuration repository. The correct configuration uses per-ImageUpdateAutomation credentials scoped to only the target repository, stored as GitRepository-referenced secrets with push permissions to only that repository.

Second, using long-lived registry credentials. For AWS ECR, Flux supports IRSA and will refresh credentials automatically. For GCP Artifact Registry, Workload Identity works. For Docker Hub, there is no good story except rotating the token frequently and scoping it to read-only on specific repositories. If you find yourself with a Docker Hub personal access token with write permissions stored as a Flux secret, fix that before you read any further.

Tenant isolation in a shared cluster

The multi-tenancy model in Flux v2 assumes that each tenant gets their own Flux-managed namespace with a service account, a set of GitRepository and Kustomization resources, and that the --no-cross-namespace-refs flag prevents any tenant from referencing another tenant's sources. This works, but it requires that the AppProject-equivalent boundary be consistently enforced.

The practical question is what happens when tenant-A's git repository contains a malicious Kustomization that tries to create a resource in tenant-B's namespace. With the multi-tenancy flags set correctly, the kustomize-controller will refuse to apply it because the tenant-A service account does not have permissions in tenant-B's namespace, and the apply will fail with a clear error. Without those flags, the controller has cluster-admin permissions and the malicious apply succeeds silently.

This is why the multi-tenancy lockdown is not optional for any installation with more than one tenant. The Flux team has been explicit about this in the upstream documentation since v2.2, but the defaults in many Helm charts still do not enable the flags.

Notifications and the webhook attack surface

The notification-controller is the part of Flux that sends alerts to Slack, Teams, email, or webhook receivers. It is also the component that receives webhooks from git providers to trigger immediate reconciliation. The receiver endpoint is publicly exposed in many installations, and CVE-2024-23652 in v2.2.2 was a request-smuggling bug in the receiver logic that allowed an unauthenticated attacker to trigger reconciliation of arbitrary GitRepository resources.

The hardening step here is to require token verification on every Receiver resource, rotate the tokens, and restrict ingress to the receiver endpoint to the IP ranges of your git provider's webhook sources. GitHub publishes the current webhook IP ranges at https://api.github.com/meta, and a Kubernetes NetworkPolicy based on that range cuts off the vast majority of internet-scanning noise.

How Safeguard Helps

Safeguard integrates with FluxCD by ingesting the signed source references from GitRepository and OCIRepository resources, verifying the cosign attestations against your policy gates, and alerting when a Kustomization attempts to apply from an unverified source. The platform also tracks the service account permissions of each Flux controller, flags installations that lack the multi-tenancy lockdown flags, and monitors the notification-controller for the specific CVE patterns discussed in this post. Combined with continuous policy evaluation of image automation credentials, Safeguard gives you an auditable view of whether your Flux estate is actually running under the hardened configuration you believe it is.

Never miss an update

Weekly insights on software supply chain security, delivered to your inbox.