KubernetesXIV · Namespace ArchitectureTenancy and isolation
NetworkPolicy per namespace — microsegmentation at the cluster level
What you'll learn
- Author NetworkPolicies that isolate namespaces
- Distinguish default-allow vs default-deny models
- Reason about ingress and egress rules and their interactions
- Apply NetworkPolicy patterns for production multi-tenancy
Prerequisites
Verified against Kubernetes 1.34.x · kubeadm 1.34.x · kubectl 1.34.x · etcd 3.6.x · CoreDNS 1.11.x · containerd 1.7.x / 2.x · 2026-08-16
NetworkPolicy is the in-cluster network firewall that turns namespace isolation from name-prefix to network-enforced isolation. This lesson covers the policy model, the default-allow vs default-deny choices, and the production patterns.
The NetworkPolicy model
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: team-a-isolation
namespace: team-a-prod
spec:
podSelector: {} # applies to all Pods in this namespace
policyTypes:
- Ingress
- Egress
ingress:
- from:
- namespaceSelector:
matchLabels:
kubernetes.io/metadata.name: team-a-prod
- podSelector: {} # or from same namespace
ports:
- protocol: TCP
port: 8080
egress:
- to:
- namespaceSelector:
matchLabels:
kubernetes.io/metadata.name: kube-system
ports:
- protocol: UDP
port: 53 # DNS
The policy:
- Applies to all Pods in
team-a-prod(emptypodSelectormatches everything). - Ingress: allow TCP traffic on port 8080 from Pods in
team-a-prod. - Egress: allow UDP traffic on port 53 to Pods in
kube-system(DNS).
A Pod without matching ingress or egress rules cannot receive or send the corresponding traffic.
Default-allow vs default-deny
Two models:
- Default-allow: every Pod can talk to every other Pod unless a NetworkPolicy explicitly denies. The cluster’s default. No NetworkPolicy = all traffic allowed.
- Default-deny: every Pod is isolated unless a NetworkPolicy explicitly allows. Achieved by creating a policy that selects all Pods and denies all traffic.
flowchart LR
Default[Default-allow<br/>no policy] --> Allow[All traffic allowed]
Policy1[NetworkPolicy A] --> Allow[A overrides default]
Policy2[NetworkPolicy B] --> Deny[B explicitly denies]
DefaultDeny[Default-deny policy] --> Restrict[All denied by default]
Policy3[NetworkPolicy C] --> Allow2[C explicitly allows]
Default-allow is simpler (no policy = open); default-deny is safer (no policy = closed). Production discipline: choose default-deny per namespace; explicitly allow what is needed.
Creating a default-deny policy
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: default-deny-all
namespace: team-a-prod
spec:
podSelector: {} # applies to all Pods
policyTypes:
- Ingress
- Egress
This policy:
- Selects all Pods in
team-a-prod. - Sets policyTypes to Ingress and Egress.
- Has no rules under
ingressoregress— meaning no traffic is allowed.
The result: every Pod in team-a-prod is denied all
ingress and egress. Any explicit allow policy takes
precedence.
Adding allow policies
# Allow Pods in team-a-prod to talk to each other
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: allow-internal
namespace: team-a-prod
spec:
podSelector: {}
policyTypes:
- Ingress
- Egress
ingress:
- from:
- podSelector: {} # from same namespace
egress:
- to:
- podSelector: {} # to same namespace
This adds to the default-deny policy: Pods in team-a-prod
can communicate with each other. Other namespaces are still
blocked.
Production patterns
Isolated namespace with internal + DNS:
# Default deny
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: default-deny-all
namespace: team-a-prod
spec:
podSelector: {}
policyTypes: [Ingress, Egress]
---
# Allow internal traffic
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: allow-internal
namespace: team-a-prod
spec:
podSelector: {}
policyTypes: [Ingress, Egress]
ingress:
- from:
- podSelector: {}
egress:
- to:
- podSelector: {}
---
# Allow DNS
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: allow-dns
namespace: team-a-prod
spec:
podSelector: {}
policyTypes: [Egress]
egress:
- to:
- namespaceSelector:
matchLabels:
kubernetes.io/metadata.name: kube-system
ports:
- protocol: UDP
port: 53
- protocol: TCP
port: 53
---
# Allow egress to specific external services (e.g., a database)
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: allow-db
namespace: team-a-prod
spec:
podSelector:
matchLabels:
app: web
policyTypes: [Egress]
egress:
- to:
- namespaceSelector:
matchLabels:
kubernetes.io/metadata.name: databases
ports:
- protocol: TCP
port: 5432
This stack of policies:
- Default deny all.
- Allow internal (same-namespace) traffic.
- Allow DNS to kube-system.
- Allow web Pods to reach the database in the
databasesnamespace.
Production discipline: layer policies; each policy is small and focused.
Egress and external traffic
Default-deny blocks egress to the internet (and to other cluster services). To allow external traffic, add explicit egress rules:
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: allow-external-https
namespace: team-a-prod
spec:
podSelector:
matchLabels:
app: external-client
policyTypes: [Egress]
egress:
- ports:
- protocol: TCP
port: 443
This allows the Pods labelled app=external-client to
connect to any IP on TCP port 443. Production discipline:
limit by destination CIDR when possible:
egress:
- to:
- ipBlock:
cidr: 0.0.0.0/0 # any IP
except:
- 10.0.0.0/8 # except cluster internal
ports:
- protocol: TCP
port: 443
The ipBlock field allows CIDR-based rules; the except
field excludes specific CIDRs.
Auditing NetworkPolicy
# List all NetworkPolicies in a namespace
kubectl get networkpolicy -n team-a-prod
# List all namespaces with NetworkPolicy
kubectl get networkpolicy -A
# Find namespaces without any NetworkPolicy
for ns in $(kubectl get ns -o jsonpath='{.items[*].metadata.name}'); do
count=$(kubectl get networkpolicy -n $ns -o jsonpath='{.items}' | jq length)
if [ "$count" -eq "0" ]; then
echo "No policies: $ns"
fi
done
Production discipline: every production namespace should have a default-deny policy. Namespaces without policies are vulnerable to lateral movement.
Cross-course references
- The Linux course part
XXV-Linux-Firewallcovers iptables/nftables; NetworkPolicy is the cluster-level equivalent. - The VyOS course part
XXXVII-VyOS-Firewallcovers network firewall rules; NetworkPolicy is the cluster-level equivalent. - The Docker course part
LI-Docker-Firewallscovers container firewall patterns; NetworkPolicy is the cluster-level extension.
Quiz
Knowledge check · 4 questions
Q1. What is the right way to enforce default-deny on a namespace?
Q2. Under a default-deny NetworkPolicy, Pods can still reach the cluster DNS service without explicit configuration.
Q3. An audit reveals that namespace `team-a-prod` has no NetworkPolicy. A compromised Pod in another namespace can reach the production database. Walk through the fix.
Cluster with three namespaces: `team-a-prod` (no NetworkPolicy), `team-b-staging` (no NetworkPolicy), `databases`. A compromised Pod in `team-b-staging` reaches the PostgreSQL database in `databases` via the database Service IP. Production database access from staging is a lateral movement vector.
Q4. How do multiple NetworkPolicies interact when they select the same Pod?
Passing score: 75%. Answers are checked in this browser.
Production discipline
- Default-deny per namespace. Every production namespace has a default-deny NetworkPolicy.
- Allow DNS explicitly. Default-deny blocks DNS; Pods need name resolution.
- Layer policies. Each policy is small and focused; combine for the complete traffic model.
- Audit NetworkPolicies regularly. Namespaces without policies are vulnerable to lateral movement.
- Verify the CNI supports NetworkPolicy. Flannel does not enforce; Cilium, Calico do.