click the screen · press Enter
← back to blog
Kubernetes Journey · Topic 34

A Namespace Is a Label, Not a Wall

Kubernetes multi-tenancy: multiple tenants sharing one cluster kept apart by namespaces, RBAC, resource quotas and network policies, with a shared host kernel underneath - Kubernetes Journey

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.

Soft versus hard Kubernetes multi-tenancy: soft uses namespaces, RBAC, quotas and network policies for trusted teams, while hard adds sandboxed runtimes and separate node pools for untrusted customers sharing a cluster
The controls on the left are most of what people mean by multi-tenancy. The right is what you add when the tenants aren't friendly.

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 office building, and where the analogy breaks A cluster is a building, each tenant gets a floor (a namespace), and the lifts, car park and wiring are shared (nodes, network, kernel). It's a decent picture with one flaw worth stating: floors in a real building have solid concrete between them. Namespaces don't. A namespace is closer to a floor marked out with tape on an open-plan level, everyone can still walk across, until you build the walls yourself.

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.

A TENANT BOUNDARY
Namespace: the name
RBAC: who gets in
ResourceQuota: how much
NetworkPolicy: who they reach
Runtime / nodes: how hard the wall is
Five things. Only the first is free with the namespace. The other four you have to add, and skipping any one leaves a real gap.

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:

kubectl auth can-i --list
$ kubectl auth can-i list pods -n tenant-green --as-group=blue-team --as=alice no $ kubectl auth can-i list pods -n tenant-blue --as-group=blue-team --as=alice yes
The one check that matters: blue team can act in blue, and cannot even see green.
Two RBAC traps specific to tenancy First, a 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"
The quota has a sharp edge, so pair it with a LimitRange Once a ResourceQuota sets 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 }
kubectl describe resourcequota blue-quota -n tenant-blue
Name: blue-quota Resource Used Hard requests.cpu 1200m 4 requests.memory 2Gi 8Gi pods 6 20 == blue can grow to its cap and no further ==
Used versus Hard, per namespace. When Used hits Hard, the next pod is refused, and green never notices blue exists.

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.

NetworkPolicy needs a CNI that enforces it This one bites in labs. A NetworkPolicy is just an object in the API unless your network plugin actually implements it. Calico, Cilium and a few others do. The stock 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.

Two threat models, two answers
 Soft multi-tenancyHard multi-tenancy
Tenants areInternal teams, mostly trustedExternal customers, assume hostile
Threat you're stoppingMistakes and noisy neighboursDeliberate escape and data theft
ControlsNamespace, RBAC, quota, NetworkPolicyAll that, plus a harder boundary
Shared kernel isAn acceptable riskThe main risk to remove
Same cluster tools underneath. The difference is whether you trust the tenants not to attack the kernel they share.

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.

The distinction that saves you money and grief Don't build hard multi-tenancy for internal teams who trust each other, you'll pay overhead you don't need. Don't run untrusted customer code with only soft controls, because a namespace was never a security boundary against a determined attacker sharing your kernel. Match the walls to the threat, and be honest about which one you actually have.

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:

Who are the tenants
 Multi-teamMulti-customer
WhoInternal departments and projectsExternal paying customers (SaaS)
Cluster accessDirect, via kubectl or GitOpsNone, the cluster is behind the product
Trust levelReasonable, shared employerZero assumed, plus regulators watching
Usually needsSoft multi-tenancyHard multi-tenancy
Multi-team maps to soft, multi-customer to hard. Not a law, but a good default to start from.

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

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.