Git, CI/CD & GitOpsLXXXIII · GitOps RBACFluxTenancy
Flux multi-tenancy — lockdown flags and the per-namespace model
What you'll learn
- Describe the platform-admin and tenant split that the Flux tenancy model assumes
- Apply the lockdown flags and explain the specific attack each one closes
- Configure the per-namespace service account model so a missing field is safe rather than privileged
- Identify what the lockdown does not cover and which controls close the remainder
Prerequisites
Verified against Git 2.55.x teaching target; 2.40+ minimum · GitHub Actions continuous service; Aug 2026 documentation baseline · Argo CD v3.5.x teaching target; v3.0+ minimum · Flux v2.9.x · Sigstore Cosign v3.1.x · SLSA v1.2 · OCI Distribution Specification v1.1 · Git LFS v3.7.1 · Kubernetes (cross-course target) 1.36.x
Flux takes a different route to multi-tenancy than Argo CD. There is no project object, no policy CSV, and no user model — the controllers deliberately push every authorisation question down to Kubernetes RBAC. What Flux adds is a handful of start-up flags that remove the ways a tenant could otherwise step around that RBAC. Understanding the model means understanding what each flag closes, because with the flags off the default installation is a single-tenant one.
The two roles the model assumes
The Flux documentation splits responsibility explicitly. Platform admins have unrestricted API access, install Flux, register tenant repositories, create tenant namespaces, and define tenant RBAC; their own repository reconciles under cluster-admin. Tenants have restricted access, own one or more namespaces, and register their own sources and Kustomizations inside them.
flowchart TB
PA["Platform admin repo"] -->|"reconciled as cluster-admin"| FS["flux-system Kustomization"]
FS --> NS["Tenant namespaces, service accounts, RBAC"]
NS --> T1["Tenant A namespace"]
NS --> T2["Tenant B namespace"]
TR["Tenant A repo"] --> T1
T1 -->|"impersonates"| SA["Tenant A service account"]
SA --> API["API server authorises per request"]
Everything a tenant can do is therefore the intersection of what its service account holds and what the controller flags permit. Neither half works alone: RBAC without the flags leaves cross-namespace routes open, and flags without RBAC leave the tenant applying as whatever identity happens to be default.
The lockdown flags
Four patches applied at bootstrap turn the default installation into a multi-tenant one.
patches:
- patch: |
- op: add
path: /spec/template/spec/containers/0/args/-
value: --no-cross-namespace-refs=true
target:
kind: Deployment
name: "(kustomize-controller|helm-controller|notification-controller|image-reflector-controller|image-automation-controller)"
- patch: |
- op: add
path: /spec/template/spec/containers/0/args/-
value: --no-remote-bases=true
target:
kind: Deployment
name: "kustomize-controller"
- patch: |
- op: add
path: /spec/template/spec/containers/0/args/-
value: --default-service-account=default
target:
kind: Deployment
name: "(kustomize-controller|helm-controller)"
What each one closes:
--no-cross-namespace-refs=truestops a Kustomization or HelmRelease in one namespace referencing a source, secret reference, or event subscription in another. Without it, a tenant can point at another tenant GitRepository and reconcile someone else desired state, or subscribe to their notification events.--no-remote-bases=truestops Kustomize overlays resolving external bases at reconcile time. This is both a supply-chain control and a hermeticity one: with remote bases allowed, cluster state can change when an external repository changes, with no commit anywhere in your history.--default-service-accountmakes every Kustomization and HelmRelease without an explicitspec.serviceAccountNameimpersonate a named account in its own namespace instead of running as the controller. Pointing it atdefault, which normally holds no permissions, means an omitted field fails closed.
The per-namespace model in practice
Each tenant gets a namespace, a service account in it, a RoleBinding granting the verbs that tenant needs, and Flux custom resources that name the account explicitly.
flux create tenant dev-team --with-namespace=apps-dev --export
The command generates the namespace, service account and role binding scaffolding — it is documented as being in preview, so most teams generate once and then keep the output under review in the platform repository rather than running it against a live cluster.
kubectl apply -f rbac.yaml
The tenant Kustomization then names the account:
apiVersion: kustomize.toolkit.fluxcd.io/v1
kind: Kustomization
metadata:
name: apps-dev
namespace: apps-dev
spec:
interval: 10m
sourceRef:
kind: GitRepository
name: apps-dev
path: ./deploy
prune: true
serviceAccountName: apps-dev-reconciler
targetNamespace: apps-dev
What the lockdown does not cover
- Resource consumption. A tenant that requests enormous manifests or reconciles every ten seconds degrades shared controllers. Tenancy here is an authorisation boundary, not a resource one; ResourceQuota, LimitRange and sensible intervals cover the remainder.
- Node and network isolation. Workloads from different tenants still share nodes and a flat network unless NetworkPolicy, taints and topology constraints say otherwise.
- Secret material in the source. Blocking cross-namespace references does not stop a tenant committing a plaintext secret into their own repository.
- Cluster-role aggregation. Flux grants the built-in view, edit and admin roles access to Flux custom resources by default, so a tenant with namespace admin can manage Flux objects there. That is usually desirable, and it is removable if it is not.
Production discipline
- All four patches, at bootstrap, in the platform repository. Retrofitting them onto a running estate breaks whichever tenants quietly depended on the open behaviour, and finding that out during an incident is worse.
serviceAccountNameis mandatory in tenant custom resources. A CI check rejects tenant manifests without it, so the safe default is a backstop rather than the mechanism.- The platform path is separately reviewed and separately signed. Different repository or different owners, never the same gate as a tenant path.
- Verify the boundary, do not assert it. A periodic job that applies a deliberately over-reaching tenant manifest and expects a forbidden error is the only proof the lockdown is still in place.
Cross-course references
- Kubernetes for Production Sysadmins - Part CVII (MultiTenancy), Part CVIII (ResourceQuota) and Part XLIV (NetworkPolicy) cover the isolation dimensions the lockdown deliberately leaves out.
- Git, CI/CD & GitOps for Infrastructure Engineers - Part LXXVIII (Flux) introduced the controllers these flags configure, and Part LXVII (DependencyPinning) covers the supply-chain reasoning behind blocking remote bases.
- Ceph & Distributed Storage for Production Sysadmins - Part XXXIV (MultiTen) covers the same platform-admin and tenant split where the shared resource is storage rather than an API server.
Quiz
Knowledge check · 4 questions
Q1. A tenant writes a Kustomization and omits spec.serviceAccountName on a Flux installation with no default configured. What identity performs the apply?
Q2. Blocking remote bases is a hermeticity control as well as a supply-chain one, because with them allowed the cluster can change without any commit in your history.
Q3. Name the three Flux lockdown flags and state the specific route each one closes.
Q4. Determine what the tenant boundary actually enforces before a second team is onboarded.
A platform runs Flux bootstrapped six months ago with no lockdown patches. One team uses it. Tenant Kustomizations live in the same repository as the flux-system path, all reconcile without serviceAccountName, and one references a GitRepository in the flux-system namespace because that is where the first source was created. A second team is due to be onboarded next week into its own namespace.
Passing score: 75%. Answers are checked in this browser.