Kubernetes Multi-Tenant Security: How to Isolate Tenants in a Shared Cluster
A practical, engineering-grade guide to isolating tenants inside a shared Kubernetes cluster — namespaces, RBAC, NetworkPolicy, admission control, and the failure modes that break each layer.
Kubernetes Multi-Tenant Security: How to Isolate Tenants in a Shared Cluster
A practical, engineering-grade guide to isolating tenants inside a shared Kubernetes cluster — namespaces, RBAC, NetworkPolicy, admission control, and the failure modes that break each layer.
A shared Kubernetes cluster is not automatically a multi-tenant Kubernetes cluster. Kubernetes was designed around a single trust boundary: one API server, one etcd, one set of nodes, one scheduler making placement decisions across every workload it sees. When you put multiple tenants — different teams, different customers, or different business units — into that same trust boundary, every default Kubernetes behavior that assumes "all workloads here are mine" becomes a liability.
This matters because the failure mode is not theoretical. A namespace without a NetworkPolicy allows any pod in the cluster to open a connection to any pod in that namespace. A ServiceAccount token mounted by default gives a compromised pod a live credential to the Kubernetes API. A shared node without pod security controls lets one tenant's container escape into the kernel that every other tenant's container also runs on. None of these require a sophisticated attacker — they are just what Kubernetes does when nobody tells it otherwise.
Who should care about this: platform engineers building an internal developer platform for multiple teams, SaaS companies running customer workloads in a shared cluster to control cost, and anyone running a cluster where "trusted" and "untrusted" code will eventually sit next to each other. By the end of this article you will understand the three tenancy models Kubernetes supports, how to implement namespace-level isolation with RBAC and NetworkPolicy, what admission control and Pod Security Standards add on top, where the isolation boundary actually breaks, and how to verify — not assume — that isolation holds.
KEY TAKEAWAYS
- Namespaces provide an administrative boundary, not a security boundary — RBAC, NetworkPolicy, and Pod Security Standards have to be layered on top explicitly.
- The three tenancy models — soft, hard (namespace-based), and hard (cluster-based) — trade operational cost against isolation strength. Choose the model based on your actual trust level, not convenience.
- Default-deny NetworkPolicy plus least-privilege RBAC closes the two most commonly exploited gaps in multi-tenant clusters: lateral network access and API server access via a mounted ServiceAccount token.
- The shared kernel is the real trust boundary. If tenants are mutually untrusted, namespace isolation alone is insufficient — you need runtime sandboxing (gVisor/Kata) or dedicated node pools.
- Isolation claims must be tested adversarially. "We applied a NetworkPolicy" is not proof of isolation; a failed connection attempt from another tenant's pod is proof.
Understanding Kubernetes Tenancy Models
Before writing a single NetworkPolicy, decide which tenancy model actually matches your trust level. The CNCF and the Kubernetes documentation describe multi-tenancy along a spectrum, and picking the wrong point on that spectrum is the root cause of most "we thought we were isolated" incidents.
Soft multi-tenancy
Tenants are internal teams that trust each other and trust the platform team, but you still want blast-radius control, quota enforcement, and accidental-interference prevention. Namespaces plus RBAC plus resource quotas are usually sufficient here. The threat model is misconfiguration and accidents, not malicious tenants.
Hard multi-tenancy (namespace-based)
Tenants do not trust each other. This is the SaaS case: customer A's workload and customer B's workload run in the same cluster, and a compromise of A's workload must not lead to access to B's data or workload. Namespace-based hard multi-tenancy layers RBAC, NetworkPolicy, Pod Security Standards, admission control, and often per-tenant nodes or runtime sandboxing on top of namespace boundaries. This is the model this article focuses on, because it is the most common production requirement and the hardest to get right with defaults alone.
Hard multi-tenancy (cluster-based)
Each tenant gets a dedicated cluster, sometimes via vcluster or a managed control plane per tenant. This gives the strongest isolation — a separate API server and etcd per tenant — at the cost of significantly higher operational overhead: more control planes to patch, monitor, and pay for. Organizations move here when regulatory requirements or a security incident push the risk tolerance of namespace-based isolation below what's acceptable.
A useful rule of thumb: if you would be comfortable putting tenant A and tenant B's workloads on the same Linux VM with only Linux users and iptables separating them, namespace-based hard multi-tenancy with the controls in this article is probably enough. If you would not be comfortable with that, you need dedicated nodes at minimum, and likely dedicated clusters.
Why Kubernetes' Default Networking Is Insufficient for Tenant Isolation
What is a NetworkPolicy?
A NetworkPolicy is a Kubernetes object that constrains which pods can communicate with which other pods, and on which ports, using label selectors instead of IP addresses. It is enforced by the CNI plugin, not by the Kubernetes API server itself — Kubernetes only stores the intent.
How Kubernetes networking handles traffic by default
The Kubernetes networking model requires that every pod can reach every other pod's IP address directly, without NAT, regardless of which node or namespace they are in. This flat network model is what makes Services, DNS discovery, and cross-namespace communication work out of the box. It is also, by itself, a complete absence of isolation: a pod in the "tenant-a" namespace can open a TCP connection to a pod in "tenant-b" namespace with nothing more than that pod's ClusterIP or pod IP.
Why default networking is insufficient for tenant isolation
Without a default-deny NetworkPolicy, a compromised workload may be able to establish connections to services that should never be reachable from its namespace. This is not a hypothetical — it is the literal default behavior of every CNI plugin that implements the base Kubernetes networking model. A tenant does not need a privilege escalation exploit to reach another tenant's internal API; they only need the target's ClusterIP, which is discoverable via DNS in clusters that don't restrict CoreDNS queries across namespaces.
How NetworkPolicy changes the model
Applying a default-deny NetworkPolicy in every tenant namespace flips the model from allow-all to deny-all, and then explicit policies re-open only the traffic that is required — typically: ingress from an ingress controller namespace, egress to DNS, and egress to specific dependencies. Traffic that is not explicitly allowed is dropped by the CNI plugin at the point of connection attempt, not logged and reported after the fact.
Implementation
Covered in full in the NetworkPolicy section below, with tested manifests.
Reference Architecture for Namespace-Based Multi-Tenancy
The architecture below assumes hard multi-tenancy at the namespace level: one namespace per tenant, a shared ingress layer, a shared observability stack, and per-tenant network and RBAC boundaries.
flowchart TD
Internet -->|HTTPS| Ingress[Ingress Controller
namespace: ingress-system]
Ingress -->|routed by Host header| TenantA_Svc[Service: tenant-a]
Ingress -->|routed by Host header| TenantB_Svc[Service: tenant-b]
subgraph NS_A[Namespace: tenant-a]
TenantA_Svc --> TenantA_Pod[App Pods]
TenantA_Pod --> TenantA_DB[(Database)]
end
subgraph NS_B[Namespace: tenant-b]
TenantB_Svc --> TenantB_Pod[App Pods]
TenantB_Pod --> TenantB_DB[(Database)]
end
NP1[NetworkPolicy: default-deny + allow-from-ingress] -.enforces isolation.-> NS_A
NP2[NetworkPolicy: default-deny + allow-from-ingress] -.enforces isolation.-> NS_B
RBAC[RBAC: RoleBindings scoped per namespace] -.controls API access.-> NS_A
RBAC -.controls API access.-> NS_B
PSA[Pod Security Admission: restricted profile] -.enforces at admission.-> NS_A
PSA -.enforces at admission.-> NS_B
What is happening here? Traffic enters through a single shared ingress controller, which is the only component allowed to route into either tenant namespace. Once inside a tenant namespace, a default-deny NetworkPolicy blocks all traffic except what's explicitly allowed (ingress from the ingress controller, egress to DNS and the tenant's own database). RBAC RoleBindings scope every ServiceAccount and human identity to a single namespace so a credential leaked from tenant-a cannot list, read, or modify anything in tenant-b. Pod Security Admission enforces the restricted profile at the moment a pod is created, rejecting privileged containers, host namespace access, and root execution before they ever start.
Isolating Tenants with RBAC
RBAC in Kubernetes controls who can do what to which resources, scoped by namespace via RoleBindings or cluster-wide via ClusterRoleBindings. The single most common multi-tenancy mistake is binding a ClusterRole with a ClusterRoleBinding when a namespaced RoleBinding would have been correct — this silently grants access across every tenant namespace in the cluster.
apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
name: tenant-developer
namespace: tenant-a
rules:
- apiGroups: ["", "apps"]
resources: ["pods", "deployments", "services", "configmaps"]
verbs: ["get", "list", "watch", "create", "update", "patch", "delete"]
- apiGroups: [""]
resources: ["secrets"]
verbs: ["get", "list"] # no create/update: secrets are managed by the platform team
---
apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata:
name: tenant-a-developers
namespace: tenant-a
subjects:
- kind: Group
name: "tenant-a-devs"
apiGroup: rbac.authorization.k8s.io
roleRef:
kind: Role
name: tenant-developer
apiGroup: rbac.authorization.k8s.io
What does this configuration do? It creates a Role scoped to the tenant-a namespace only — the Role object itself is namespaced, so it has no effect outside it — and binds it to a group of human users via a RoleBinding, also namespaced. Developers in tenant-a can manage their own workloads but only read Secrets, not create or update them.
Why is it secure? Because both the Role and the RoleBinding are namespace-scoped objects, there is no API path by which this binding grants any permission in tenant-b. Restricting Secrets to read-only for developers reduces the blast radius if a developer account is phished — they can't plant a malicious Secret or exfiltrate by overwriting one.
What should we verify? Run kubectl auth can-i --as=user get pods -n tenant-b and confirm it returns no. Also audit for any ClusterRoleBindings that reference tenant groups — those are the actual source of most cross-tenant RBAC leaks in real clusters.
Every default ServiceAccount automatically gets a token mounted into pods unless automountServiceAccountToken: false is set. That token is a live Kubernetes API credential with whatever permissions the default ServiceAccount's RoleBindings grant — often more than the workload itself needs. Disable auto-mounting for workloads that never call the Kubernetes API.
Isolating Tenants with NetworkPolicy
NetworkPolicy requires a CNI plugin that implements it — Calico, Cilium, or Azure CNI in policy mode all work; the default kubenet on some managed clusters does not enforce NetworkPolicy at all, which means the manifests below would apply with no effect. Verify enforcement before relying on this layer.
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: default-deny-all
namespace: tenant-a
spec:
podSelector: {} # applies to every pod in this namespace
policyTypes:
- Ingress
- Egress
---
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: allow-dns-egress
namespace: tenant-a
spec:
podSelector: {}
policyTypes:
- Egress
egress:
- to:
- namespaceSelector:
matchLabels:
kubernetes.io/metadata.name: kube-system
ports:
- protocol: UDP
port: 53
- protocol: TCP
port: 53
---
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: allow-ingress-from-ingress-controller
namespace: tenant-a
spec:
podSelector:
matchLabels:
app: tenant-a-web
policyTypes:
- Ingress
ingress:
- from:
- namespaceSelector:
matchLabels:
kubernetes.io/metadata.name: ingress-system
ports:
- protocol: TCP
port: 8080
What does this configuration do? The first policy sets a default-deny baseline for both ingress and egress on every pod in the namespace — an empty podSelector matches all pods. The second policy re-opens the one thing every pod needs regardless of tenant: DNS resolution against CoreDNS in kube-system. The third policy allows inbound traffic to the tenant's web pods, but only from pods in the ingress-system namespace, on the specific port the app listens on.
Why is it secure? Nothing is reachable by default. Every additional path has to be explicitly justified and scoped to a namespace, a label, and a port — there is no rule that says "allow from anywhere" or "allow all ports."
What should we verify? Exec into a pod in tenant-b and attempt curl against a tenant-a pod's ClusterIP on the app port. It must time out. Then verify the legitimate path — the ingress controller — still reaches the app successfully. Both checks matter: a policy that blocks everything including legitimate traffic is a functional bug disguised as security.
NetworkPolicy operates at L3/L4 (IP and port). It cannot inspect HTTP paths, distinguish between two services on the same port, or stop DNS-based data exfiltration to external domains unless egress is also restricted to specific destinations. For L7 controls (mTLS, path-based authorization between tenants), a service mesh like Istio or Linkerd sits on top of, not instead of, NetworkPolicy.
Pod Security Standards and Admission Control
Pod Security Admission is a built-in Kubernetes admission controller that enforces one of three predefined profiles — privileged, baseline, or restricted — at the namespace level via labels. It replaced the deprecated PodSecurityPolicy resource.
apiVersion: v1
kind: Namespace
metadata:
name: tenant-a
labels:
pod-security.kubernetes.io/enforce: restricted
pod-security.kubernetes.io/audit: restricted
pod-security.kubernetes.io/warn: restricted
What does this configuration do? Every pod creation request in tenant-a is checked against the restricted profile — which requires running as non-root, forbids privilege escalation, drops all Linux capabilities by default, and disallows host namespaces and host path volumes — and rejected at admission if it fails.
Why is it secure? It stops a whole category of container-breakout techniques before the container ever starts, independent of what the application code does at runtime. A tenant cannot simply set privileged: true in their own Deployment spec and get away with it.
What should we verify? Attempt to deploy a pod with privileged: true or hostNetwork: true into the namespace and confirm the API server rejects it with a Pod Security admission error, not just a warning.
For requirements beyond what the built-in profiles cover — for example, restricting which container registries a tenant may pull from, or requiring specific labels on every resource — an admission controller like Kyverno or OPA Gatekeeper enforces custom policy as code, evaluated the same way at every API request.
Resource Isolation: Quotas and LimitRanges
Network and identity isolation don't prevent a noisy-neighbor problem: one tenant's workload consuming enough CPU or memory to starve everyone else on the same node. ResourceQuota caps aggregate consumption per namespace; LimitRange sets defaults and bounds per container so a tenant can't omit limits entirely.
apiVersion: v1
kind: ResourceQuota
metadata:
name: tenant-a-quota
namespace: tenant-a
spec:
hard:
requests.cpu: "8"
requests.memory: 16Gi
limits.cpu: "16"
limits.memory: 32Gi
pods: "50"
persistentvolumeclaims: "10"
---
apiVersion: v1
kind: LimitRange
metadata:
name: tenant-a-limits
namespace: tenant-a
spec:
limits:
- type: Container
default:
cpu: 500m
memory: 512Mi
defaultRequest:
cpu: 250m
memory: 256Mi
max:
cpu: "2"
memory: 2Gi
What does this configuration do? The ResourceQuota caps total CPU, memory, pod count, and PVC count for the whole namespace. The LimitRange fills in defaults for containers that don't specify requests/limits, and caps how large a single container's limits can be, preventing one pod from consuming the entire quota.
Why is it secure? Resource exhaustion is a legitimate denial-of-service vector in shared clusters, and it doesn't require any exploit — just a misconfigured or malicious Deployment with no limits set. Quotas make that impossible to do by accident and expensive to do deliberately.
What should we verify? Attempt to create a Deployment requesting more CPU than the quota allows and confirm the API server rejects it with a quota-exceeded error.
Lab: Implementing a Two-Tenant Isolated Namespace Model
Prerequisites
- Operating system: any Linux, macOS, or WSL2 host able to run a local cluster
- Software: kubectl v1.29+, a local cluster tool such as kind v0.23+ or minikube v1.33+
- CNI: kind's default (kindnetd) does not enforce NetworkPolicy — install Calico or use a kind config with Cilium for this lab to work as described
- Resources: 4 vCPU / 8GB RAM available to the local cluster
Step 1 — Prepare the environment
# create a kind cluster without the default CNI so we can install Calico
cat <
Step 2 — Deploy the infrastructure
kubectl create namespace tenant-a
kubectl create namespace tenant-b
kubectl label namespace tenant-a pod-security.kubernetes.io/enforce=restricted
kubectl label namespace tenant-b pod-security.kubernetes.io/enforce=restricted
kubectl -n tenant-a create deployment web --image=nginx:1.27-alpine
kubectl -n tenant-a expose deployment web --port=80
kubectl -n tenant-b create deployment web --image=nginx:1.27-alpine
kubectl -n tenant-b expose deployment web --port=80
Step 3 — Configure security
# apply the default-deny and DNS-allow policies from the NetworkPolicy section
# to both namespaces (repeat with -n tenant-b)
kubectl apply -n tenant-a -f default-deny-all.yaml
kubectl apply -n tenant-a -f allow-dns-egress.yaml
kubectl apply -n tenant-b -f default-deny-all.yaml
kubectl apply -n tenant-b -f allow-dns-egress.yaml
Step 4 — Test the implementation
kubectl -n tenant-a run tester --image=busybox:1.36 --restart=Never -- sleep 3600
kubectl -n tenant-a wait --for=condition=Ready pod/tester --timeout=60s
TENANT_B_IP=$(kubectl -n tenant-b get svc web -o jsonpath='{.spec.clusterIP}')
kubectl -n tenant-a exec tester -- wget -T 5 -O- "http://${TENANT_B_IP}"
Step 5 — Verify the result
Expect the wget command to hang and then fail with a timeout — that failure is the evidence isolation is working, not an error to fix. Before the NetworkPolicy is applied, the same command should succeed and return the nginx welcome page; run the test both before and after applying the policy to see the actual difference it makes.
Step 6 — Test failure scenarios
# confirm DNS still resolves (the one thing we deliberately allowed)
kubectl -n tenant-a exec tester -- nslookup web.tenant-a.svc.cluster.local
# confirm the tenant's own service is still reachable once we add the
# allow-ingress-from-ingress-controller-equivalent rule scoped to same-namespace traffic
kubectl -n tenant-a exec tester -- wget -T 5 -O- "http://web.tenant-a.svc.cluster.local"
If DNS resolution fails, the egress-to-kube-system rule is misconfigured — check that the namespace selector label matches your CNI's actual label on kube-system (some clusters label it differently). If same-namespace traffic also fails, the default-deny policy has no matching allow rule for intra-namespace communication, which may or may not be intentional depending on whether your app tiers within a tenant need to talk to each other.
Security Analysis
Threat model
The primary adversary in a multi-tenant cluster is another tenant — via a compromised dependency in their own workload, a misconfigured application, or, in SaaS contexts, malicious intent. The secondary adversary is an external attacker who has compromised one tenant's application and is now attempting to pivot laterally or escalate to the shared infrastructure.
Attack surface
The attack surface spans: the Kubernetes API server (via any leaked or over-privileged ServiceAccount token), the pod network (via the flat networking model absent NetworkPolicy), the container runtime and kernel (via container breakout techniques), and the CI/CD pipeline that deploys into the cluster (via supply-chain compromise of a base image or dependency).
Authentication and authorization
Kubernetes authenticates identities (users, groups, ServiceAccounts) but delegates all authorization decisions to RBAC (or webhook authorizers). Every RoleBinding and ClusterRoleBinding in the cluster is part of the authorization surface — a single overly broad ClusterRoleBinding undermines every namespace boundary built on top of it.
Network isolation
Covered in depth above. The critical point for a security review: NetworkPolicy is enforced by the CNI, and a cluster running a CNI without NetworkPolicy support (or with it disabled) has zero network isolation regardless of how many NetworkPolicy manifests are applied.
Secrets
Kubernetes Secrets are base64-encoded, not encrypted, by default in etcd unless encryption at rest is explicitly configured. In a multi-tenant cluster, anyone with read access to etcd backups, or with the RBAC permission to read Secrets in a namespace they shouldn't have access to (see the RBAC section), can read tenant credentials in plaintext.
Privilege escalation and container security
Pod Security Standards' restricted profile blocks the most common escalation paths — privileged containers, host namespace sharing, arbitrary Linux capabilities. It does not eliminate every container-to-host escape; the residual risk is a kernel vulnerability exploitable even from a properly restricted container, which is why mutually distrustful tenants on the same kernel remains a real risk even with every control in this article applied.
Supply-chain and misconfiguration risks
Tenants pulling images from public registries introduce supply-chain risk into a shared cluster. An admission controller restricting allowed registries, combined with image signature verification, reduces this. Misconfiguration — a missing NetworkPolicy in a newly created tenant namespace, a ClusterRoleBinding added during an incident and never removed — is more common in practice than any exploit and should be caught by continuous policy validation, not a one-time review.
Logging, monitoring, detection, and incident response
Kubernetes audit logs record every API request, including who accessed what in which namespace — this is the primary detection surface for cross-tenant access attempts. Network flow logs from the CNI (Calico and Cilium both support this) show attempted connections that a NetworkPolicy blocked, which is direct evidence of either a misconfigured legitimate service or an active lateral-movement attempt.
Map these controls to OWASP Kubernetes Top Ten, the CIS Kubernetes Benchmark, and relevant techniques in MITRE ATT&CK for Containers when building a formal threat model for your cluster — this article covers the controls, not a full compliance mapping.
Before / After Comparison
BEFORE — flat trust, no isolation:
Cluster (single flat network, no default-deny)
|
+-- tenant-a namespace
| +-- web pods (reachable from anywhere in the cluster)
| +-- database (reachable from anywhere in the cluster)
|
+-- tenant-b namespace
+-- web pods (reachable from anywhere in the cluster)
+-- database (reachable from anywhere in the cluster)
RBAC: broad ClusterRoleBinding grants "view" across all namespaces
Pods: run as root, no Pod Security enforcement
AFTER — enforced tenant boundaries:
Cluster
|
+-- tenant-a namespace [pod-security: restricted]
| +-- default-deny NetworkPolicy
| +-- allow: ingress-system -> web:8080, egress -> kube-system:53
| +-- RoleBinding scoped to tenant-a only
|
+-- tenant-b namespace [pod-security: restricted]
+-- default-deny NetworkPolicy
+-- allow: ingress-system -> web:8080, egress -> kube-system:53
+-- RoleBinding scoped to tenant-b only
Cross-namespace ClusterRoleBindings: removed / audited
What changed: network reachability moved from allow-all to explicit-allow-only per namespace; RBAC moved from cluster-wide bindings to namespace-scoped bindings; and pod creation is now checked against a security profile at admission time instead of being unconstrained. None of these changes require a different cluster — they are policy applied on top of the same infrastructure.
Common Mistakes
1. Treating namespaces as a security boundary by themselves
What people do: create one namespace per tenant and consider the isolation work done. Why it's dangerous: namespaces are an administrative and naming boundary in the Kubernetes API — they add zero network or RBAC restriction on their own. How to fix it: pair every namespace with a default-deny NetworkPolicy and namespace-scoped RBAC from the moment the namespace is created, ideally automated via a namespace-provisioning pipeline rather than manual steps.
2. Binding ClusterRoles with ClusterRoleBindings for tenant users
What people do: grant a tenant team's group a convenient ClusterRoleBinding because it's one object instead of one RoleBinding per namespace. Why it's dangerous: it grants that permission in every current and future namespace in the cluster, including other tenants'. How to fix it: use RoleBindings referencing a ClusterRole (this is valid and namespaces the effect), never ClusterRoleBindings, for tenant-scoped access.
3. Forgetting egress rules and only restricting ingress
What people do: write a NetworkPolicy that restricts inbound traffic to a tenant's pods but leaves egress unrestricted. Why it's dangerous: a compromised pod in tenant-a can still reach out to tenant-b's services, exfiltrate data to the internet, or call internal cloud metadata endpoints. How to fix it: apply default-deny to both Ingress and Egress policyTypes, and explicitly allow only DNS and required destinations.
4. Assuming NetworkPolicy is enforced without checking the CNI
What people do: apply NetworkPolicy manifests and assume they work because kubectl apply succeeded. Why it's dangerous: kubectl apply succeeding only means the object was stored in etcd — the API server does not itself enforce NetworkPolicy, the CNI plugin does, and several CNIs (or CNI configurations) silently ignore NetworkPolicy objects entirely. How to fix it: run the adversarial connectivity test from the lab section — attempt a blocked connection and confirm it actually fails — on every cluster, not just once at setup.
5. Sharing a default ServiceAccount token across all pods in a namespace
What people do: leave the default ServiceAccount's automatic token mount enabled for every workload, including ones that never call the Kubernetes API. Why it's dangerous: a compromised pod inherits a live, valid Kubernetes API credential it doesn't need, giving an attacker with only container-level access a path to the control plane. How to fix it: set automountServiceAccountToken: false at the pod or ServiceAccount level for workloads with no legitimate need to call the API, and create narrowly scoped dedicated ServiceAccounts for the ones that do.
6. Running mutually untrusted tenants on the same node pool without runtime sandboxing
What people do: treat namespace + RBAC + NetworkPolicy as sufficient isolation for genuinely adversarial tenants (e.g., running arbitrary customer-submitted code). Why it's dangerous: all tenants still share the same Linux kernel; a kernel-level container escape bypasses every namespace-level control simultaneously. How to fix it: for genuinely untrusted workloads, use dedicated node pools per tenant, or a sandboxed runtime like gVisor or Kata Containers that adds a stronger isolation boundary below the container.
Production Considerations
| Aspect | Development | Production |
|---|---|---|
| Scalability | One shared small cluster is fine | Plan namespace sprawl; consider per-tenant resource quotas tied to billing tiers |
| High availability | Single control plane, single node acceptable | Multi-AZ control plane and node pools; a control-plane outage affects every tenant simultaneously |
| Performance | Noisy-neighbor effects tolerated | Enforce LimitRange and ResourceQuota strictly; consider CPU pinning for latency-sensitive tenants |
| Cost | Not a concern | Shared cluster reduces cost per tenant versus dedicated clusters — the core economic reason for namespace-based multi-tenancy |
| Security | Baseline Pod Security profile acceptable | Restricted Pod Security profile, admission policy-as-code (Kyverno/OPA), image provenance verification |
| Monitoring | Basic metrics | Per-tenant dashboards, NetworkPolicy drop-rate alerting, audit log analysis for cross-namespace access attempts |
| Disaster recovery / backup | Often skipped | Per-tenant backup scope and restore testing; a shared-cluster DR plan must not restore tenant-a data into tenant-b's namespace during recovery |
| Multi-region | Single region | Tenant data residency requirements may force per-region clusters even within a namespace-based model |
| Automation / IaC | Manual namespace creation acceptable | Namespace provisioning, RBAC, NetworkPolicy, and quota should be templated (Kustomize/Helm) and applied via GitOps so no tenant namespace is ever created without its full security baseline |
The single highest-leverage production change: never let a human create a tenant namespace manually. Every tenant onboarding should run through a pipeline that creates the namespace, the default-deny NetworkPolicy, the RoleBindings, the ResourceQuota, and the Pod Security labels atomically, so isolation is never a step someone forgot.
Security Checklist
- [ ] Every tenant has its own namespace, created via an automated pipeline, not manually
- [ ] Default-deny NetworkPolicy (Ingress + Egress) applied in every tenant namespace
- [ ] Explicit allow rules exist only for required paths (DNS, ingress controller, named dependencies)
- [ ] CNI plugin confirmed to enforce NetworkPolicy (tested, not assumed)
- [ ] RBAC uses namespaced RoleBindings for tenant access; no tenant-facing ClusterRoleBindings
- [ ] `pod-security.kubernetes.io/enforce: restricted` label set on every tenant namespace
- [ ] `automountServiceAccountToken: false` set for workloads that don't call the Kubernetes API
- [ ] ResourceQuota and LimitRange applied per tenant namespace
- [ ] Secrets encryption at rest enabled in etcd
- [ ] Audit logging enabled and reviewed for cross-namespace access attempts
- [ ] Adversarial cross-tenant connectivity test performed and re-run after any CNI or policy change
- [ ] Genuinely untrusted tenants scheduled on dedicated node pools or a sandboxed runtime, not shared nodes
FAQ
Is Kubernetes Namespace isolation enough for multi-tenancy?
No. A namespace is an API-level grouping mechanism with no inherent network, compute, or identity isolation. It becomes a security boundary only once RBAC, NetworkPolicy, Pod Security Standards, and resource quotas are applied on top of it.
Can NetworkPolicies completely isolate tenants?
They isolate at the network layer (L3/L4) and are a necessary control, but not sufficient alone. RBAC still governs API-level access, Pod Security Standards govern what a container can do on its node, and the underlying kernel is still shared — a container-breakout vulnerability bypasses NetworkPolicy entirely.
Do I need a service mesh for multi-tenant Kubernetes?
Not strictly. A service mesh adds L7 controls (mTLS between services, fine-grained authorization by HTTP path/method) that NetworkPolicy cannot express. It's valuable when tenants need cryptographic proof of peer identity or fine-grained API-level authorization between services, but namespace + RBAC + NetworkPolicy + Pod Security Standards is a complete baseline without one.
What's the difference between soft and hard multi-tenancy?
Soft multi-tenancy assumes tenants trust each other and the platform (typical for internal teams); hard multi-tenancy assumes tenants do not trust each other (typical for SaaS customers) and requires stronger enforced boundaries — RBAC, NetworkPolicy, Pod Security Standards, and often dedicated nodes — rather than relying on tenant goodwill.
Should I use dedicated clusters instead of shared namespaces?
Use dedicated clusters when regulatory requirements demand a separate control plane per tenant, when tenants are fully adversarial and a kernel-level compromise is unacceptable risk, or when the operational cost of managing many clusters is lower than the cost of a namespace-isolation failure for your business. Otherwise, namespace-based hard multi-tenancy with the controls in this article is the common production choice.
How do I test that my isolation actually works?
Deploy a throwaway pod in one tenant's namespace and attempt to reach another tenant's Service ClusterIP and the Kubernetes API server directly. A successful isolation setup produces connection timeouts and RBAC "Forbidden" errors respectively. Re-run this test after any change to the CNI, RBAC, or admission configuration.
Does Pod Security Admission replace PodSecurityPolicy?
Yes — PodSecurityPolicy was removed from Kubernetes as of v1.25. Pod Security Admission, using the predefined privileged/baseline/restricted profiles applied via namespace labels, is its built-in replacement; OPA Gatekeeper or Kyverno are used when requirements go beyond the built-in profiles.
Conclusion
Kubernetes gives you the primitives for multi-tenancy — namespaces, RBAC, NetworkPolicy, Pod Security Admission, ResourceQuota — but none of them are isolating by default, and none of them are optional if tenants don't fully trust each other. The most important lesson is that isolation is additive: each layer closes a specific gap the others don't cover, and skipping any one of them (especially egress NetworkPolicy or namespace-scoped RBAC) leaves a real, exploitable path between tenants.
What should be done next: audit your current cluster for ClusterRoleBindings that shouldn't exist, confirm your CNI actually enforces NetworkPolicy, and add the adversarial connectivity test from the lab section to your CI/CD pipeline so isolation failures are caught before they reach production. If your tenants are genuinely adversarial rather than merely separate, start evaluating dedicated node pools or a sandboxed runtime now, before an incident forces the decision.
The practical recommendation: build tenant namespace provisioning as a single automated, version-controlled pipeline that applies every control in the checklist atomically. Isolation that depends on a human remembering a manual step will eventually fail; isolation encoded as policy-as-code will not.
Related reading on 0thman.tech:
- [Internal Link: Zero Trust Cloud Security Architecture]
- [Internal Link: Kubernetes RBAC Deep Dive]
- [Internal Link: DevSecOps Pipeline Design]
- [Internal Link: GitOps with ArgoCD]
- [Internal Link: Container Security Fundamentals]
References
- Kubernetes Documentation — Network Policies
- Kubernetes Documentation — RBAC Good Practices
- Kubernetes Documentation — Pod Security Standards
- Kubernetes Documentation — Resource Quotas
- Kubernetes Documentation — Multi-tenancy
- CNCF — Multi-Tenancy Working Group guidance
- OWASP — Kubernetes Top Ten
- CIS — Kubernetes Benchmark
- MITRE ATT&CK — Containers Matrix
- Project Calico — Network Policy documentation