Multi-Tenancy Patterns: Namespace Isolation, vCluster, and Dedicated Clusters#
Multi-tenancy in Kubernetes means running workloads for multiple teams, customers, or environments on shared infrastructure. The core tension is always the same: sharing reduces cost, but isolation prevents blast radius. Choosing the wrong model creates security gaps or wastes money. This guide provides a framework for selecting the right approach and implementing it correctly.
The Three Models#
Every Kubernetes multi-tenancy approach falls into one of three categories, each with different isolation guarantees:
| Model | Isolation Level | Cost | Complexity | Use Case |
|---|---|---|---|---|
| Namespace isolation | Soft (logical) | Low | Low | Internal teams, dev/staging environments |
| vCluster (virtual clusters) | Medium (API-level) | Medium | Medium | Platform teams, CI/CD environments, stronger tenant boundaries |
| Dedicated clusters | Hard (infrastructure) | High | High | Regulatory requirements, hostile tenants, production SaaS |
The decision depends on your threat model. If tenants are internal teams who trust each other, namespace isolation is sufficient. If tenants are external customers who might be adversarial, you need dedicated clusters or at minimum vCluster with strict controls.
Model 1: Namespace Isolation#
Namespace isolation is the simplest and most common model. Each tenant gets one or more namespaces with RBAC, ResourceQuotas, NetworkPolicies, and optionally Pod Security Standards restricting what they can do.
Namespace-per-Tenant Setup#
Create a namespace for each tenant with standard labels:
kubectl create namespace tenant-alpha
kubectl label namespace tenant-alpha tenant=alpha environment=productionResource Quotas: Prevent Resource Starvation#
Without quotas, one tenant can consume the entire cluster. Apply quotas to every tenant namespace:
apiVersion: v1
kind: ResourceQuota
metadata:
name: tenant-quota
namespace: tenant-alpha
spec:
hard:
requests.cpu: "8"
requests.memory: 16Gi
limits.cpu: "16"
limits.memory: 32Gi
pods: "50"
services: "20"
persistentvolumeclaims: "10"
services.loadbalancers: "2"Pair this with a LimitRange so pods without explicit resource specs get reasonable defaults instead of being rejected:
apiVersion: v1
kind: LimitRange
metadata:
name: tenant-limits
namespace: tenant-alpha
spec:
limits:
- default:
cpu: 500m
memory: 512Mi
defaultRequest:
cpu: 100m
memory: 128Mi
type: ContainerRBAC: Scope Tenant Access#
Grant each tenant a Role scoped to their namespace. Never use ClusterRoleBindings for tenant access:
apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
name: tenant-admin
namespace: tenant-alpha
rules:
- apiGroups: ["", "apps", "batch", "networking.k8s.io"]
resources: ["pods", "deployments", "services", "configmaps",
"jobs", "cronjobs", "ingresses", "statefulsets"]
verbs: ["get", "list", "watch", "create", "update", "patch", "delete"]
- apiGroups: [""]
resources: ["pods/log", "pods/exec"]
verbs: ["get", "create"]
---
apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata:
name: tenant-alpha-admin
namespace: tenant-alpha
subjects:
- kind: Group
name: tenant-alpha-team
apiGroup: rbac.authorization.k8s.io
roleRef:
kind: Role
name: tenant-admin
apiGroup: rbac.authorization.k8s.ioCritical exclusions: tenants should never have access to Secrets in other namespaces, ClusterRoles, ClusterRoleBindings, Nodes, or PersistentVolumes (the cluster-scoped resource, not PVCs).
Network Policies: Tenant Network Isolation#
Default-deny all cross-namespace traffic, then allow only what is necessary:
# Default deny all ingress and egress in tenant namespace
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: default-deny-all
namespace: tenant-alpha
spec:
podSelector: {}
policyTypes:
- Ingress
- Egress
---
# Allow DNS (required for any egress deny)
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: allow-dns
namespace: tenant-alpha
spec:
podSelector: {}
policyTypes:
- Egress
egress:
- to:
- namespaceSelector:
matchLabels:
kubernetes.io/metadata.name: kube-system
ports:
- protocol: UDP
port: 53
- protocol: TCP
port: 53
---
# Allow intra-namespace traffic
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: allow-same-namespace
namespace: tenant-alpha
spec:
podSelector: {}
policyTypes:
- Ingress
- Egress
ingress:
- from:
- podSelector: {}
egress:
- to:
- podSelector: {}This pattern ensures tenants can communicate freely within their namespace but cannot reach pods in other tenant namespaces. To allow specific cross-tenant traffic (for example, a shared ingress controller), add targeted policies using namespaceSelector.
Pod Security Standards: Restrict Privileged Workloads#
Prevent tenants from running privileged containers, mounting the host filesystem, or escalating privileges:
kubectl label namespace tenant-alpha \
pod-security.kubernetes.io/enforce=restricted \
pod-security.kubernetes.io/warn=restricted \
pod-security.kubernetes.io/audit=restrictedThe restricted level blocks hostPath mounts, privileged containers, host networking, and running as root. Use baseline if restricted is too strict for your workloads.
Limitations of Namespace Isolation#
Namespace isolation has real gaps. Tenants share the same control plane, so a tenant flooding the API server with requests affects everyone. Cluster-scoped resources (CRDs, ClusterRoles, IngressClasses) cannot be isolated per namespace. Node-level attacks (container escapes) affect all tenants on that node. For stronger isolation, move to vCluster or dedicated clusters.
Model 2: vCluster (Virtual Clusters)#
vCluster creates lightweight virtual Kubernetes clusters that run inside namespaces of a host cluster. Each tenant gets what looks and feels like a full Kubernetes cluster, with its own API server, controller manager, and etcd (or SQLite), but pods are scheduled on the host cluster’s nodes.
When vCluster Makes Sense#
- Tenants need cluster-admin-level access (install CRDs, manage namespaces) without affecting others
- You need hundreds of isolated environments (CI/CD, developer sandboxes) without the cost of dedicated clusters
- You want stronger API-level isolation than namespaces provide but cannot justify separate clusters
Deploying a vCluster#
# Create a virtual cluster for a tenant
vcluster create tenant-alpha --namespace vcluster-alpha
# Connect to the virtual cluster
vcluster connect tenant-alpha --namespace vcluster-alpha
# For production, use Helm with resource limits and restricted syncing
helm upgrade --install tenant-alpha vcluster \
--repo https://charts.loft.sh \
--namespace vcluster-alpha \
--create-namespace \
--set syncer.resources.limits.cpu=1 \
--set syncer.resources.limits.memory=1Gi \
--set sync.persistentvolumes.enabled=false \
--set sync.nodes.enabled=falseDisabling PV and node syncing prevents tenants from seeing host-level resources.
Isolation Boundaries in vCluster#
vCluster provides API-level isolation, not runtime isolation. Pods still run on the same nodes as other tenants. Combine vCluster with node affinity or node pools to add runtime isolation:
# In the vCluster Helm values, restrict pods to specific nodes
isolation:
enabled: true
nodeProxyPermission:
enabled: false
resourceQuota:
enabled: true
quota:
requests.cpu: "10"
requests.memory: 20GiModel 3: Dedicated Clusters#
Dedicated clusters give each tenant their own control plane, nodes, and network. This is the only model that provides true infrastructure-level isolation.
When Dedicated Clusters Are Required#
- Regulatory or compliance requirements mandate infrastructure separation (PCI-DSS, HIPAA, SOC2 with strict scoping)
- Tenants are untrusted or potentially adversarial (public SaaS where customers run arbitrary code)
- Blast radius must be zero – a failure in one tenant’s cluster cannot affect another tenant
- Tenants need different Kubernetes versions, CNI plugins, or cluster configurations
Managing Multiple Clusters#
The operational cost of dedicated clusters is significant. Reduce it with automation:
# Standardized cluster provisioning with Terraform or Crossplane
# Each tenant gets identical infrastructure with parameterized values
# Use a fleet management tool to deploy policies across all clusters
# Example: apply a baseline NetworkPolicy to every tenant cluster
for cluster in $(kubectl config get-contexts -o name | grep tenant-); do
kubectl --context "$cluster" apply -f baseline-policies/
doneFleet management tools like Rancher, Anthos Config Management, or ArgoCD with ApplicationSets help keep policies, addons, and configurations consistent across tenant clusters.
Decision Framework#
Use these questions to select a model:
Start with namespace isolation if:
- Tenants are internal teams or environments within the same organization
- You have fewer than 20 tenants
- No regulatory requirement mandates infrastructure separation
- Tenants do not need cluster-admin access or custom CRDs
Move to vCluster if:
- Tenants need their own namespaces, CRDs, or cluster-level resources
- You need dozens to hundreds of isolated environments
- You want the cost profile of shared infrastructure with stronger isolation than namespaces
- Tenants are semi-trusted (internal teams, partners) but need independence
Move to dedicated clusters if:
- Tenants are untrusted or external customers
- Compliance requires infrastructure-level separation
- Tenants need different Kubernetes versions or configurations
- You need guaranteed blast radius containment
Verifying Isolation#
After implementing any model, verify the boundaries actually work:
# Test cross-tenant network access (should be blocked)
kubectl exec -it test-pod -n tenant-alpha -- \
wget -qO- --timeout=3 http://service.tenant-beta.svc.cluster.local:8080
# Verify RBAC prevents cross-tenant resource access
kubectl auth can-i get pods -n tenant-beta \
--as=system:serviceaccount:tenant-alpha:default
# Expected: no
# Check that resource quotas are enforced
kubectl describe resourcequota -n tenant-alpha
# Verify Pod Security Standards are blocking privileged containers
kubectl run priv-test --image=nginx --restart=Never -n tenant-alpha \
--overrides='{"spec":{"containers":[{"name":"test","image":"nginx","securityContext":{"privileged":true}}]}}'
# Expected: rejected by pod security policyIsolation that is not tested is isolation that does not exist. Run these checks after every change to your tenancy model.