CKS — Namespace Isolation

8 minute read

Isolating Namespaces with NetworkPolicies

In a multi-tenant cluster, namespaces represent trust boundaries. Teams, environments, and applications are separated into different namespaces. But by default, Kubernetes allows all cross-namespace traffic — a pod in team-a can freely reach pods in team-b.

Namespace isolation with NetworkPolicies ensures that:

  • Pods can only communicate with pods in namespaces they are explicitly allowed to reach
  • Compromising one namespace does not give access to others
  • Multi-tenant clusters enforce genuine network isolation

Labeling Namespaces for Policy Matching

NetworkPolicies use namespaceSelector to match namespaces by labels. Before writing namespace isolation policies, you need to label your namespaces.

Built-in Label

Kubernetes 1.22+ automatically adds a label to every namespace:

kubernetes.io/metadata.name: <namespace-name>

You can use this immediately without manual labeling:

kubectl get namespace --show-labels

Custom Labels

For more flexible matching, add custom labels:

# Label by team
kubectl label namespace team-a team=team-a
kubectl label namespace team-b team=team-b

# Label by environment
kubectl label namespace production environment=production
kubectl label namespace staging environment=staging

# Label by tier
kubectl label namespace frontend tier=frontend
kubectl label namespace backend tier=backend
kubectl label namespace database tier=database

Verify Labels

kubectl get namespaces --show-labels | grep -E "team|tier|environment"

Preventing All Cross-Namespace Traffic

Start with default deny, then use namespaceSelector to restrict which namespaces can communicate.

Step 1: Default Deny in Each Namespace

apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
  name: default-deny-all
  namespace: team-a
spec:
  podSelector: {}
  policyTypes:
    - Ingress
    - Egress

Apply the same policy to team-b, team-c, etc.

Step 2: Allow Only Same-Namespace Traffic

apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
  name: allow-same-namespace
  namespace: team-a
spec:
  podSelector: {}
  policyTypes:
    - Ingress
    - Egress
  ingress:
    - from:
        - podSelector: {}
  egress:
    - to:
        - podSelector: {}
    - to:
        - namespaceSelector: {}
          podSelector:
            matchLabels:
              k8s-app: kube-dns
      ports:
        - protocol: UDP
          port: 53
        - protocol: TCP
          port: 53

What This Does

  • ingress.from[].podSelector: {} — allows ingress from any pod in the same namespace (no namespaceSelector means same namespace only)
  • egress.to[].podSelector: {} — allows egress to any pod in the same namespace
  • DNS egress is included so pods can still resolve service names
  • Cross-namespace traffic is blocked — no namespaceSelector is used in the allow rules (except for DNS)

Allowing Specific Namespace-to-Namespace Traffic

In a typical microservices architecture:

  • frontend → backend (allowed)
  • backend → database (allowed)
  • frontend → database (denied — frontend should never directly access the database)

Setup: Label the Namespaces

kubectl create namespace frontend
kubectl create namespace backend
kubectl create namespace database

kubectl label namespace frontend tier=frontend
kubectl label namespace backend tier=backend
kubectl label namespace database tier=database

Policy: Allow Frontend → Backend (Ingress Side)

Applied in the backend namespace — allow ingress from frontend:

apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
  name: allow-from-frontend
  namespace: backend
spec:
  podSelector: {}
  policyTypes:
    - Ingress
  ingress:
    - from:
        - namespaceSelector:
            matchLabels:
              tier: frontend

Policy: Allow Frontend → Backend (Egress Side)

Applied in the frontend namespace — allow egress to backend:

apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
  name: allow-to-backend
  namespace: frontend
spec:
  podSelector: {}
  policyTypes:
    - Egress
  egress:
    - to:
        - namespaceSelector:
            matchLabels:
              tier: backend
    - to:
        - namespaceSelector: {}
          podSelector:
            matchLabels:
              k8s-app: kube-dns
      ports:
        - protocol: UDP
          port: 53
        - protocol: TCP
          port: 53

Policy: Allow Backend → Database (Ingress Side)

Applied in the database namespace:

apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
  name: allow-from-backend
  namespace: database
