Ch. 14 · Kubernetes

Kubernetes Namespaces and Isolation Limits

What a namespace scopes (names, RBAC, quotas, policies) and what it leaves open: network traffic, nodes, cluster-scoped objects and DNS.

~6 min readintermediateupdated Oct 7, 2026

Ask a room of engineers what a Kubernetes namespace gives you and most will say “isolation”. That answer is half right, and interviewers like it because the wrong half is where the interesting follow-up questions live. A namespace is a scope: object names are unique within it, and many policies attach to it. What it does not do by itself is stop a Pod in team-a from opening a TCP connection to a Pod in team-b, or stop them from running on the same node.

Examples assume Kubernetes 1.30 or later. Command output is illustrative of what kubectl prints, not captured from a particular cluster.

What a namespace actually scopes

Start with names. Two Deployments called api can exist in payments and search because the full identity of a namespaced object is namespace plus kind plus name. That is the most basic thing a namespace does, and it’s genuinely useful: teams can pick names without coordinating.

The second thing is that a lot of policy objects are themselves namespaced and apply only to their own namespace:

  • RBAC: a Role and RoleBinding grant permissions inside one namespace.
  • ResourceQuota: caps total CPU, memory, object counts or storage requested in the namespace.
  • LimitRange: sets default and maximum requests and limits for containers created there.
  • NetworkPolicy: selects Pods in its own namespace and restricts their traffic.
  • Pod Security Admission: configured with labels on the Namespace object, such as pod-security.kubernetes.io/enforce: restricted.
  • ServiceAccounts, Secrets and ConfigMaps: a Pod can only mount Secrets and ConfigMaps from its own namespace and run as a ServiceAccount from its own namespace.

That last point is easy to overlook and is a real boundary. A Pod in search can’t reference payments/db-credentials in a volume or envFrom; the API server rejects the reference, or the kubelet fails to find it, because the lookup is always in the Pod’s namespace.

You can see which kinds are namespaced at all:

$ kubectl api-resources --namespaced=false | head
NAME                 SHORTNAMES   APIVERSION                     NAMESPACED   KIND
namespaces           ns           v1                             false        Namespace
nodes                no           v1                             false        Node
persistentvolumes    pv           v1                             false        PersistentVolume
clusterroles                      rbac.authorization.k8s.io/v1   false        ClusterRole
storageclasses       sc           storage.k8s.io/v1              false        StorageClass
customresourcedefinitions  crd    apiextensions.k8s.io/v1        false        CustomResourceDefinition
Terminal

Everything in that list lives outside any namespace. If a team can create PersistentVolumes or CRDs, they’re changing something every other team shares, and no RoleBinding in their namespace could have granted or restricted that.

What it leaves wide open

Here’s where the “isolation” answer goes wrong.

The network is flat. By default, Kubernetes networking lets every Pod reach every other Pod by IP, across namespaces. Service DNS makes it even easier: api.payments.svc.cluster.local resolves from any namespace. The short name api resolves to the Service in the caller’s own namespace because of the DNS search path in /etc/resolv.conf, which is why people think namespaces separate traffic. They separate name resolution defaults, not reachability.

Nodes are shared. The scheduler places Pods from all namespaces on the same nodes unless you add taints, tolerations or node affinity. A noisy neighbour in batch can compete for disk I/O and network bandwidth with payments on the same machine, and a container escape on that node exposes every Pod there.

Cluster-scoped privileges cut across. A ClusterRoleBinding to cluster-admin ignores namespaces entirely. So does anything that can create Pods with hostPath volumes or privileged: true, because from the node a container can see other namespaces’ data.

Some objects are deliberately cross-namespace. An ExternalName Service or an ingress controller can route to Services in other namespaces. A Gateway API HTTPRoute can reference a backend in another namespace if a ReferenceGrant allows it.

Gotcha

Deleting a namespace deletes everything in it, and the operation can hang in Terminating if a finalizer can’t complete (often a CRD whose controller was removed first). Namespaces are a convenient blast radius for cleanup, which is also why kubectl delete ns deserves a second look.

Building a tenant boundary out of parts

Suppose two teams share a cluster and you’ve been asked to make team-a a reasonable boundary. Each piece below closes one of the gaps.

First, label the namespace so Pod Security Admission rejects privileged Pods:

apiVersion: v1
kind: Namespace
metadata:
  name: team-a
  labels:
    pod-security.kubernetes.io/enforce: restricted
    pod-security.kubernetes.io/enforce-version: latest
yaml

