Kubernetes multi-tenant governance: managing multi-tenant Kubernetes clusters

Most Kubernetes clusters called 'multi-tenant' are really hygiene multi-tenancy. This field guide separates trust tiers, shows what namespaces give you and what they don't, and says when to reach for sandboxed runtimes, virtual clusters, or separate clusters instead of more YAML.

"Multi-tenancy" is doing a lot of hiding. Most teams who say they run a multi-tenant Kubernetes cluster really mean a handful of internal teams sharing namespaces under a trust assumption they have never written down. That is fine as long as everyone knows it. The trouble starts when that phrase gets mistaken for a security model.

The Kubernetes docs themselves are refreshingly blunt about this. The RBAC Good Practices page says that "boundaries within a namespace should be considered weak." The Kubernetes Multi-Tenancy Working Group quietly archived its primary repository in 2023. The Hierarchical Namespace Controller, once the flagship tool for namespace hierarchy, was archived in April 2025. Meanwhile, 88% of organisations told the CNCF 2024 Annual Survey they use namespaces to separate applications inside a cluster, a jump of 16 percentage points year over year. The industry is leaning harder on namespace-based multi-tenancy exactly when the Kubernetes project itself has stepped back from the term.

This article is the field guide I wish the original version of this post had been. It takes a position. The tactical layers matter, and I walk through them. But the real question is simpler than any of them, and most teams skip straight past it.

TL;DR

  • "Multi-tenancy" is really a trust model in disguise. The only question that matters is how much your tenants trust each other.
  • Namespaces, RBAC, ResourceQuota and LimitRange give you cluster hygiene. They do nothing by themselves to isolate one tenant from another. The Kubernetes docs explicitly call namespace boundaries "weak."
  • The control-plane noisy-neighbor problem is solved by API Priority and Fairness, stable since Kubernetes 1.29. Most articles skip it.
  • For untrusted tenants, reach for sandboxed runtimes (gVisor, Kata Containers), virtual clusters (vCluster), or separate clusters. More admission policies will not close the gap.
  • CVE-2025-1974 (IngressNightmare) and the retirement of ingress-nginx in March 2026 are the clearest warning shots against treating shared cluster-scoped resources as tenant boundaries.

Table of contents

  1. What "multi-tenancy" is actually hiding
  2. The only question that matters: how much do tenants trust each other?
  3. What namespaces give you, and what they don't
  4. The noisy-neighbor problem is really two problems
  5. Policy-as-code: Kyverno just graduated, Gatekeeper is still there
  6. Network policies: the ceiling is lower than most teams think
  7. Three kinds of isolation theater
  8. Cost allocation: showback teaches, chargeback enforces
  9. When namespaces are not enough
  10. Key takeaways

What "multi-tenancy" is actually hiding

The Kubernetes documentation on multi-tenancy warns you on the first paragraph that the terms "hard" and "soft" multi-tenancy "can often be confusing, as there is no single definition that will apply to all users." It calls hardness a "broad spectrum," not a category. Then it pivots to a more honest framing: the real axis is whether your tenants trust each other.

I think that framing is still too polite. Here is what I would say instead.

"Multi-tenancy" in Kubernetes covers at least three distinct situations:

  • Several product teams at the same company sharing a cluster to save on operations.
  • A SaaS platform running one application instance per customer, all on the same cluster.
  • A platform that runs arbitrary, untrusted user code (AI execution environments, CI runners for open-source repos, a notebook service).

From a distance these look similar. Each tenant gets a namespace, each namespace gets a quota, each team gets an RBAC binding. But the threat models are not on the same continent. Scenario one is about colleagues accidentally shipping a pod without memory limits. Scenario three is about someone deliberately trying to escape their container and read another tenant's secrets.

This is why the Kubernetes project archived the Multi-Tenancy Working Group: there is no single set of primitives that answers all three cases at once. The working group declared its initial charter complete, handed off its reference projects, and stepped back. HNC itself was archived two years later. That is honest engineering. Namespace hierarchy turned out not to be the abstraction teams actually needed, and stronger isolation layered on top was.

