30.6 Multi-Tenancy with Flux: Namespaced Tenants
Right, so you’ve got Flux humming along, deploying your apps beautifully. But now you’ve got a new problem: other people. Maybe it’s another team, a contractor, or a client who needs a sliver of your cluster. You need to give them a sandbox—a dedicated namespace with GitOps superpowers—without handing them the keys to the entire kingdom and your prized prod database. This, my friend, is where namespaced tenancy in Flux saves the day.
The core idea is brilliantly simple yet powerful: you can scope a Flux Kustomization or HelmRelease to reconcile resources only in a specific namespace. This means you can grant a tenant the ability to deploy their own apps via GitOps into their own namespace, while a central platform team retains control over everything else (like CRDs, cluster-level resources, and other namespaces). It’s the perfect balance of autonomy and control.
The Anatomy of a Namespaced Kustomization
Let’s be clear: a standard Kustomization is a cluster-scoped bully. It’ll deploy things wherever its YAML tells it to. A namespaced one is different. You create it inside the tenant’s namespace, and you set the spec.targetNamespace. This tells Flux, “Everything you find in my source, please reconcile it here, and nowhere else.”
Here’s what that looks like. First, create the tenant’s namespace itself. This is usually something you, the platform admin, would do.
# 1-namespace.yaml (Applied by a cluster-admin Kustomization)
apiVersion: v1
kind: Namespace
metadata:
name: team-brontosaurus
Now, inside that namespace, you place the tenant’s Flux Kustomization. This is the magic piece.
# team-brontosaurus/flux-kustomization.yaml
apiVersion: kustomize.toolkit.fluxcd.io/v1
kind: Kustomization
metadata:
name: team-brontosaurus-apps
namespace: team-brontosaurus # <- CRITICAL: This object lives in the tenant's NS
spec:
interval: 10m
path: "./apps"
prune: true
sourceRef:
kind: GitRepository
name: team-brontosaurus-repo # This GitRepository must also exist in the same namespace
targetNamespace: team-brontosaurus # <- The secret sauce! Forces all resources here.
Notice the two key fields: metadata.namespace and spec.targetNamespace. The first says where this custom resource lives, the second dictates where the resources it describes will be created. This is the linchpin of the entire setup.
Why This is a Security Masterstroke
This architecture is a gift for security and compliance. Here’s why:
Role-Based Access Control (RBAC) is Simple: You can create a
RoleandRoleBindingin theteam-brontosaurusnamespace that grants the tenant’s identity (e.g., a CI/CD service account) just enough permission to create and updateKustomizationobjects only in that namespace. They can’t even see other namespaces, let alone modify them.# team-brontosaurus/rbac.yaml (Applied by platform team) apiVersion: rbac.authorization.k8s.io/v1 kind: Role metadata: name: flux-controller namespace: team-brontosaurus rules: - apiGroups: ["kustomize.toolkit.fluxcd.io"] resources: ["kustomizations"] verbs: ["get", "list", "watch", "create", "update", "patch", "delete"] --- apiVersion: rbac.authorization.k8s.io/v1 kind: RoleBinding metadata: name: brontosaurus-ci-bot namespace: team-brontosaurus subjects: - kind: ServiceAccount name: brontosaurus-ci-bot # Their CI service account namespace: team-brontosaurus roleRef: kind: Role name: flux-controller apiGroup: rbac.authorization.k8s.ioBlast Radius Containment: If a tenant messes up their
Kustomizationand tries to deploy something malicious or just broken, the damage is confined to their own namespace. They can’t accidentally (or “accidentally”) delete aClusterRoleor disrupt another team’s work.
The Gotchas: Where This “Perfect” Plan Meets Reality
This isn’t all rainbows and unicorns. The designers made some choices that will bite you if you’re not aware. Here are the big ones:
The SourceRef Must Be Namespaced: This is the most common “aha!” moment. In the
Kustomizationexample above, thesourceRefpoints to aGitRepositorynamedteam-brontosaurus-repo. The brutal, non-negotiable rule is that thisGitRepositoryobject must also be created in the same namespace as the Kustomization (team-brontosaurus). You cannot point a namespacedKustomizationto a cluster-scopedGitRepositorysource. This is a deliberate security feature, not an oversight. It means each tenant manages their own source definition.Cluster-Scoped Resources are a No-Go: This is the big limitation. Your tenant cannot deploy anything that exists outside a namespace. This includes
PersistentVolume,ClusterRole,ClusterRoleBinding,CustomResourceDefinition(CRD), and so on. If their application YAML includes one, Flux will reconcile it, the Kubernetes API will accept it, and then it will sit there, foreverReady=Falsewith an error message about “namespaced scope.” The solution? The platform team must pre-install any required cluster-scoped resources (like CRDs) that the tenants need. It’s a trade-off: you lose tenant self-service for these items but gain immense control and stability.Dependency Hell Across Namespaces: What if an app in
team-brontosaurusneeds to talk to a central, platform-managed Redis in theinfranamespace? Your tenant’sKustomizationcan’t create aServicein another namespace. The best practice is to use a pattern like Service Exports (from projects like KubeFed) or have the platform team create the necessaryServiceandEndpointsobjects in the tenant’s namespace to point to the central service. It’s a bit more work, but it keeps the boundaries clean.
The bottom line? Multi-tenancy with namespaced Flux resources is the way to go for 90% of use cases where you need to isolate teams. It leverages native Kubernetes primitives (namespaces, RBAC) beautifully, is simple to understand, and is rock-solid stable. Just remember the rules: keep sources namespaced, pre-install CRDs, and design your inter-team dependencies carefully. Now go forth and delegate responsibly.