Multi-Tenancy¶
What You'll Learn¶
- How to isolate tenants inside a shared cluster using namespaces,
ResourceQuota, andLimitRange - How
NetworkPolicyand RBAC scoping close the gaps namespaces alone leave open - The real trade-off between soft multi-tenancy in one cluster, separate clusters per tenant, and virtual clusters (vcluster)
Why This Matters¶
"Just give every team a namespace" is the default answer, and it's usually right — until one team's runaway job starves another team's production workload of CPU, or a misconfigured Service accidentally becomes reachable from a namespace it has no business talking to. Multi-tenancy is the set of guardrails that makes "shared cluster" not mean "shared blast radius."
Mental Model¶
A Kubernetes namespace is a scoping boundary for names and some policies — it is not, by itself, a security or resource-isolation boundary. Two pods in different namespaces on the same node still share the same kernel, the same node's CPU and memory, and can reach each other over the network by default. Everything in this chapter is about actively closing those gaps, because namespaces alone don't.
ResourceQuota: capping what a tenant can consume¶
apiVersion: v1
kind: ResourceQuota
metadata:
name: team-checkout-quota
namespace: team-checkout
spec:
hard:
requests.cpu: "20"
requests.memory: 40Gi
limits.cpu: "40"
limits.memory: 80Gi
pods: "50"
persistentvolumeclaims: "10"
services.loadbalancers: "2"
A ResourceQuota caps the sum of requests/limits across every pod in the namespace, plus counts of objects like PVCs and LoadBalancer Services (which cost real money from a cloud provider). Once a namespace hits its quota, new pod creation is rejected at admission time — the tenant gets an immediate, clear error instead of silently starving other tenants of cluster capacity.
LimitRange: setting sane per-pod defaults¶
apiVersion: v1
kind: LimitRange
metadata:
name: team-checkout-limits
namespace: team-checkout
spec:
limits:
- type: Container
default:
cpu: 500m
memory: 512Mi
defaultRequest:
cpu: 100m
memory: 128Mi
max:
cpu: "4"
memory: 4Gi
min:
cpu: 50m
memory: 64Mi
ResourceQuota caps the namespace total; LimitRange caps and defaults individual containers within it. Without a LimitRange, a single pod with no resource requests set can consume the entire namespace's quota by itself, or — worse — consume unbounded node resources if the namespace has no quota at all.
NetworkPolicy: closing the default-open network¶
By default, every pod in a cluster can reach every other pod, across namespaces, unless a CNI plugin that enforces NetworkPolicy is installed and policies are defined. Isolating a tenant's namespace means writing a default-deny policy and then explicitly allowing what's needed:
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: default-deny-ingress
namespace: team-checkout
spec:
podSelector: {}
policyTypes:
- Ingress
---
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: allow-from-ingress-controller-and-same-namespace
namespace: team-checkout
spec:
podSelector: {}
policyTypes:
- Ingress
ingress:
- from:
- namespaceSelector:
matchLabels:
kubernetes.io/metadata.name: ingress-nginx
- podSelector: {} # allow same-namespace traffic
This pair denies all inbound traffic to team-checkout by default, then re-opens exactly two paths: traffic from the ingress controller's namespace, and traffic between pods within the same namespace. Every other namespace is now unreachable from team-checkout unless another policy explicitly allows it.
RBAC: scoping who can do what, per namespace¶
apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata:
name: team-checkout-edit
namespace: team-checkout
subjects:
- kind: Group
name: team-checkout-engineers
apiGroup: rbac.authorization.k8s.io
roleRef:
kind: ClusterRole
name: edit # built-in ClusterRole, bound namespace-locally via RoleBinding
apiGroup: rbac.authorization.k8s.io
Binding the built-in edit ClusterRole via a namespace-scoped RoleBinding (rather than a ClusterRoleBinding) gives the team full CRUD on most namespaced resources inside their own namespace only — they can't list, read, or modify anything in another team's namespace, and they can't touch cluster-scoped resources like Node or ClusterRole objects at all.
Namespaces vs. Separate Clusters vs. Virtual Clusters¶
flowchart LR
A["Namespaces + quotas + NetworkPolicy + RBAC<br/>(soft multi-tenancy)"] --> B["vcluster<br/>(virtual control plane per tenant)"]
B --> C["Separate physical clusters<br/>(hard multi-tenancy)"]
| Approach | Isolation strength | Operational cost | Good fit when |
|---|---|---|---|
| Namespaces + quotas + NetworkPolicy + RBAC | Soft — shared kernel, shared control plane, shared node pool | Lowest — one cluster to upgrade, monitor, back up | Trusted internal teams, cost efficiency matters, workloads aren't adversarial toward each other |
| Virtual clusters (vcluster) | Medium — each tenant gets its own API server and control-plane objects, syncing down into a shared underlying cluster | Moderate — more control planes to reason about, but still one physical cluster's worth of nodes | Tenants need their own CRDs, RBAC root, or even Kubernetes version, without the cost of dedicated nodes |
| Separate physical clusters | Strong — separate control plane, separate etcd, often separate node pools entirely | Highest — N clusters to upgrade, back up, patch, and monitor | Regulatory separation requirements, genuinely untrusted or adversarial tenants, or wildly different availability/compliance needs per tenant |
The honest trade-off: namespace-based multi-tenancy is cheap and usually sufficient, but a determined or careless tenant can still exhaust shared cluster-level resources (the API server itself, image pull bandwidth, DNS) that quotas don't fully bound. Reach for vcluster or separate clusters only once you've identified a specific isolation gap namespaces can't close — not by default, because "more clusters" is a genuinely larger ongoing operational burden.
Common Mistakes¶
- Creating a namespace per tenant with no
ResourceQuotaat all, so "multi-tenant" provides zero actual resource isolation. - Setting a
ResourceQuotabut noLimitRange, letting a single unbounded pod claim the entire namespace's quota. - Assuming namespaces block network traffic by default — they don't; without a CNI enforcing
NetworkPolicyand explicit policies, every pod can reach every other pod cluster-wide. - Granting
ClusterRoleBindings (cluster-wide) instead of namespace-scopedRoleBindings "to save time," which quietly defeats the whole point of tenant isolation. - Reaching for separate clusters per tenant by default, before establishing that namespace-based isolation is actually insufficient for the threat model.
Interview Questions¶
- What does a Kubernetes namespace actually isolate, and what does it not isolate by default?
- How do
ResourceQuotaandLimitRangework together, and what happens if you set one without the other? - Walk through the
NetworkPolicyrules needed to fully isolate one tenant's namespace from another's. - When would you choose vcluster or separate physical clusters over namespace-based multi-tenancy?
See Interview Prep for full answers.
Next¶
Continue to CI/CD and GitOps to see how deployments get onto these tenant-isolated namespaces safely and repeatably.