Today's operator is left with a patchwork. Namespaces, RBAC, ResourceQuota, NetworkPolicy, Pod Security Standards, admission webhooks, sandboxed runtimes, virtual clusters, and separate clusters are all valid answers. Each of them answers a different question. Pick the wrong one for your trust model and you get either unnecessary complexity (scenario one with gVisor) or false confidence (scenario three with namespaces and a NetworkPolicy).

The rest of this article is about picking the right layer for the right question.

The only question that matters: how much do tenants trust each other?

Before any YAML, answer this. I use three rough tiers.

Trusted tenants. Internal teams under one company, one security policy, one on-call rotation. You worry about accidents: a pod without limits, a CI runner that opens a thousand API connections, a junior engineer who runs kubectl in the wrong context. You do not seriously worry about one team attacking another. In this world, namespace per team with RBAC, ResourceQuota, LimitRange, Pod Security Standards on baseline, and a default-deny NetworkPolicy is enough. Call it hygiene multi-tenancy. Most teams who describe their cluster as "multi-tenant" live right here.

Semi-trusted tenants. Separate products under one roof with different risk tolerances. Maybe some workloads touch regulated data (PCI, patient records) and some don't. Maybe an operator is installed cluster-wide and touches the same CRDs every tenant uses. You need everything from tier one, plus dedicated node pools enforced by policy, Pod Security Standards on restricted where possible, and an admission controller that blocks tenants from touching cluster-scoped objects. The real work is writing down what "semi-trusted" actually means for your organisation. If the honest answer is "we are not sure," you are probably in tier three.

Untrusted tenants. Arbitrary code from customers, partners, or the internet. This is the tier that breaks namespace-based multi-tenancy. You cannot assume good faith. CVE-2024-21626 (Leaky Vessels) proved in January 2024 how a hostile container image could escape runc to the host filesystem. CVE-2025-1974 (IngressNightmare) proved in March 2025 that an unauthenticated attacker on the pod network could take over the ingress-nginx controller and read every Secret in the cluster. Wiz reported 43% of cloud environments were vulnerable at disclosure. For a tier-three cluster the answer is sandboxed runtimes, virtual control planes, or separate clusters. It is not "write better RBAC."

Here is the honest version: if you cannot name the trust tier your cluster is serving, you are running a tier-three cluster with tier-one controls. That gap is where incidents live.

What namespaces give you, and what they don't

A namespace is two things. It is a name scope, so ten teams can have a Deployment called api without collisions. It is a policy scope, so you can attach Role bindings, quotas, and Pod Security labels per tenant. That is real value. Every pattern in this article, from tier one to tier three, still starts with namespaces.

What namespaces are not, the Kubernetes RBAC Good Practices page says directly: a namespace prevents accidental API calls across tenants. It does nothing by itself to prevent a compromised pod from reaching another tenant's pods over the pod network, exhausting shared node resources, or exploiting a kernel vulnerability to reach the host.

The baseline kit for every namespace-per-tenant cluster looks roughly like this:

# Namespace-scoped hygiene for tenant team-alpha.
# Tier one. This is the hygiene layer.

apiVersion: v1
kind: Namespace
metadata:
  name: team-alpha
  labels:
    pod-security.kubernetes.io/enforce: baseline
    pod-security.kubernetes.io/warn: restricted
    tenant: alpha
    cost-center: CC-1001
---
apiVersion: v1
kind: ResourceQuota
metadata:
  name: quota
  namespace: team-alpha
spec:
  hard:
    requests.cpu: "50"
    requests.memory: 200Gi
    limits.memory: 400Gi
    pods: "200"
    services.loadbalancers: "2"
---
apiVersion: v1
kind: LimitRange
metadata:
  name: defaults
  namespace: team-alpha
spec:
  limits:
    - type: Container
      default:
        memory: 256Mi
      defaultRequest:
        cpu: 50m
        memory: 128Mi
      max:
        cpu: "4"
        memory: 8Gi

A few things are worth pointing out.

Pod Security Standards are applied via labels on the namespace. PSA went stable in Kubernetes 1.25 and replaced PodSecurityPolicy. The baseline profile blocks the obvious privilege escalations like hostNetwork, hostPID, and privileged containers for zero operational cost. restricted is much stricter and a sensible target for new workloads. If your cluster skips this on any tenant namespace, you have accepted whatever Kubernetes defaults to. Call it what you want, but it isn't really multi-tenancy.