spec:
  podSelector: {}
  policyTypes:
    - Ingress
  ingress:
    - from:
        - namespaceSelector:
            matchLabels:
              tier: backend
      ports:
        - protocol: TCP
          port: 5432

This allows only TCP port 5432 (PostgreSQL) from the backend namespace. Even if someone in backend tries to SSH or HTTP to the database pods, it is denied.

Policy: Allow Backend → Database (Egress Side)

Applied in the backend namespace:

apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
  name: allow-to-database
  namespace: backend
spec:
  podSelector: {}
  policyTypes:
    - Egress
  egress:
    - to:
        - namespaceSelector:
            matchLabels:
              tier: database
      ports:
        - protocol: TCP
          port: 5432
    - to:
        - namespaceSelector: {}
          podSelector:
            matchLabels:
              k8s-app: kube-dns
      ports:
        - protocol: UDP
          port: 53
        - protocol: TCP
          port: 53

kube-system Namespace Considerations

The kube-system namespace runs critical cluster services:

  • CoreDNS — every pod needs DNS
  • kube-proxy — service routing
  • Metrics server — monitoring
  • CNI pods — networking itself

Do NOT Apply Aggressive Default Deny to kube-system

Blocking traffic to/from kube-system can break:

  • DNS resolution (CoreDNS)
  • Service discovery
  • Cluster monitoring
  • Node-to-control-plane communication

Safe Pattern: Allow DNS From All Namespaces

If you must apply policies to kube-system, ensure DNS remains accessible:

apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
  name: allow-dns-from-all
  namespace: kube-system
spec:
  podSelector:
    matchLabels:
      k8s-app: kube-dns
  policyTypes:
    - Ingress
  ingress:
    - from:
        - namespaceSelector: {}
      ports:
        - protocol: UDP
          port: 53
        - protocol: TCP
          port: 53

This allows DNS queries from any namespace to CoreDNS, but blocks other traffic to CoreDNS pods on non-DNS ports.


Multi-Tenant Namespace Isolation Pattern

For clusters hosting multiple tenants, each tenant gets a namespace and a label. Tenants are completely isolated from each other.

Step 1: Create and Label Tenant Namespaces

kubectl create namespace tenant-alpha
kubectl create namespace tenant-beta
kubectl create namespace tenant-gamma

kubectl label namespace tenant-alpha tenant=alpha
kubectl label namespace tenant-beta tenant=beta
kubectl label namespace tenant-gamma tenant=gamma

Step 2: Default Deny All in Each Tenant Namespace

for ns in tenant-alpha tenant-beta tenant-gamma; do
  cat <<EOF | kubectl apply -f -
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
  name: default-deny-all
  namespace: $ns
spec:
  podSelector: {}
  policyTypes:
    - Ingress
    - Egress
EOF
done

Step 3: Allow Intra-Namespace Traffic + DNS

apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
  name: allow-same-namespace-and-dns
  namespace: tenant-alpha
spec:
  podSelector: {}
  policyTypes:
    - Ingress
    - Egress
  ingress:
    - from:
        - podSelector: {}
  egress:
    - to:
        - podSelector: {}
    - to:
        - namespaceSelector: {}
          podSelector:
            matchLabels:
              k8s-app: kube-dns
      ports:
        - protocol: UDP
          port: 53
        - protocol: TCP
          port: 53

Apply the same pattern to each tenant namespace. Now:

  • Pods within tenant-alpha can talk to each other
  • Pods in tenant-alpha cannot reach pods in tenant-beta or tenant-gamma
  • DNS still works

namespaceSelector vs podSelector — Selector Scope

Understanding scope is critical:

Selector Where It Matches
podSelector: {} alone (no namespaceSelector) All pods in the same namespace as the policy
namespaceSelector: {} alone All pods in all namespaces
namespaceSelector + podSelector (same from/to entry) Pods matching the podSelector in namespaces matching the namespaceSelector
namespaceSelector and podSelector in separate from/to entries pods in matching namespaces OR pods matching selector in same namespace

AND vs OR — Critical Distinction

AND (same object — both conditions must match):

ingress:
  - from:
      - namespaceSelector:
          matchLabels:
            tier: frontend
        podSelector:
          matchLabels:
            app: web

