CKS — Namespace Isolation
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 (nonamespaceSelectormeans 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
namespaceSelectoris 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-alphacan talk to each other - Pods in
tenant-alphacannot reach pods intenant-betaortenant-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:
namespaceSelectormatches namespaces with the labeltier: frontendportsrestricts 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.