LimitRange is there to catch the developer who forgot. In a namespace with ResourceQuota, Kubernetes refuses to schedule a pod without requests and limits, and LimitRange fills them in so work still flows.

pods: "200" is a fuse. A runaway operator can create thousands of pods per minute; the ResourceQuota exists so that trips something loud instead of silently pinning a node.

Cost labels belong on the namespace itself. I come back to this later in the cost allocation section.

The noisy-neighbor problem is really two problems

Most articles about multi-tenant Kubernetes treat the noisy-neighbor question as one problem: a pod without limits eating the CPU of its node neighbours. That is the easy one. Set requests and limits, enforce via LimitRange, done.

The second noisy-neighbor problem is harder and almost never discussed: the control plane.

A tenant can hurt other tenants without using a single extra byte of node memory. A misconfigured GitOps controller creating thousands of objects per minute. A buggy operator reconciling in a tight loop. A CI pipeline that spams kubectl get against the API server. Once the kube-apiserver starts dropping requests, leader election times out, built-in controllers stall, and every tenant's deployments hang. The symptom looks like "Kubernetes is down," not "tenant X is being loud."

The answer is API Priority and Fairness (APF). It went stable in Kubernetes 1.29, with the v1 API default on. APF classifies incoming API requests via FlowSchema objects, assigns them to priority levels, and fair-queues inside each level. The default configuration already protects leader election and built-in controllers from tenant request floods. In the docs' own words: an "ill-behaved Pod that floods the API server with requests cannot prevent leader election or actions by the built-in controllers from succeeding."

For multi-tenant clusters, the operational pattern is simple: create per-tenant FlowSchema objects, rate-limit each tenant into its own PriorityLevelConfiguration, and leave the cluster-control flows at their defaults. Think of this as the control-plane equivalent of ResourceQuota, and treat it as mandatory once the cluster has more than a handful of tenants.

A note on CPU limits, which is the second-most-contested topic in Kubernetes after Helm vs Kustomize. For latency-sensitive long-running services inside a trusted tier, the current practitioner consensus (Tim Hockin and others in SIG Node) is to set memory limits equal to memory requests and skip CPU limits, because CFS throttling causes tail-latency problems on nodes that are otherwise idle. For untrusted or batch workloads in a multi-tenant cluster, CPU limits are still the right call. The tier matters more than the rule. The open GitHub issue on CFS throttling is still the canonical reference if you want the details.

Policy-as-code: Kyverno just graduated, Gatekeeper is still there

On March 24, 2026 the CNCF graduated Kyverno. Graduation is the CNCF's signal that a project has reached production scale. LinkedIn reported 20,000 admission requests per minute across 230+ clusters at graduation. The CEL-based policy engine shipped as v1 stable in Kyverno 1.17 two months earlier.

I already covered Kyverno for namespace bootstrapping in my earlier write-up on generating NetworkPolicy and LimitRange objects on namespace creation, so I will not repeat that here. What matters for multi-tenancy is what a policy engine actually does for you.

Three jobs, in order of importance:

  1. Block obviously bad configurations. Containers running as root in a namespace that should be restricted. Images without a digest pinned. hostPath volumes. Privileged init containers. There are 30 rules you can steal from the Kyverno policy library and never think about again.
  2. Generate the hygiene layer for you. When a new namespace is created with label tenant=foo, auto-generate its default-deny NetworkPolicy, its LimitRange, and its baseline Pod Security labels. Kyverno's generate rules do this out of the box. Gatekeeper does not.
  3. Mutate for safe defaults. If someone omits runAsNonRoot, add it. If someone omits a team label, fill it in from the namespace.

The Kyverno versus Gatekeeper question still comes up a lot. Both work. If your platform team has deep Rego expertise, Gatekeeper is fine and gives you access to OPA's cross-system policy logic. For most Kubernetes-native teams, Kyverno is easier to adopt because it speaks Kubernetes-shaped YAML instead of a second query language, and it ships resource generation and image verification as first-class features. The CEL engine in recent Kyverno versions closes most of the remaining gap with Rego for pure validation logic.

One thing neither tool solves on its own: what happens when the admission webhook itself is down. Plan for it. Set failurePolicy: Fail for security-critical policies, keep the Kyverno or Gatekeeper pods outside the tenant namespaces so they are not accidentally killed by a tenant's ResourceQuota, and make sure you have a break-glass path when your policy controller breaks the cluster at 03:00.

