Namespaces
Rewritten for this edition in October 2026 for Kubernetes 1.37. Commands checked against the official documentation.
A Kubernetes namespace is a named scope inside one cluster. Object names only have to be unique within their namespace, so two teams can each run a Deployment called web without colliding. Quotas, default resource limits, RBAC Roles and Pod Security labels all attach at the namespace level too. Three things it isn't: a separate cluster, a network wall, or a place for every kind of object. Nodes, PersistentVolumes, StorageClasses and namespaces themselves live outside any namespace. Day to day you'll need just three commands: kubectl create namespace, the -n flag, and kubectl config set-context --current --namespace= to change your default.
What a namespace scopes, and what it leaves alone#
The namespaces concept page draws the line plainly: namespace scoping applies to namespaced objects like Deployments and Services, and not to cluster-wide ones. Your cluster can tell you which is which. Ask it for the second group:
kubectl api-resources --namespaced=false
You'll see Nodes, PersistentVolumes, StorageClasses, ClusterRoles and Namespace itself in that list. Swap false for true and you get everything that does belong to a namespace: Pods, Deployments, Services, ConfigMaps, Secrets, Roles. Keep this split in mind, because it explains a lot of later surprises. A PersistentVolumeClaim sits in your namespace; the PersistentVolume it binds to doesn't.
Two rules hold without exception. Namespaces can't be nested, and every namespaced object belongs to exactly one of them.
The docs also say when you don't need extra namespaces. A cluster with a few to tens of users can live without them, and two versions of the same app are better told apart by labels than by separate namespaces. So the reasons to add one are the features below: separate names, separate permissions, separate resource budgets.
The four namespaces every cluster starts with#
A fresh cluster already has four. Here's what each one is for, per the official list:
defaultexists so you can start working without creating anything. Every chapter before this one put its objects here.kube-systemholds the objects the Kubernetes system creates for itself. Look, but leave it alone.kube-publicis readable by all clients, unauthenticated ones included. That "public" part is a convention, not something the API forces on it.kube-node-leasecarries one Lease object per node. The kubelet renews its Lease as a heartbeat, which is how the control plane notices a node has died.
The kube- prefix is reserved for these system namespaces, so don't use it for yours. And for a production cluster the docs advise against running workloads in default at all.
Before you start#
You need a running cluster and a working kubectl. A local one from the KinD Cluster or K3s and K3sup chapter is perfect, since you'll be deleting things on purpose. It helps if you've already worked through Kubernetes Deployment; this chapter creates a couple and won't explain them again. Check the starting point:
kubectl get namespaces
On a new cluster, output looks like this:
NAME STATUS AGE
default Active 1d
kube-node-lease Active 1d
kube-public Active 1d
kube-system Active 1d
Your distribution may add more of its own (an ingress controller or a storage provisioner often ships in one). That's normal.
Create two namespaces, two ways#
The walkthrough uses team-a and team-b. Create the first one imperatively:
kubectl create namespace team-a
For the second, write a manifest. This is the form you'd commit to Git, and it lets you add labels at creation time:
apiVersion: v1
kind: Namespace
metadata:
name: team-b
labels:
team: b
Save it as team-b.yaml and apply it:
kubectl apply -f team-b.yaml
kubectl get namespaces --show-labels
Notice a label you didn't write. Since Kubernetes 1.22 the control plane stamps every namespace with an immutable kubernetes.io/metadata.name label whose value is the namespace's own name. It looks redundant now; it's what lets a NetworkPolicy pick a namespace by name later in this chapter.
Names have rules. A namespace name must be a valid RFC 1123 DNS label (at most 63 characters, lowercase letters, digits and hyphens, starting with a letter and ending with a letter or digit), because it ends up inside DNS names. The docs carry a warning that follows from this: a namespace named after a public top-level domain such as com can give its Services short names that shadow real public DNS records for every workload in the cluster. That's why creating namespaces should be a privilege you hand only to people you trust.
Put workloads into a namespace#
Every kubectl command takes -n (long form --namespace) to say where a request goes. Create a small Deployment and a Service in team-a:
kubectl create deployment web --image=nginx --replicas=2 -n team-a
kubectl expose deployment web --port=80 -n team-a
kubectl get deployment,service -n team-a
Now run plain kubectl get deployments without the flag. Nothing named web shows up, because the request went to default. This is the first namespace mistake everyone makes: the object exists, you're just looking in the wrong place. When you don't know where something lives, list across all of them:
kubectl get pods -A
-A is short for --all-namespaces, and it ignores the namespace in your current context.
In a manifest, the namespace goes under metadata.namespace. Some teams leave it out and pass -n at apply time so the same file can land in several namespaces; others pin it so a file can't be applied to the wrong place by accident. Both are fine. Pick one per repository.
Stop typing -n on every command#
Your kubeconfig context remembers a namespace. Change it for the context you're in right now:
kubectl config set-context --current --namespace=team-a
kubectl config view --minify | grep namespace:
The second line prints namespace: team-a, and from here on kubectl get pods means team-a's pods. You could build a whole new context with --cluster and --user spelled out, but --current edits the context you already use and doesn't care which distribution you run.
Build one habit now. Before anything destructive, check where you're pointed, because a kubectl delete with no -n runs against whatever namespace the context names.
Reaching a Service in another namespace#
Every Service gets a DNS name of the form <service>.<namespace>.svc.cluster.local. A Pod's resolver searches its own namespace first, so the short name web only finds a Service called web in the same namespace as the Pod asking. That's a feature: the same config file works unchanged in a dev, staging and production namespace. Crossing over takes the namespace in the name.
Prove it from team-b. The DNS debugging page in the official docs uses an agnhost test image that ships nslookup; here's a Pod built on it:
apiVersion: v1
kind: Pod
metadata:
name: dnsutils
namespace: team-b
spec:
containers:
- name: dnsutils
image: registry.k8s.io/e2e-test-images/agnhost:2.39
imagePullPolicy: IfNotPresent
restartPolicy: Always
kubectl apply -f dnsutils.yaml
kubectl exec -i -t dnsutils -n team-b -- nslookup web
kubectl exec -i -t dnsutils -n team-b -- nslookup web.team-a
The first lookup fails. There's no Service named web in team-b, and the resolver expanded the name to web.team-b.svc.cluster.local. The second one resolves to the ClusterIP of team-a's Service. The reason sits in the Pod's /etc/resolv.conf, which the kubelet writes with a search line that starts with team-b.svc.cluster.local. In application config, the full form web.team-a.svc.cluster.local is the least ambiguous choice. The Kubernetes Service chapter covers what sits behind that name.
Delete the test Pod before moving on; the next section counts Pods in team-b, and you want that count to start at zero:
kubectl delete pod dnsutils -n team-b
Give a namespace a budget: ResourceQuota and LimitRange#
These two objects are why a namespace is useful for sharing a cluster. They do different jobs:
- A ResourceQuota caps the total for the whole namespace: summed CPU and memory requests and limits across its Pods, plus counts of objects such as Pods, Services or Secrets.
- A LimitRange works per container (min, max and, most usefully, a default request and limit for containers that don't set their own).
Order matters here. Once a quota covers cpu or memory, every new Pod in that namespace has to state requests or limits for them, or the control plane may refuse it. A plain kubectl run sets neither. The LimitRange fills them in at admission, so apply it first. Save both objects in one file, team-b-budget.yaml:
apiVersion: v1
kind: LimitRange
metadata:
name: container-defaults
namespace: team-b
spec:
limits:
- default:
cpu: 500m
memory: 256Mi
defaultRequest:
cpu: 100m
memory: 128Mi
type: Container
---
apiVersion: v1
kind: ResourceQuota
metadata:
name: team-b-budget
namespace: team-b
spec:
hard:
requests.cpu: "1"
requests.memory: 1Gi
limits.cpu: "2"
limits.memory: 2Gi
pods: "3"
kubectl apply -f team-b-budget.yaml
kubectl describe namespace team-b
describe namespace shows both objects: the quota, and a Resource Limits block with your defaults. Now try to start four Pods when the quota allows three:
for i in 1 2 3 4; do kubectl run web-$i --image=nginx -n team-b; done
Three are created. The fourth comes back with 403 Forbidden and a message naming the quota and the limit it would break. Check what the LimitRange did to one that got in:
kubectl get pod web-1 -n team-b -o jsonpath='{.spec.containers[0].resources}'
kubectl describe quota team-b-budget -n team-b
The Pod carries the 100m/128Mi requests and 500m/256Mi limits you never typed, and the quota's Used column now reads 3 pods. Both objects act only when something is admitted. Pods that were already running when you added or changed them keep their old settings.
Deployments behave differently, and it catches people out. A Deployment that asks for more than the quota allows is created without complaint; it just can't get all of its Pods to exist. When one in a quota'd namespace stays short of replicas, kubectl describe deployment is where the quota shows up.
A namespace on its own isn't a security boundary#
The DNS test above already showed it: a Pod in team-b could look up a Service in team-a, and nothing would stop it connecting either. By default a Pod accepts traffic from anywhere and can send it anywhere. Namespaces give you a place to hang the two features that do isolate, and you have to add both yourself.
Network isolation comes from NetworkPolicy. This one, from the official examples, blocks all incoming traffic to every Pod in the namespace it's applied to:
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: default-deny-ingress
spec:
podSelector: {}
policyTypes:
- Ingress
From there you add policies that allow specific sources, and a namespaceSelector matching kubernetes.io/metadata.name: team-a is how you admit one namespace by name. Policies are enforced by the network plugin, and a plugin without NetworkPolicy support accepts the object and enforces nothing. Check yours before you trust it.
Who can do what is the job of RBAC. A Role always lives in one namespace and grants permissions only there; a RoleBinding hands a Role's permissions to users, groups or service accounts, inside that one namespace. A RoleBinding can also point at a ClusterRole, which reuses one shared set of rules but still only grants them inside the binding's namespace. Only a ClusterRoleBinding reaches across the whole cluster. Giving a team "their namespace" in practice means a RoleBinding in it, and none elsewhere.
Pod Security labels on a namespace#
The third control lives on the namespace object itself. Pod Security admission, stable since Kubernetes 1.25, reads labels of the form pod-security.kubernetes.io/<MODE>: <LEVEL>. MODE is enforce (violating Pods are rejected), audit (an annotation in the audit log) or warn (a warning to the user). LEVEL is one of the Pod Security Standards: privileged, baseline or restricted. An optional <MODE>-version label pins the rules to a Kubernetes minor version.
Test before you enforce. A server-side dry run evaluates the Pods already in the namespace and reports what would break, without changing anything:
kubectl label --dry-run=server --overwrite ns team-a \
pod-security.kubernetes.io/enforce=restricted
If that prints warnings about the web Pods, the dry run is doing its job; those Pods were created with no security settings at all. A sensible setting for this namespace is to enforce baseline and only warn about restricted:
kubectl label --overwrite ns team-a \
pod-security.kubernetes.io/enforce=baseline \
pod-security.kubernetes.io/enforce-version=v1.37 \
pod-security.kubernetes.io/warn=restricted \
pod-security.kubernetes.io/warn-version=v1.37
The same labels can go straight into a Namespace manifest under metadata.labels, which is the better home for them once you've settled on a level.
Deleting a namespace, and one that won't go away#
Deleting a namespace deletes everything inside it. There's no confirmation prompt and no undo:
kubectl delete namespace team-b
kubectl get namespace team-b
Deletion is asynchronous. For a while the namespace shows Terminating, and it won't accept new objects in that phase. Then it's gone.
When it sits in Terminating and doesn't leave, check finalizers first. A finalizer is a key that tells the API server to hold an object until some controller has finished its cleanup and removed the key. If that controller is gone (say, the operator that added the key was uninstalled), nobody removes it. The tasks page gives the simplest version of this: create a namespace with a finalizer that doesn't exist, and it'll get stuck in Terminating when you try to delete it. Start by reading the namespace:
kubectl get namespace team-b -o yaml
Look at spec.finalizers and metadata.finalizers, and read status.conditions, where the namespace controller records what it's waiting on. Then find what's still inside:
kubectl api-resources --verbs=list --namespaced -o name \
| xargs -n 1 kubectl get --show-kind --ignore-not-found -n team-b
Whatever remains usually carries a finalizer of its own, and the docs suggest checking finalizers and owner references on both the stuck object and its dependents. Fix the thing that should remove the finalizer, for instance by bringing the controller back. The official guidance is to avoid deleting finalizers by hand unless you understand what each one was protecting and have done that cleanup another way.
Clean up the rest of the walkthrough and point your context back:
kubectl delete namespace team-a
kubectl config set-context --current --namespace=default
Skip the second line and every command afterwards goes to a namespace that no longer exists.
Common mistakes#
- "Not found" for an object you just created. It's in another namespace;
kubectl get <kind> -Afinds it. - Short Service names in config that crosses namespaces.
webresolves only next door,web.team-a.svc.cluster.localresolves everywhere. - A CPU or memory quota with no LimitRange, then Pods that never start because nobody set requests.
- Treating a namespace as isolation. Without NetworkPolicy and RBAC, it's a naming scope with a budget.
- One namespace per app version. Labels do that job; the docs say so directly.
- Names starting with
kube-, or names that match a public top-level domain. - Stripping finalizers to make a stuck namespace go away, and skipping the cleanup each one was there to guarantee.
Next in the book#
Namespaces show up in nearly every chapter from here on, starting with ConfigMaps, which are namespaced too: a Pod can only mount a ConfigMap from its own namespace. For everything this chapter touched, the namespace walkthrough on kubernetes.io is the reference to keep open.
Further watching#
- Video by Anais Urlichs (the original 100 Days of Kubernetes series)