Flux CD Flux CD

Set Up Multi-Tenancy with Flux

Give each team its own namespace and repository, and enforce isolation with Kubernetes RBAC.

15 min Advanced

When several teams share a cluster, you want each to deploy its own applications without being able to touch anyone else’s. Flux builds this on Kubernetes primitives: each tenant gets a namespace, a service account, and a role binding, and Flux applies the tenant’s manifests while impersonating that service account. Isolation is enforced by Kubernetes RBAC, with no separate policy engine required. This guide sets up a tenant called dev-team.

The two roles

  • Platform admins own the cluster and the Flux installation. They create tenants and decide what each may access.
  • Tenants own their applications. They commit manifests to their own repository, and Flux applies them inside the tenant’s namespace only.

Create the tenant

The CLI generates the namespace, service account, and RBAC for you. Run it in a clone of the platform repository:

mkdir -p tenants/base/dev-team
flux create tenant dev-team \
  --with-namespace=apps \
  --export > tenants/base/dev-team/rbac.yaml

The generated YAML contains a Namespace named apps, a ServiceAccount named dev-team, and a RoleBinding that grants that service account the cluster-admin role within the apps namespace only. It can’t do anything outside it.

Connect the tenant’s repository

Create a source and Kustomization for the tenant’s own repository, in the tenant’s namespace:

flux create source git dev-team \
  --namespace=apps \
  --url=https://github.com/your-org/dev-team-apps \
  --branch=main \
  --export > tenants/base/dev-team/sync.yaml

flux create kustomization dev-team \
  --namespace=apps \
  --service-account=dev-team \
  --source=GitRepository/dev-team \
  --path="./" \
  --prune=true --interval=5m \
  --export >> tenants/base/dev-team/sync.yaml

The important line is serviceAccountName: dev-team on the Kustomization. It makes kustomize-controller apply the tenant’s manifests as that service account, so any attempt to create resources outside apps, or cluster-wide resources such as a ClusterRole, is rejected by the Kubernetes API. The same field exists on HelmRelease.

Add a kustomization.yaml in tenants/base/dev-team/ listing rbac.yaml and sync.yaml, and reference the tenant from the cluster’s folder, for example through a Flux Kustomization in clusters/production/tenants.yaml with path: ./tenants/production. Commit and push. Flux creates the namespace, the RBAC, and the tenant’s sync, and the team’s own repository takes over from there.

For a private tenant repository, create the credentials in the tenant’s namespace so only that tenant’s objects reference them:

flux create secret git dev-team-auth \
  --namespace=apps \
  --url=https://github.com/your-org/dev-team-apps \
  --username=git --password=<token>

Then add --secret-ref=dev-team-auth to the source.

Lock down the controllers

Service accounts only help if tenants can’t bypass them. Configure the controllers to enforce the rules by patching the Flux installation in clusters/<env>/flux-system/kustomization.yaml:

patches:
  - patch: |
      - op: add
        path: /spec/template/spec/containers/0/args/-
        value: --no-cross-namespace-refs=true
      - op: add
        path: /spec/template/spec/containers/0/args/-
        value: --default-service-account=default
    target:
      kind: Deployment
      name: "(kustomize-controller|helm-controller)"
  - patch: |
      - op: add
        path: /spec/template/spec/containers/0/args/-
        value: --no-cross-namespace-refs=true
    target:
      kind: Deployment
      name: "(notification-controller|image-reflector-controller|image-automation-controller)"

What these flags do:

  • --no-cross-namespace-refs=true stops a tenant’s Kustomization from referencing a source in another namespace, so tenants can’t use each other’s repositories or credentials.
  • --default-service-account=default makes controllers impersonate the default service account in the object’s namespace when none is set. A tenant that forgets serviceAccountName gets almost no permissions instead of Flux’s cluster-admin identity.

Commit the patches to the kustomization.yaml that bootstrap generated in that folder. Re-running bootstrap keeps customizations you add to that file. Check the current Flux multi-tenancy documentation for the exact patch for your version.

Restrict what tenants can do

Beyond the namespace boundary, consider extra controls:

  • Namespace-scoped RBAC. Replace the generated cluster-admin role binding with a narrower role if tenants shouldn’t create, say, Roles or NetworkPolicies.
  • Admission policy. Use Kyverno or a ValidatingAdmissionPolicy to enforce image registries, resource limits, or forbid privileged Pods.
  • Quotas. Add a ResourceQuota and LimitRange to the tenant namespace, alongside the RBAC.
  • Network isolation. Add default-deny NetworkPolicy objects so tenant namespaces can’t talk to each other.

Verify the isolation

Add a manifest that creates something outside the tenant namespace, such as a ClusterRole, to the tenant’s repository and push. Then check:

flux get kustomizations -n apps
kubectl describe kustomization dev-team -n apps

The Kustomization fails with a forbidden error naming the service account, which is exactly what you want. Remove the bad manifest and Flux recovers on its own.

How this compares with project-based tenancy

Some tools model tenants as projects with their own policy objects. Flux has no such concept. A tenant is simply a namespace plus a service account that Flux impersonates, which means the Kubernetes API server is the single place permissions are enforced.

Popular Guides