Network policies: the ceiling is lower than most teams think

The default Kubernetes NetworkPolicy is L3/L4. It is still useful, but the list of things it cannot do matters more than the list of things it can.

It cannot filter by hostname. You cannot write "allow egress to api.example.com" in a standard NetworkPolicy. You have to use IP blocks, and most APIs you care about refuse to commit to a stable IP range. Cilium's toFQDNs solves this, GKE has its own FQDNNetworkPolicy, and upstream is slowly coming around.

It cannot do L7. No HTTP methods, no path matching, no headers. If you want service-level authorisation, you need a service mesh.

It does not encrypt anything. Traffic between pods is plaintext by default. If a node is compromised, so is every packet passing through it.

It is silently dependent on your CNI. The NetworkPolicy API is just the spec; enforcement lives in whichever network plugin you installed. If that plugin does not support network policies, Kubernetes will happily accept your NetworkPolicy objects and do absolutely nothing with them.

The new kid on the block is AdminNetworkPolicy (ANP) and BaselineAdminNetworkPolicy (BANP), cluster-scoped policies that a platform admin can enforce before and after tenant-owned NetworkPolicy. This is exactly what you want for multi-tenancy: an admin-set "deny cross-namespace by default" floor that tenants cannot override. As of April 2026 ANP is still v1alpha1 with Calico 3.30+ supporting both kinds, while Cilium users should reach for CiliumClusterwideNetworkPolicy in the meantime.

Default-deny, namespace-scoped NetworkPolicy on every tenant namespace is table stakes. Treat it as the floor you start from and keep going.

Three kinds of isolation theater

These three patterns look like isolation. They are not. Each of them has burned someone in production.

Shared ingress controller with path-based tenant routing. This is the pattern where team A gets /app-a and team B gets /app-b, served by one ingress-nginx deployment, and someone on a Slack call calls it "multi-tenant." The ingress controller has cluster-wide read access to every Secret in the cluster by default, because it needs that to load TLS certificates. An RCE in the controller reads all Secrets. That is exactly what CVE-2025-1974 (IngressNightmare) demonstrated in March 2025: unauthenticated, pod-network-only access was enough to take over the controller. Wiz reported that 43% of cloud environments were vulnerable and 6,500 clusters had the admission webhook exposed to the public internet. The ingress-nginx project retired in March 2026, so there are no more security patches coming either. If you are still running it for anything above hygiene multi-tenancy, moving to Gateway API is overdue. I wrote a step-by-step on that migration without a big-bang cutover for this exact reason.

Shared CRDs as a tenant plane. Custom Resource Definitions are cluster-scoped. Two tenants cannot install two different versions of the same CRD. A tenant who can create or modify CRDs can affect every other tenant. In a namespace-per-tenant cluster, tenants typically cannot manage their own CRDs at all, which blocks most operators and a surprising number of Helm charts. If you find yourself writing policy to let one tenant install a CRD "just for them," you are asking the namespace boundary to do something it cannot do.

Shared admission webhooks. ValidatingWebhookConfiguration and MutatingWebhookConfiguration objects are cluster-scoped too. A malicious or misconfigured webhook can break or poison cluster-wide API traffic, and CVE-2022-3172 showed how an aggregated API server could redirect credential-carrying requests to a third party. Tenants should never be able to create these objects. That sounds obvious until you check what your default RBAC aggregation actually grants.

The common thread: anything cluster-scoped is a shared blast radius by definition. If you are using it to separate tenants, you are not separating tenants.

Cost allocation: showback teaches, chargeback enforces

Technical isolation without cost visibility is incomplete multi-tenancy. You end up subsidising the loudest team out of the general budget and then wondering why the cluster bill keeps climbing.

Two models:

  • Showback reports costs per tenant without actually charging anyone. It is a learning mechanism. "Team beta used €600 of cluster capacity last month." It works best when the team that owns the budget and the team that owns the spend are the same people.
  • Chargeback actually moves money. You book the cost against the tenant's budget line. Useful when nobody on the engineering side feels any financial pressure to optimise.

