In a previous article, the reasons why you would need to implement multi-tenancy in your Kubernetes cluster were covered, along with what it is and the different ways to implement it.
This article covers tenant isolation strategies, so you know which one to use and when, from namespace separation through to virtual control planes and dedicated node pools.
Isolation mechanisms and boundaries in Kubernetes multi-tenancy
For a quick recap, each row below shows a boundary you can implement between tenants, and how far it actually separates them on a shared cluster:
Namespace isolation as the primary isolation boundary
Namespaces are the logical starting point for multi-tenancy, but they are strictly organizational units rather than secure barriers. Relying on namespaces alone, without explicit policy configuration, results in very weak security.
By default, native namespaces provide very weak isolation because they suffer from structural visibility leaks:
Tenants can enumerate every namespace in the cluster. Namespaces are a globally scoped type, so a tenant who can list namespaces at all sees all of them, including the names of your other customers.
Tenants can discover every service in the cluster through DNS. CoreDNS answers queries about services in other namespaces by default, so a pod in one tenant's namespace can enumerate another tenant's services.
While these leaks do not expose raw data on their own, they hand potential attackers a detailed map of what to target next, which is what makes strict policy enforcement necessary:
- Lock down globally scoped RBAC. Keep list and watch permissions for cluster-scoped resources such as namespaces completely off your tenant roles, so one tenant cannot enumerate your customer names.
- Constrain CoreDNS discovery. Use CoreDNS firewall rules or policy plugins to block cross-namespace service discovery queries.
- Manage namespaces through GitOps. Disable the auto-create namespace option in Argo CD or its equivalent in your own CD tool, then define, version-control, and deploy every namespace manifest from Git, so tenant lifecycles stay clean and nothing is left orphaned behind a deleted tenant.
- Apply quotas and limits. Enforce namespace-level resource quotas so no single tenant's workload can expand and starve the others of CPU, memory, or storage.
Control plane isolation strategies
When you run multiple teams on a single shared cluster, everyone talks to the same host API server.
That creates two structural bottlenecks.
First, a single team's demanding controller or automated pipeline can saturate the host control plane, exhausting request capacity for everyone else.
Second, any CustomResourceDefinition installed by one tenant is registered globally, so two teams cannot run different versions of the same operator without breaking each other's deployments.
A virtual control plane removes those API-level limitations. Each tenant gets an isolated environment running its own copy of the three pieces that make up a Kubernetes control plane:
- API server
- Controller manager
- Datastore
Tenants can then install custom CRD versions, configure their own admission webhooks, and run different Kubernetes API versions without conflicting with anyone or needing cluster-level administrator permissions.
Their containerized workloads are still scheduled and executed on the physical nodes of your shared host cluster.
Depending on your scaling needs, two open source CNCF Sandbox projects take different routes to that separation:
- vCluster runs a lightweight virtual control plane inside a standard namespace of the host cluster. A syncer pod translates and replicates tenant resources down to the host as standard pods. It is cheap to run, fast to provision, and gives developers full admin rights inside their own virtual cluster.
- Kamaji, from CLASTIX, runs each tenant's API server and datastore as a pod inside a central admin cluster. Tenant worker nodes are provisioned on separate node pools and configured to join their respective remote control planes. It operates like a self-managed Kubernetes service, which makes it a better fit when tenants need strictly dedicated computing infrastructure.
Virtual control planes isolate API traffic and custom resources, and neither project separates the operating system kernel. The workloads still run on shared host nodes, so a successful container escape reaches the host kernel and gives an attacker a path into other tenants' workloads on that machine.
Running a separate physical cluster for each team is financially draining, and consolidating those teams onto virtual clusters removes a large part of that bill by cutting out redundant infrastructure.
The bigger saving is shared platform overhead. Instead of deploying a separate ingress controller, logging agent, and monitoring stack for every tenant, your virtual clusters reuse a single platform stack on the host cluster. Developers still get full admin rights inside their own virtual cluster, without the operational and financial overhead of dedicated infrastructure.
Dedicated node pools: Hardening the physical boundary
Even when namespaces, RBAC, and virtual clusters are configured, tenant workloads still share the same physical nodes and the same operating system kernel by default. To close that exposure, schedule sensitive tenant workloads onto dedicated node pools.
The Kubernetes scheduler uses taints and tolerations along with node affinity to guide those deployment decisions, attracting workloads to their designated hardware while pushing unauthorized pods away.
Label and taint the nodes so unauthorized pods are pushed away:
taints:
- key: tenant
value: tenant-a
effect: NoScheduleThen give the tenant's pods a matching toleration and a node affinity, so they are both permitted on those nodes and attracted to them:
spec:
affinity:
nodeAffinity:
requiredDuringSchedulingIgnoredDuringExecution:
nodeSelectorTerms:
- matchExpressions:
- key: node-restriction.kubernetes.io/tenant
operator: In
values: ["tenant-a"]
tolerations:
- key: tenant
operator: Equal
value: tenant-a
effect: NoScheduleSeparate nodes stop tenants sharing a machine, and the containers on each machine still share its kernel. Close that gap with a runtime that replaces the kernel or a monitor that watches it:
- gVisor puts a userspace kernel in front of the real one and intercepts syscalls there, so an escape lands in the sandbox rather than on the host.
- Kata Containers removes the sharing entirely by running each pod in a lightweight VM.
- Falco monitors Linux system calls in real time and alerts you when a container tries to escape its boundary and reach the host.
Network isolation: Securing the shared overlay
In a fresh Kubernetes cluster, every pod can reach every other pod by default, even across different namespaces.
To establish true logical multi-tenancy, reverse that default-open behavior with NetworkPolicies that restrict traffic flow and segment your tenant workloads.
These policies let a tenant's applications talk to their own services while staying isolated from other tenants sharing the cluster.
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: default-deny-all
namespace: tenant-a
spec:
podSelector: {}
policyTypes: ["Ingress", "Egress"]For a zero-trust model, layer encryption and access control on top of those basic network rules. Standard network policies control traffic at the IP address and port level, and a service mesh adds cryptographically secure service-to-service identity. At the edge, design your ingress architecture deliberately, deciding whether to use a shared ingress controller to consolidate infrastructure costs or run dedicated controllers to isolate incoming load balancer traffic.
Four architectural guardrails matter here:
- Ensure CNI policy enforcement. A NetworkPolicy object is ignored unless your underlying Container Network Interface plugin supports and enforces it. Calico and Cilium enforce policy, and Flannel on its own does not.
- Layer on a service mesh for zero-trust. When tenants do not trust each other, a service mesh such as Linkerd or Istio enforces mutual TLS and secures service-to-service communication without any modification to your developers' application code.
- Balance ingress costs against isolation. Use a shared ingress controller and load balancer to minimize your cloud bill and centralize TLS certificate management, or provision dedicated per-tenant ingress controllers for the strongest traffic boundaries.
- Enforce centralized network governance. Rather than relying on individual developers to write secure network configurations, use GitOps to declaratively deploy cluster-wide policy engines such as Kyverno or OPA Gatekeeper that automatically audit or block non-compliant networking manifests.
Storage isolation and secrets protection
While namespaces logically isolate resources like PersistentVolumeClaims by default, managing tenant-specific storage takes active platform guardrails.
In shared clusters, administrators use StorageClasses to define different tiers of storage, such as high-performance database volumes against low-cost archival storage, to match varying tenant priorities.
Because dynamic provisioning lets workloads request storage automatically, a single tenant can monopolize and exhaust the physical backing store. To enforce fair allocation, apply namespace-level resource quotas that restrict total storage capacity and stop any one tenant starving the others.
Data protection matters as much, and a common pitfall is assuming that native Kubernetes Secrets are secure by default. Kubernetes Secrets are base64-encoded rather than encrypted, and base64 is a reversible encoding that anyone with access can decode.
Securing sensitive credentials takes encryption at rest and in transit.
GitOps makes it worse beacuse your whole cluster state lives in Git. This means that Secrets have to as well, and a raw Secret committed once sits in the history and on every machine that has cloned the repository.
You can use two standard strategies to solve this:
- Sealed Secrets encrypts your secrets locally into a SealedSecret custom resource that is safe to commit to Git, and only an in-cluster controller can decrypt it.
- External Secrets Operator keeps sensitive values outside the cluster in a vault such as Azure Key Vault, AWS Secrets Manager, or Google Secret Manager, then injects them into your workloads at runtime.
Conclusion
In the Kubernetes world, there is no single silver bullet for isolation. True multi-tenancy is always a stack of logical, physical, and policy boundaries.
But remember the golden rule of platform engineering: just because you can build a boundary does not mean you should.
Every single layer of isolation you introduce, from network policies to virtual control planes, carries a continuous maintenance tax on your backlog.
Build only the boundaries that your tenants' threat models actually justify, because the most secure platform is the one your team actually has the operational bandwidth to keep correct as the cluster evolves.
When you have decided your approach to isolation, the next step is deciding what each tenant is allowed to do inside the cluster. To understand this, you can go through our guide on RBAC, admission control, and resource quotas in multi-tenant clusters.
Sign up to Rackspace Spot and get a cluster starting at $7.20 a month.
Frequently asked questions
Do namespaces provide security isolation in Kubernetes?
No. A namespace is an organizational unit. It becomes a boundary only once you add RBAC, network policy, and quotas on top of it.
Does a NetworkPolicy work with any CNI?
No. The API accepts the object either way, so policies can look applied and block nothing. Calico and Cilium enforce them, and Flannel on its own does not.
Can two tenants run different versions of the same CRD?
Not in one cluster. CustomResourceDefinitions are cluster-scoped, so one version serves everyone. Give each tenant a virtual control plane or a cluster of their own.
How do you give a tenant its own nodes?
Taint the nodes so other pods are pushed away, then give the tenant's pods a matching toleration and a node affinity. You need both, since either alone leaves a gap.
Are Kubernetes Secrets encrypted?
No. They are base64-encoded, which anyone can reverse. Enable encryption at rest with an external key management service, or keep the values outside the cluster.