Then cap what the namespace can consume, so one team can’t starve the other of schedulable capacity:

apiVersion: v1
kind: ResourceQuota
metadata:
  name: compute
  namespace: team-a
spec:
  hard:
    requests.cpu: "20"
    requests.memory: 40Gi
    limits.memory: 60Gi
    pods: "100"
    persistentvolumeclaims: "20"
---
apiVersion: v1
kind: LimitRange
metadata:
  name: defaults
  namespace: team-a
spec:
  limits:
    - type: Container
      defaultRequest:
        cpu: 100m
        memory: 128Mi
      default:
        memory: 256Mi
yaml

The LimitRange matters because a quota on requests.cpu makes the API server reject any Pod that doesn’t set a CPU request. Defaults fill them in so teams aren’t surprised by failed quota: compute: must specify requests.cpu.

Next, make the network match the namespace. This policy lets Pods in team-a receive traffic only from other Pods in team-a:

apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
  name: same-namespace-only
  namespace: team-a
spec:
  podSelector: {}
  policyTypes: ["Ingress"]
  ingress:
    - from:
        - podSelector: {}
yaml

An empty podSelector inside from means “Pods in this policy’s namespace”, which is exactly the boundary you want. It only works if the cluster’s CNI plugin enforces NetworkPolicy (Calico, Cilium and others do; some basic setups don’t).

Finally, give the team admin rights inside the namespace and nothing outside it, using the built-in admin ClusterRole bound with a namespaced RoleBinding:

apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata:
  name: team-a-admins
  namespace: team-a
subjects:
  - kind: Group
    name: team-a
    apiGroup: rbac.authorization.k8s.io
roleRef:
  kind: ClusterRole
  name: admin
  apiGroup: rbac.authorization.k8s.io
yaml

Binding a ClusterRole through a RoleBinding is a common pattern: the ClusterRole is a reusable template, and the RoleBinding limits its effect to one namespace. Interviewers often ask whether that grants cluster-wide access. It doesn’t.

You can verify the RBAC part without touching any workload:

$ kubectl auth can-i create deployments -n team-a --as=jane --as-group=team-a
yes
$ kubectl auth can-i create deployments -n team-b --as=jane --as-group=team-a
no
$ kubectl auth can-i create persistentvolumes --as=jane --as-group=team-a
no
Terminal

When a namespace isn’t enough

Even with all of that, both teams share a control plane, an API server rate limit, cluster-scoped CRDs and the kernel on each node. That’s “soft” multi-tenancy, fine for teams in one company who trust each other not to be hostile. For tenants you don’t trust (customers running their own code, say), the usual answers are dedicated node pools with taints so tenants never share a kernel, sandboxed runtimes such as gVisor or Kata Containers, virtual control planes, or simply separate clusters. Saying “separate clusters” in an interview isn’t a cop-out; it’s often the right call when compliance requires hard isolation.

A related mistake is the opposite one: creating a namespace per microservice. That multiplies quotas, RBAC bindings and policies to maintain without adding isolation the services need. Namespaces map well to teams, environments or tenants, the things that need different permissions and budgets.

Interview practice

“Your company runs staging and production as two namespaces in one cluster. A developer says this is safe because namespaces isolate environments. What would you check, and what would you change?”

Answer

I’d start by testing the claim. From a staging Pod, try curl http://api.production.svc.cluster.local; with no NetworkPolicies it will connect, so staging traffic or a misconfigured staging client can hit production. Then I’d look at RBAC: kubectl get clusterrolebindings -o wide to find anyone with cluster-wide roles, and kubectl auth can-i --list -n production --as=<staging-deployer> to confirm staging’s CI identity can’t write to production. I’d check whether the namespaces have quotas, because a runaway staging load test can consume the cluster’s capacity and leave production Pods Pending. And I’d check node sharing: staging and production Pods probably run side by side.

To change it: default-deny NetworkPolicies in production with explicit allows, separate service accounts and RoleBindings per environment, ResourceQuotas on staging, Pod Security Admission restricted on both, and ideally a tainted node pool for production. If the organisation needs staging upgrades of Kubernetes itself to be tested before production, that’s an argument for separate clusters, because a namespace can’t run a different Kubernetes version.

Further reading

More in Kubernetes

read ✓Kubernetes · mid

Kubernetes RBAC and Least-Privilege Access

Build a least-privilege Role for a CI deployer, verify it with kubectl auth can-i, and spot grants like pods/exec that quietly escalate.

~6 min readread →
esc