Implementation in 2026: use OpenCost, which has been CNCF incubating since October 2024, as your open allocation engine. Requests-based allocation is the defensible standard because it matches what the scheduler actually reserves. Tag every namespace with team, cost-center, and environment labels and let the cloud provider's cost reporting pick them up. On EKS, you can now propagate custom Kubernetes labels into the AWS Cost and Usage Report directly.

When namespaces are not enough

This is the "when it's not a good fit" section, and it is the most important one in the article.

If you decide you need more than hygiene multi-tenancy, you have four escalation steps. Pick the lowest step that meets your threat model and stop there.

Step 1: dedicated node pools

Taint a node pool with tenant=foo:NoSchedule, add the matching toleration to the tenant's pods, and enforce via Kyverno or Gatekeeper that no other namespace can add that toleration. You now have per-tenant hardware isolation inside one cluster. GKE explicitly recommends this pattern in its enterprise multi-tenancy guide. The catch: a container escape at the node level still compromises everyone on that node. Kernel isolation is unchanged. If the goal is "don't let tenant A's memory-hungry batch jobs starve tenant B," dedicated node pools are the cheapest answer. If the goal is "protect against a kernel CVE," they are not.

Node pools also interact with the scheduler in unpleasant ways. If a tenant's pods fail to schedule because their node is unhealthy, they need to fail over fast. I wrote about how to keep half-ready nodes out of the scheduler entirely in a separate post; it is the kind of problem that gets worse the more tenants you have sharing a pool.

Step 2: sandboxed container runtimes

gVisor, Kata Containers, and Firecracker-backed runtimes intercept syscalls in user space or run each pod in a lightweight VM. They block the exact class of exploit that CVE-2024-21626 used. GKE Sandbox gives you gVisor with a node-pool annotation; AWS Fargate runs on Firecracker micro-VMs. The trade-off is measurable I/O overhead for gVisor, slightly higher memory overhead for Kata, and some syscall compatibility edge cases. This is the right floor when you run untrusted code from customers.

Step 3: virtual clusters

vCluster runs a full Kubernetes control plane per tenant (its own API server, its own controller manager, its own etcd or SQLite) inside a namespace of a host cluster. Tenants get their own API server, their own CRDs, their own cluster-scoped objects, and their own kubectl config endpoint. Worker nodes are still shared. It is the middle ground I reach for most often when tenants genuinely need their own CRDs and cluster-scoped objects but cost pressure rules out N separate clusters. You still want sandboxed runtimes underneath for genuinely untrusted code, because vCluster isolates the control plane while the kernel stays shared.

Step 4: separate clusters

Still the strongest boundary. Costs have shifted: EKS and GKE charge $0.10 per hour per cluster (roughly $73 per month) for the standard control plane, and AKS charges nothing at all for the control plane. For 20 tenants on AKS you are paying for nodes and nothing else. For 50 tenants on EKS you are paying $3,650 per month in control-plane fees alone, before a single pod runs. The economics genuinely depend on your cloud, so do the math before assuming separate clusters are out of reach.

A hybrid is usually the right answer. 80% of tenants sit in one hygiene-tier cluster, and the 20% with compliance or trust requirements get their own vCluster or their own full cluster. If you are running WordPress on Kubernetes for many customer sites, you are already making this trade-off; the principles apply the same way.

Key takeaways

  • "Multi-tenancy" is a trust model, not a security property. Decide your tier before you write any YAML.
  • Namespaces, RBAC, ResourceQuota and LimitRange are hygiene. The Kubernetes docs call namespace boundaries weak, and they mean it.
  • The control-plane noisy-neighbor problem is real and solved by API Priority and Fairness, stable since Kubernetes 1.29. Configure per-tenant FlowSchema once you have more than a handful of tenants.
  • IngressNightmare and the retirement of ingress-nginx are the clearest warning shots against treating shared cluster-scoped resources as tenant boundaries. Audit your shared ingress, CRD, and webhook surface accordingly.
  • For untrusted tenants, reach for sandboxed runtimes, vCluster, or separate clusters. More admission policies will not close the gap on their own.

Recurring server or deployment issues?

I help teams make production reliable with CI/CD, Kubernetes, and cloud—so fixes stick and deploys stop being stressful.

Explore DevOps consultancy

Search this site

Start typing to search, or browse the knowledge base and blog.