Kubernetes Namespaces: Multi-Tenant Best Practices
Kubernetes namespaces provide a mechanism for isolating groups of resources within a single cluster. In a multi-tenant environment — whether that means multiple teams, multiple applications, or multiple environments (dev/staging/production) — namespaces are the foundational building block for implementing access control, resource limits, network isolation, and operational boundaries. Understanding how to use namespaces effectively is one of the most important skills for operating a shared Kubernetes cluster at scale.
Table of Contents
Namespace Basics and Scope
Namespaces divide cluster resources into virtual sub-clusters. Most Kubernetes objects are namespace-scoped: Pods, Services, Deployments, ConfigMaps, Secrets, ServiceAccounts, and PersistentVolumeClaims all live within a namespace. A few resources are cluster-scoped and exist outside any namespace: Nodes, PersistentVolumes, StorageClasses, ClusterRoles, and ClusterRoleBindings.
Kubernetes creates four system namespaces by default:
- default — the namespace for objects with no specified namespace. Avoid deploying production workloads here.
- kube-system — Kubernetes system components: API server, controller manager, CoreDNS, kube-proxy
- kube-public — publicly readable resources; contains the cluster-info ConfigMap
- kube-node-lease — node heartbeat Lease objects used by the node lifecycle controller
# List all namespaces
kubectl get namespaces
# Create a new namespace
kubectl create namespace team-payments
# Or declaratively
kubectl apply -f - <
Naming Conventions and Lifecycle
Consistent namespace naming makes cluster management predictable. Common patterns include:
- Team-based:
team-payments,team-identity,team-platform - Environment-based:
payments-dev,payments-staging,payments-production - Service-based:
svc-api-gateway,svc-notification
Apply standard labels to every namespace for policy enforcement and cost allocation:
apiVersion: v1
kind: Namespace
metadata:
name: team-payments-production
labels:
team: payments
environment: production
cost-center: "cc-1042"
pod-security.kubernetes.io/enforce: restricted
pod-security.kubernetes.io/warn: restricted
annotations:
owner: payments-team@company.com
slack-channel: "#payments-oncall"
runbook: "https://wiki.company.com/payments/runbook"
Namespace deletion is permanent and cascades to all resources within. Protect critical namespaces from accidental deletion with a finalizer or admission webhook that blocks deletion of namespaces labelled protected: true.
RBAC for Namespace Isolation
Role-based access control (RBAC) is the primary mechanism for enforcing who can do what within a namespace. Grant namespace-scoped permissions with Role and RoleBinding, not ClusterRole.
# Developer role — can read/write most resources but not secrets
apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
name: developer
namespace: team-payments
rules:
- apiGroups: ["", "apps", "batch"]
resources: ["pods", "services", "deployments", "jobs", "configmaps"]
verbs: ["get", "list", "watch", "create", "update", "patch", "delete"]
- apiGroups: [""]
resources: ["pods/log", "pods/exec"]
verbs: ["get", "list"]
# Read-only on secrets
- apiGroups: [""]
resources: ["secrets"]
verbs: ["get", "list"]
---
# Bind the role to a group (all members of the payments team)
apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata:
name: developer-binding
namespace: team-payments
subjects:
- kind: Group
name: payments-developers # maps to OIDC group claim
apiGroup: rbac.authorization.k8s.io
roleRef:
kind: Role
name: developer
apiGroup: rbac.authorization.k8s.io
Network Isolation with Network Policies
By default, all pods in a Kubernetes cluster can communicate with all other pods regardless of namespace. Apply a default-deny NetworkPolicy to each namespace and then explicitly allow the required communication paths.
# Default deny all ingress and egress within the namespace
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: default-deny-all
namespace: team-payments
spec:
podSelector: {}
policyTypes:
- Ingress
- Egress
---
# Allow DNS egress (required for all pods to resolve service names)
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: allow-dns
namespace: team-payments
spec:
podSelector: {}
policyTypes:
- Egress
egress:
- ports:
- port: 53
protocol: UDP
- port: 53
protocol: TCP
---
# Allow payments API to reach the database namespace
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: allow-payments-to-db
namespace: team-payments
spec:
podSelector:
matchLabels:
app: payments-api
policyTypes:
- Egress
egress:
- to:
- namespaceSelector:
matchLabels:
team: database
ports:
- port: 5432
Resource Quotas per Namespace
ResourceQuota prevents a single namespace from consuming all cluster resources, protecting other tenants from noisy-neighbour effects.
apiVersion: v1
kind: ResourceQuota
metadata:
name: team-payments-quota
namespace: team-payments
spec:
hard:
# Compute
requests.cpu: "8"
requests.memory: 16Gi
limits.cpu: "16"
limits.memory: 32Gi
# Storage
requests.storage: 200Gi
persistentvolumeclaims: "10"
# Object counts
pods: "50"
services: "20"
secrets: "50"
configmaps: "30"
# LoadBalancer services (expensive on cloud)
services.loadbalancers: "2"
# Check current quota usage
kubectl describe resourcequota team-payments-quota -n team-payments
LimitRanges for Default Constraints
LimitRange sets default resource requests and limits for pods that do not specify them, and enforces minimum/maximum bounds. Without LimitRange, a pod with no resource requests can schedule on any node and consume unlimited resources.
apiVersion: v1
kind: LimitRange
metadata:
name: team-payments-limits
namespace: team-payments
spec:
limits:
- type: Container
default: # applied when no limits specified
cpu: 500m
memory: 512Mi
defaultRequest: # applied when no requests specified
cpu: 100m
memory: 128Mi
max: # container cannot exceed these
cpu: "4"
memory: 4Gi
min: # container must request at least these
cpu: 50m
memory: 64Mi
- type: Pod
max:
cpu: "8"
memory: 8Gi
- type: PersistentVolumeClaim
max:
storage: 50Gi
min:
storage: 1Gi
Namespace Templates with Helm
Creating a new team namespace involves deploying a Namespace, ResourceQuota, LimitRange, NetworkPolicies, Roles, and RoleBindings consistently. Package this as a Helm chart to ensure every new namespace gets the same baseline security and resource controls.
# Helm chart structure for namespace template
namespace-template/
├── Chart.yaml
├── values.yaml
└── templates/
├── namespace.yaml
├── resourcequota.yaml
├── limitrange.yaml
├── networkpolicies.yaml
├── role-developer.yaml
└── rolebinding-developer.yaml
# Deploy a new namespace for team-identity
helm upgrade --install team-identity ./namespace-template \
--set team.name=identity \
--set team.environment=production \
--set team.costCenter=cc-1055 \
--set team.slackChannel="#identity-oncall" \
--set quota.cpuRequests=4 \
--set quota.memoryRequests=8Gi \
--set rbac.developerGroup=identity-developers
Multi-Tenancy Patterns and Limitations
Kubernetes namespaces provide soft multi-tenancy: isolation through RBAC and network policies, but sharing the same kernel, node resources, and control plane. This is appropriate for trusted tenants (different teams in the same company) but insufficient for untrusted tenants (customers running arbitrary workloads).
For stricter isolation, consider these patterns:
- Virtual clusters (vcluster): Run a lightweight Kubernetes API server inside a namespace. Each tenant gets their own API server with full admin access while sharing the underlying node pool. Strong isolation for devops/platform teams.
- Node isolation: Dedicate specific nodes to specific namespaces using node selectors and taints. Prevents cross-tenant resource contention at the kernel level.
- Separate clusters: For truly untrusted tenants or strict regulatory separation, separate clusters provide the strongest isolation boundary. Tools like Cluster API or cloud provider managed clusters make multi-cluster management feasible.
company > division > team > environment.