This allows ingress from pods labeled app: web AND in namespaces labeled tier: frontend.

OR (separate list items — either condition matches):

ingress:
  - from:
      - namespaceSelector:
          matchLabels:
            tier: frontend
      - podSelector:
          matchLabels:
            app: web

This allows ingress from any pod in namespaces labeled tier: frontend OR from pods labeled app: web in the same namespace. These are two separate rules under the same from list — either one matching is sufficient.

This is one of the most common mistakes on the CKS exam. Pay close attention to YAML indentation.


Practice Questions

Question 1: Write a NetworkPolicy in the `backend` namespace that allows ingress only from pods in namespaces labeled `tier: frontend` on port 8080.
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
  name: allow-frontend-ingress
  namespace: backend
spec:
  podSelector: {}
  policyTypes:
    - Ingress
  ingress:
    - from:
        - namespaceSelector:
            matchLabels:
              tier: frontend
      ports:
        - protocol: TCP
          port: 8080

Key points:

  • namespaceSelector matches namespaces with the label tier: frontend
  • ports restricts to TCP 8080 only
  • This must be combined with a default deny policy to actually block other traffic
Question 2: What is the difference between these two ingress rules?

Rule A:

from:
  - namespaceSelector:
      matchLabels:
        env: prod
    podSelector:
      matchLabels:
        app: api

Rule B:

from:
  - namespaceSelector:
      matchLabels:
        env: prod
  - podSelector:
      matchLabels:
        app: api

Rule A is an AND — both conditions must be true. It allows traffic from pods labeled app: api that are in namespaces labeled env: prod.

Rule B is an OR — either condition is sufficient. It allows traffic from any pod in namespaces labeled env: prod, OR from pods labeled app: api in the policy’s own namespace.

The difference is YAML indentation. Rule A has namespaceSelector and podSelector as siblings in the same list item. Rule B has them as separate list items.

Question 3: How do you allow pods in namespace `monitoring` to scrape metrics from pods in all other namespaces on port 9090?

In each namespace that has pods to be scraped, apply:

apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
  name: allow-monitoring-scrape
  namespace: <target-namespace>
spec:
  podSelector: {}
  policyTypes:
    - Ingress
  ingress:
    - from:
        - namespaceSelector:
            matchLabels:
              kubernetes.io/metadata.name: monitoring
      ports:
        - protocol: TCP
          port: 9090

This allows ingress from the monitoring namespace on TCP 9090 only. The kubernetes.io/metadata.name label is automatically set by Kubernetes.

Question 4: You applied a "allow same namespace only" policy but pods can still receive traffic from other namespaces. What went wrong?

Most likely, there is no default deny policy in the namespace. NetworkPolicies are additive — they only allow additional traffic. Without a default deny policy, all traffic is already allowed, and adding an “allow same namespace” policy has no restrictive effect.

You need to first apply a default deny ingress (or default deny all) policy, then add the allow policy.

Question 5: In a multi-tenant cluster, tenant `alpha` needs to allow its pods to call an API service in the `shared-services` namespace. Write the egress policy for tenant `alpha` and the ingress policy for `shared-services`.

Egress policy in tenant-alpha:

apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
  name: allow-egress-to-shared-api
  namespace: tenant-alpha
spec:
  podSelector: {}
  policyTypes:
    - Egress
  egress:
    - to:
        - namespaceSelector:
            matchLabels:
              kubernetes.io/metadata.name: shared-services
          podSelector:
            matchLabels:
              app: api-gateway
      ports:
        - protocol: TCP
          port: 443
    - to:
        - namespaceSelector: {}
          podSelector:
            matchLabels:
              k8s-app: kube-dns
      ports:
        - protocol: UDP
          port: 53
        - protocol: TCP
          port: 53

Ingress policy in shared-services:

apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
  name: allow-ingress-from-tenants
  namespace: shared-services
spec:
  podSelector:
    matchLabels:
      app: api-gateway
  policyTypes:
    - Ingress
  ingress:
    - from:
        - namespaceSelector:
            matchLabels:
              tenant: alpha
      ports:
        - protocol: TCP
          port: 443

Both egress and ingress must be explicitly allowed when default deny is in place on both namespaces.

Updated: