
Give a team a namespace and tell them it's theirs. Feels like a wall. It isn't. By default, a pod in your namespace can open a network connection to a pod in mine, and unless someone set a quota it can also eat every core on the node we're both scheduled to. The name is separate. Almost nothing else is, until you make it so.
That's the whole subject of this post: how you actually keep tenants apart on one shared cluster, which controls do what, and the honest line past which "namespaces and RBAC" stops being enough and you need something heavier.
What multi-tenancy actually is
Multi-tenancy is running workloads for more than one group, teams inside your company or external customers, on the same cluster, without letting them see, starve or reach each other.
You give each tenant a boundary and then stack controls on that boundary. The boundary is almost always a namespace. The controls are the interesting part, and there are four of them that matter.
The alternative is a cluster per tenant, and that sounds safe right up until you have forty of them. Forty control planes to patch, forty sets of upgrades, forty bills. Beyond a handful of tenants, a cluster each is the expensive mistake, which is why the Kubernetes community treats it as an anti-pattern for scale rather than a best practice.
So: one cluster, many tenants, held apart by layered controls rather than by the namespace on its own.
The namespace is a label, not a barrier
This is the misconception to kill first, because everything else is built on getting it right. A namespace groups resources and gives them a name to live under. That is genuinely all it does on its own.
kubectl create namespace tenant-blue
kubectl create namespace tenant-green
Right now those two namespaces provide organisation and nothing else. No access control, no resource limits, no network separation. A user pointed at tenant-blue can still list tenant-green if their RBAC allows it, a tenant-blue pod can still connect to a tenant-green pod, and either one can still grab all the memory on the node. The namespace is the coat hook. The isolation is four separate things you hang on it.
RBAC: who can touch the namespace
The first wall. RBAC decides which identities can do what, and scoping a Role to a namespace is how you keep a tenant inside their own floor. A Role lives in one namespace and a RoleBinding grants it to a user or service account there:
apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
namespace: tenant-blue
name: tenant-admin
rules:
- apiGroups: ["", "apps"]
resources: ["pods", "deployments", "services", "configmaps"]
verbs: ["get", "list", "watch", "create", "update", "delete"]
---
apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata:
namespace: tenant-blue
name: blue-team-admin
subjects:
- kind: Group
name: blue-team
apiGroup: rbac.authorization.k8s.io
roleRef:
kind: Role
name: tenant-admin
apiGroup: rbac.authorization.k8s.io
Blue team now has full control inside tenant-blue and zero visibility into anything else. Verify it the way I'd verify any RBAC claim, by impersonating the identity rather than trusting the YAML:
ClusterRoleBinding ignores namespaces entirely, so one accidental cluster-wide binding hands a tenant the run of every namespace. For tenants you want RoleBinding, in the namespace, every time. Second, some resources are cluster-scoped and live outside any namespace, nodes, PersistentVolumes, and crucially other namespaces themselves, so never grant a tenant verbs on those unless you mean it.ResourceQuota: stop one tenant eating the cluster
RBAC keeps a tenant out of other namespaces. It does nothing about a tenant that stays politely in its own namespace and requests two hundred cores. That's the noisy neighbour: one tenant degrading everyone else by hogging shared compute. The fix is a ResourceQuota, which caps a namespace's total consumption:
apiVersion: v1
kind: ResourceQuota
metadata:
namespace: tenant-blue
name: blue-quota
spec:
hard:
requests.cpu: "4"
requests.memory: 8Gi
limits.cpu: "8"
limits.memory: 16Gi
pods: "20"
persistentvolumeclaims: "5"
requests.cpu or limits.memory, Kubernetes refuses any pod in that namespace that doesn't specify its own requests and limits. A tenant deploying a bare pod suddenly gets rejected with a confusing error. A LimitRange fixes it by giving the namespace sensible per-pod defaults, so pods that don't ask for resources inherit a reasonable amount instead of being turned away. Ship the two together or the quota looks broken on day one.apiVersion: v1
kind: LimitRange
metadata:
namespace: tenant-blue
name: blue-limits
spec:
limits:
- type: Container
default: { cpu: "500m", memory: 512Mi }
defaultRequest: { cpu: "100m", memory: 128Mi }
NetworkPolicy: stop the tenants talking
Here's the default that surprises people every time. In a fresh cluster, every pod can reach every other pod, across namespaces, with nothing in the way. Blue's pods can open a socket straight to green's database. Nothing stops them until you add a NetworkPolicy, and the first one to apply per namespace is a default-deny:
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
namespace: tenant-blue
name: default-deny-all
spec:
podSelector: {} # every pod in the namespace
policyTypes: ["Ingress", "Egress"]
# no ingress/egress rules below = deny everything
That switches tenant-blue from "anyone can reach it" to "nothing can", and then you add back only the traffic blue genuinely needs. The critical detail for tenancy is cross-namespace traffic: to let something in from another namespace you have to name it explicitly, which is exactly the control you want.
kubenet and some default setups silently ignore the policy, so it applies cleanly, shows up in kubectl get netpol, and blocks precisely nothing. Test it with a real cross-namespace connection, never assume the object alone is doing the work.The honest line: soft vs hard multi-tenancy
Now the part that actually decides your architecture, and the thing the four controls above quietly depend on. Everything so far, namespaces, RBAC, quotas, network policies, is enforced by the Kubernetes API and a shared Linux kernel. That works beautifully when your tenants are basically trustworthy. It's a different story when they're not.
| Soft multi-tenancy | Hard multi-tenancy | |
|---|---|---|
| Tenants are | Internal teams, mostly trusted | External customers, assume hostile |
| Threat you're stopping | Mistakes and noisy neighbours | Deliberate escape and data theft |
| Controls | Namespace, RBAC, quota, NetworkPolicy | All that, plus a harder boundary |
| Shared kernel is | An acceptable risk | The main risk to remove |
Soft multi-tenancy is the multi-team case: different departments or projects inside one organisation. They might make a mess by accident, they're not trying to break out and steal each other's data, and namespaces plus RBAC plus quotas plus network policy is a genuinely solid answer. This is most internal platform teams, and it's the right amount of control for that job.
Hard multi-tenancy is the multi-customer, SaaS case: you're running code that outside customers wrote, and you have to assume some of it is malicious. Here the shared kernel from the sandboxing topic stops being an abstract worry and becomes the actual attack surface, because a container escape doesn't just cross a namespace, it lands on a node running other customers' workloads. So hard multi-tenancy keeps all four soft controls and adds a real boundary on top: sandboxed runtimes like gVisor or Kata via RuntimeClass, dedicated node pools per tenant so a breakout has nowhere useful to land, and stricter data and network separation to meet the compliance rules external customers bring with them.
Multi-team vs multi-customer, quickly
The source material splits these out and it's a useful split, because it maps almost exactly onto soft vs hard:
| Multi-team | Multi-customer | |
|---|---|---|
| Who | Internal departments and projects | External paying customers (SaaS) |
| Cluster access | Direct, via kubectl or GitOps | None, the cluster is behind the product |
| Trust level | Reasonable, shared employer | Zero assumed, plus regulators watching |
| Usually needs | Soft multi-tenancy | Hard multi-tenancy |
What I check on a shared cluster
If I'm assessing a multi-tenant cluster on an authorised engagement, the isolation is exactly where I'd push, because it's usually half-built. Roughly in order:
# 1. do namespaces even have network isolation, or is it wide open?
kubectl get networkpolicy -A
# no policies in a tenant namespace = every pod can reach every pod
# 2. are there quotas, or can one tenant starve the rest?
kubectl get resourcequota -A
# 3. any cluster-wide bindings handing a "tenant" the whole cluster?
kubectl get clusterrolebindings -o wide | grep -v 'system:'
# 4. can a tenant identity see across the boundary?
kubectl auth can-i --list --as=system:serviceaccount:tenant-blue:default
# 5. is untrusted code running on the shared kernel with no sandbox?
kubectl get pods -A -o custom-columns=NS:.metadata.namespace,POD:.metadata.name,RC:.spec.runtimeClassName
The two that pay most often are one and five. A cluster sold internally as "multi-tenant" with no NetworkPolicy anywhere means the tenancy is organisational fiction, every pod can talk to every other pod. And a SaaS platform running customer workloads with an empty runtimeClassName column is doing hard multi-tenancy's job with soft multi-tenancy's tools, which is the finding I'd lead the report with.
The idea I wish I'd had sooner
The single idea I wish I'd internalised earlier: a namespace is not a security boundary. It's an organisational one. I used to half-believe that dropping a workload in its own namespace bought me isolation, and it buys you a name and a place to attach real controls, nothing more. The isolation is RBAC and quotas and network policy and, when it matters, a harder runtime. Miss one and you've got a gap shaped exactly like the thing you skipped.
My actual opinion on the architecture question: for internal teams, soft multi-tenancy on a shared cluster is the right answer almost always, and a cluster-per-team is usually someone avoiding the work of writing good RBAC. For external customers running their own code, I'd be nervous relying on soft controls alone, and I think the honest move is either sandboxed runtimes or, past a certain risk level, genuinely separate clusters or node pools. The tools reach a long way, but "namespace plus RBAC" against a hostile tenant sharing your kernel is trusting a paper wall.
Where I'm unsure: I've built soft multi-tenancy in a homelab and read a lot about how the big SaaS platforms do hard multi-tenancy, but I haven't run a hostile-tenant cluster at scale, so I can't tell you first-hand where the operational pain lives, whether it's the per-tenant node pools, the quota tuning, or the network policy sprawl. If you operate real multi-customer Kubernetes, I'd love to know which of those three actually keeps you busy.
That's a natural pause in the security series. Next I want to get back to something hands-on and offensive: what's actually inside a container image, and how much of your attack surface you inherited without choosing it.
References
- Kubernetes docs: multi-tenancy
- Kubernetes docs: resource quotas
- Kubernetes docs: limit ranges
- Kubernetes docs: network policies
- Kubernetes docs: RBAC good practices
FAQ
What is multi-tenancy in Kubernetes?
Multi-tenancy is running workloads for multiple teams or customers on one shared cluster while keeping them isolated. Instead of a cluster per tenant, each tenant gets a boundary, usually a namespace, plus RBAC, resource quotas and network policies so they cannot see, starve or reach each other.
Is a namespace enough to isolate tenants?
No. A namespace only groups and names resources. On its own it does not restrict access, cap resource use or block network traffic between tenants. You have to add RBAC, a ResourceQuota and NetworkPolicy on top, and even then tenants still share one host kernel.
What is the difference between soft and hard multi-tenancy?
Soft multi-tenancy assumes tenants are trusted-ish, like internal teams, and separates them with namespaces, RBAC and quotas. Hard multi-tenancy assumes tenants are hostile, like external customers running arbitrary code, and adds sandboxed runtimes or separate node pools because the shared kernel is a real risk.
What is a noisy neighbour in Kubernetes?
A noisy neighbour is one tenant consuming so much CPU, memory or storage that other tenants on the same nodes suffer. ResourceQuota caps a namespace's total, and LimitRange sets per-pod defaults, so a single tenant cannot starve the cluster by accident or on purpose.
Should I give each customer their own cluster instead?
A cluster per tenant gives the strongest isolation but the cost and operational load grow fast as tenants multiply, which is why it is usually an anti-pattern at scale. Most setups share a cluster with strong in-cluster isolation, and reserve separate clusters for the few tenants that genuinely require it.
Related reading
- Topic 9: Kubernetes Authorization and RBAC (the first wall between tenants)
- Topic 14: Network Policies (stopping tenants from talking)
- Topic 32: Container Sandboxing (the harder boundary hard multi-tenancy needs)
- Topic 33: One-Way vs Mutual TLS (how tenants' services prove who they are)
- Browse the whole Kubernetes Journey