When I first started working with Kubernetes, namespaces felt like an afterthought — just a way to organize things in kubectl get pods. It didn’t take long running a real multi-team cluster to realize namespaces are actually one of the most important organizational and security primitives in the whole system. In this guide, I’ll walk through everything from the basics to advanced namespace-based multi-tenancy patterns on AWS EKS.
What Is a Namespace?
A namespace is a way to divide cluster resources between multiple users, teams, or applications within a single physical cluster. Kubernetes objects like Pods, Services, Deployments, ConfigMaps, and Secrets are namespaced — meaning their names only need to be unique within a namespace, not across the whole cluster. Cluster-scoped objects like Nodes, PersistentVolumes, and ClusterRoles exist outside any namespace.
Every cluster ships with four namespaces by default:
default— where objects land if you don’t specify a namespacekube-system— core Kubernetes system components (kube-dns, kube-proxy, etc.)kube-public— readable by all users, including unauthenticated ones; rarely used directlykube-node-lease— holds Lease objects used for node heartbeats
Why Namespaces Matter
Namespaces give you four practical levers:
- Resource isolation — via ResourceQuotas and LimitRanges scoped per namespace
- Access control — RBAC Roles and RoleBindings are namespace-scoped
- Network isolation — NetworkPolicies commonly scope traffic rules by namespace
- Organizational clarity — separating
dev,staging,prod, or per-team/per-product boundaries
Creating and Managing Namespaces
The simplest way:
kubectl create namespace team-payments
Or declaratively, which is what you want for any real environment:
apiVersion: v1
kind: Namespace
metadata:
name: team-payments
labels:
team: payments
environment: production
pod-security.kubernetes.io/enforce: restricted
annotations:
owner: payments-team@company.com
kubectl apply -f team-payments-namespace.yaml
List all namespaces:
kubectl get namespaces
NAME STATUS AGE
default Active 200d
kube-node-lease Active 200d
kube-public Active 200d
kube-system Active 200d
team-payments Active 2m
Set a default namespace for your current context so you don’t have to type -n team-payments on every command:
kubectl config set-context --current --namespace=team-payments
Delete a namespace (this cascades and deletes everything inside it — be careful):
kubectl delete namespace team-payments
Resource Quotas
A ResourceQuota constrains aggregate resource consumption per namespace — critical in any shared, multi-tenant cluster.
apiVersion: v1
kind: ResourceQuota
metadata:
name: payments-quota
namespace: team-payments
spec:
hard:
requests.cpu: "20"
requests.memory: 40Gi
limits.cpu: "40"
limits.memory: 80Gi
persistentvolumeclaims: "10"
services.loadbalancers: "2"
pods: "50"
Apply and check usage:
kubectl apply -f payments-quota.yaml
kubectl describe resourcequota payments-quota -n team-payments
Name: payments-quota
Namespace: team-payments
Resource Used Hard
-------- ---- ----
limits.cpu 12 40
limits.memory 24Gi 80Gi
pods 18 50
requests.cpu 6 20
requests.memory 12Gi 40Gi
services.loadbalancers 1 2
Once a ResourceQuota exists in a namespace, every pod created there must specify resource requests and limits, or the API server will reject it — this is a common source of confusion for teams that add a quota to an existing namespace without updating their manifests.
LimitRanges
While ResourceQuota caps the namespace total, LimitRange sets defaults and bounds per individual pod/container — useful so a single misconfigured deployment doesn’t eat the whole quota.
apiVersion: v1
kind: LimitRange
metadata:
name: payments-limits
namespace: team-payments
spec:
limits:
- type: Container
default:
cpu: 500m
memory: 512Mi
defaultRequest:
cpu: 100m
memory: 128Mi
max:
cpu: "2"
memory: 2Gi
min:
cpu: 50m
memory: 64Mi
RBAC Within Namespaces
Namespaces become genuinely powerful when combined with RBAC. Here’s a Role scoped to team-payments that lets developers manage Deployments and read Pods, but not touch Secrets:
apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
namespace: team-payments
name: developer
rules:
- apiGroups: ["apps"]
resources: ["deployments"]
verbs: ["get", "list", "watch", "create", "update", "patch"]
- apiGroups: [""]
resources: ["pods", "pods/log"]
verbs: ["get", "list", "watch"]
---
apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata:
name: developer-binding
namespace: team-payments
subjects:
- kind: Group
name: payments-devs
apiGroup: rbac.authorization.k8s.io
roleRef:
kind: Role
name: developer
apiGroup: rbac.authorization.k8s.io
On EKS, this pairs with aws-auth ConfigMap mappings (or the newer EKS access entries API) that map IAM identities to Kubernetes groups like payments-devs.
Namespace-Scoped Network Policies
By default, all pods can talk to all other pods across namespaces — Kubernetes networking is flat unless you restrict it. A common production pattern is default-deny plus explicit allow rules:
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: default-deny-ingress
namespace: team-payments
spec:
podSelector: {}
policyTypes:
- Ingress
---
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: allow-from-gateway
namespace: team-payments
spec:
podSelector:
matchLabels:
app: payments-api
policyTypes:
- Ingress
ingress:
- from:
- namespaceSelector:
matchLabels:
team: gateway
ports:
- protocol: TCP
port: 8080
Note that this requires a CNI that enforces NetworkPolicy — the default AWS VPC CNI historically didn’t, though it now supports NetworkPolicy enforcement in recent versions; otherwise you’d typically pair EKS with Calico or Cilium for this.
Multi-Tenancy Patterns Using Namespaces
For most organizations, namespace-per-team or namespace-per-environment is the pragmatic middle ground between “one shared cluster with no isolation” and “one cluster per team” (which multiplies operational overhead). A common EKS layout:
production-payments
production-checkout
production-inventory
staging-payments
staging-checkout
staging-inventory
Combine this with:
- Hierarchical namespace controllers (like the CNCF
hncproject) if you need namespace inheritance - Kyverno or Gatekeeper policies to enforce naming conventions and mandatory labels on namespace creation
- Karpenter or Cluster Autoscaler node groups tagged and tainted per namespace group if you need hard compute isolation, not just logical isolation
For genuinely hostile multi-tenancy (untrusted third-party workloads), namespaces alone are not a security boundary — consider separate clusters, or EKS with Firecracker-based isolation via Fargate, since a container escape can still cross namespace boundaries at the kernel/node level.
Automating Namespace Provisioning in CI/CD
A realistic GitOps pattern: teams request namespaces via a pull request to a namespaces/ directory, and ArgoCD or Flux syncs them automatically along with their quotas, RBAC, and network policies.
# Example Argo CD Application targeting a namespaces directory
kubectl apply -f - <<EOF
apiVersion: argoproj.io/v1alpha1
kind: Application
metadata:
name: namespace-provisioning
namespace: argocd
spec:
project: default
source:
repoURL: https://github.com/company/k8s-platform
path: namespaces
targetRevision: main
destination:
server: https://kubernetes.default.svc
syncPolicy:
automated:
prune: true
selfHeal: true
EOF
Troubleshooting
Error from server (NotFound): namespaces "x" not found— checkkubectl get nsfor typos, or confirm your kubeconfig context points at the right cluster.- Pods stuck
Pendingafter adding a ResourceQuota — checkkubectl describe podforFailedCreateevents; usually missing resource requests/limits. - Namespace stuck in
Terminating— usually a finalizer on a resource inside it (often a custom resource whose controller is gone). Check with:
kubectl get namespace team-payments -o json | jq '.spec.finalizers, .status'
If genuinely stuck, you can force-remove finalizers as a last resort (understand the risk — this can leave orphaned cloud resources):
kubectl get namespace team-payments -o json | \
jq '.spec.finalizers = []' | \
kubectl replace --raw "/api/v1/namespaces/team-payments/finalize" -f -
Cross-Namespace Communication Patterns
Even with isolation in place, teams almost always need some cross-namespace traffic — a shared internal API, a common logging pipeline, a shared ingress controller. The cleanest pattern is a dedicated shared-services namespace with explicitly allow-listed NetworkPolicy rules pointing at it, rather than punching arbitrary holes between individual team namespaces:
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: allow-shared-services-egress
namespace: team-payments
spec:
podSelector: {}
policyTypes:
- Egress
egress:
- to:
- namespaceSelector:
matchLabels:
tier: shared-services
- to:
- namespaceSelector: {}
podSelector:
matchLabels:
k8s-app: kube-dns
ports:
- protocol: UDP
port: 53
Note the explicit DNS egress rule — a very common mistake when adopting default-deny NetworkPolicies is forgetting that pods still need to resolve DNS, which itself is cross-namespace traffic to kube-system. Without an explicit allow rule for port 53, a default-deny egress policy silently breaks all outbound DNS resolution, which manifests as confusing “service not found” errors that have nothing to do with the actual target service.
Namespace Naming Conventions
A surprisingly high-leverage decision early on: settling on a consistent naming scheme before namespaces sprawl across dozens of teams. A common, readable pattern is <environment>-<team>-<optional-component>:
prod-payments
prod-payments-batch
staging-payments
dev-payments
This sorts cleanly in kubectl get ns, makes RBAC and NetworkPolicy label selectors predictable, and makes it trivial to write a Kyverno policy that validates new namespaces match the convention:
apiVersion: kyverno.io/v1
kind: ClusterPolicy
metadata:
name: enforce-namespace-naming
spec:
validationFailureAction: Enforce
rules:
- name: check-naming-pattern
match:
any:
- resources:
kinds:
- Namespace
validate:
message: "Namespace names must follow <env>-<team>[-<component>] convention."
pattern:
metadata:
name: "?(dev|staging|prod)-*"
Best Practices
- Never deploy directly into
defaultin any real environment — always create purpose-named namespaces. - Always pair ResourceQuota with LimitRange to avoid pods getting stuck due to missing defaults.
- Label namespaces consistently (
team,environment,cost-center) — this pays off enormously for cost allocation and policy targeting later. - Treat namespace manifests as code, reviewed and synced via GitOps, not created ad hoc with
kubectl create namespace. - Use Pod Security Admission labels at the namespace level as your security baseline (see the companion article on Pod Security).
Summary
Namespaces are the backbone of multi-tenant Kubernetes operations — they scope RBAC, resource quotas, network policies, and organizational ownership. On their own they’re a logical boundary, not a hard security boundary, so pair them with quotas, RBAC, network policies, and (for genuinely untrusted workloads) additional isolation like separate clusters or Fargate. Treat namespace creation as a governed, automated process rather than a manual kubectl create step, and your cluster will scale far more gracefully as teams and workloads grow.