4.6 Namespace-Based Multi-Tenancy Patterns and Their Limits
Alright, let’s talk about using namespaces for multi-tenancy. You’re probably thinking, “I’ll just slap each customer into their own namespace, it’ll be clean, isolated, and perfect.” And I’m here to be that brilliant friend who tells you, “Yes, but also no, and here’s why you’re about to get a nasty surprise at 3 AM.”
The core idea is sound. A Kubernetes namespace is a fantastic boundary for organization, not unlike having separate folders for different projects on your laptop. It lets you scope object names, apply access controls with RBAC, and assign resource quotas. For a lot of use cases, this is 90% of what you need. But—and it’s a big but—namespaces are not a security boundary. They’re a organizational boundary that sits inside a single, shared cluster security domain. This distinction is everything.
The Illusion of Hard Isolation
Let’s get this out of the way first: namespaces do not provide network isolation by default. Pods in the customer-a namespace can, right out of the gate, talk to pods in the customer-b namespace if they know the correct Service DNS name (<service-name>.<namespace-name>.svc.cluster.local). This is a multi-tenancy nightmare.
# customer-a's pod could talk to customer-b's database like this:
curl http://customer-b-database.customer-b.svc.cluster.local:5432
This is the part where the designers were maybe a bit too optimistic about everyone’s goodwill. To fix this, you must use a Kubernetes Network Policy to explicitly deny all cross-namespace traffic. And I mean you must, because it’s not there by default.
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: deny-cross-namespace
namespace: customer-a
spec:
podSelector: {} # Selects all pods in the namespace
policyTypes:
- Ingress
- Egress
ingress:
- from:
- podSelector: {} # Only allow traffic from pods in this same namespace
egress:
- to:
- podSelector: {} # Only allow traffic to pods in this same namespace
You need to apply this in every tenant namespace. Forgetting one is like leaving the back door to your bank vault wide open with a welcome sign.
Resource Quotas: Your First Real Line of Defense
This is where namespaces genuinely shine. ResourceQuota objects are namespace-scoped and are absolutely critical for preventing a noisy neighbor scenario. Without them, Tenant A can greedily consume all the CPU and memory in the entire cluster, bringing everyone else to a grinding halt.
apiVersion: v1
kind: ResourceQuota
metadata:
name: tenant-a-quota
namespace: tenant-a
spec:
hard:
requests.cpu: "10"
requests.memory: 20Gi
limits.cpu: "20"
limits.memory: 40Gi
requests.storage: 100Gi
pods: "50"
services: "10"
The beauty here is the API server will straight-up reject any Pod or PersistentVolumeClaim creation that would exceed these limits. It’s a hard stop. Use them. Love them.
The RBAC Glue That Holds It All Together
Isolation is useless if Tenant A’s developer can kubectl get secrets -n tenant-b. This is where Role-Based Access Control (RBAC) becomes your best friend. You need to be meticulous.
- Create a Role in each tenant’s namespace that defines what they can do there.
- Create a RoleBinding in each tenant’s namespace that grants that Role to a user or group.
# A Role in the tenant-a namespace
apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
namespace: tenant-a
name: tenant-a-admin
rules:
- apiGroups: [""]
resources: ["pods", "services", "secrets", "configmaps"]
verbs: ["get", "list", "watch", "create", "update", "delete"]
---
# Binding that role to a user
apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata:
namespace: tenant-a
name: tenant-a-admin-binding
subjects:
- kind: User
name: "tenant-a-admin"
apiGroup: rbac.authorization.k8s.io
roleRef:
kind: Role
name: tenant-a-admin
apiGroup: rbac.authorization.k8s.io
The critical thing to remember: a RoleBinding can only reference a Role in its own namespace. This is what scopes the permission and makes the model work.
The Limits of the Namespace Model
Here’s where the cracks start to show. Namespace-scoping falls apart for a few critical resources:
- Cluster-Scoped Resources: PersistentVolumes, StorageClasses, IngressClasses, and CustomResourceDefinitions (CRDs) are cluster-scoped. A tenant can’t be allowed to create their own StorageClass that provisions expensive SSDs on your dime. You have to manage these centrally. This is a hard limit.
- Node Resources: A pod from any tenant can schedule onto any node. While quotas limit a pod’s request, a buggy pod could still saturate a node’s network or disk I/O, affecting other tenants on that node. Advanced techniques like taints, tolerations, and dedicated node pools are needed to mitigate this.
- The Control Plane: It’s still shared. A single tenant creating 500 pods in rapid succession can still increase the load on the API server and controller managers for everyone else. There’s no way to rate-limit by namespace at the Kubernetes level.
So, When Should You Use This Pattern?
Use namespace-based multi-tenancy when your tenants are trusted (e.g., different internal teams, different environments) or when the cost of a breach is low. It’s perfect for development environments, CI/CD systems, or internal platforms.
Do not use it for true hostile multi-tenancy (e.g., random, untrusted customers on a public cloud platform). For that, you need the nuclear option: separate clusters. Or you need to layer on so many additional security policies and monitoring tools (e.g., Pod Security Admission, OPA/Gatekeeper, service meshes) that you’ve essentially built a software-based cluster per namespace. At that point, you might as well use the real thing.
The pattern is powerful, but it’s a carefully constructed illusion. Your job is to make the walls of that illusion so thick and well-guarded that no one ever realizes they’re not real.