Module 13 — Network Policies
Overview
Network Policies are Kubernetes firewalls — they control which pods can talk to which. By default, all pods can communicate with all other pods. Network Policies restrict that. The CKA exam heavily tests creating, combining, and debugging Network Policies. This module covers the full spec, default deny patterns, policy combination logic, and troubleshooting.
1. Network Policy Fundamentals
1.1 Default Behavior
Without any Network Policy, Kubernetes allows all traffic between all pods:
| ┌─────────┐ ┌─────────┐ ┌─────────┐
│ Pod A │◀───▶│ Pod B │◀───▶│ Pod C │
└─────────┘ └─────────┘ └─────────┘
▲ ▲ ▲
└───────────────┴───────────────┘
ALL traffic allowed
|
1.2 What Network Policies Do
A Network Policy selects pods and defines allowed ingress (incoming) and/or egress (outgoing) traffic. All other traffic to/from selected pods is denied.
| With NetworkPolicy on Pod B (allow ingress from Pod A only):
┌─────────┐ ┌─────────┐ ┌─────────┐
│ Pod A │────▶│ Pod B │ ✗ │ Pod C │
└─────────┘ └─────────┘ └─────────┘
(policy applied)
A → B: ✓ allowed
C → B: ✗ denied
B → anywhere: ✓ allowed (no egress policy)
|
1.3 Key Concepts
| Concept |
Detail |
| Additive |
Multiple policies are unioned — if ANY policy allows the traffic, it's allowed |
| Namespaced |
Network Policies apply within a namespace |
| Pod selector |
Selects which pods the policy applies TO |
| Ingress rules |
Define allowed INCOMING traffic |
| Egress rules |
Define allowed OUTGOING traffic |
| No explicit deny |
Unmatched traffic is denied implicitly once a policy selects a pod |
| CNI required |
The CNI plugin must support Network Policies (Calico ✅, Cilium ✅, Flannel ❌) |
CKA Tip: If the cluster uses Flannel, Network Policies are ignored — they're created but not enforced. The exam typically uses Calico.
2. NetworkPolicy Spec
2.1 Full Structure
| apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: my-policy
namespace: default
spec:
podSelector: # which pods this policy applies to
matchLabels:
app: web
policyTypes: # which directions are restricted
- Ingress
- Egress
ingress: # allowed incoming traffic
- from:
- podSelector: {} # who can send traffic
- namespaceSelector: {}
- ipBlock: {}
ports: # on which ports
- port: 80
protocol: TCP
egress: # allowed outgoing traffic
- to:
- podSelector: {}
- namespaceSelector: {}
- ipBlock: {}
ports:
- port: 53
protocol: UDP
|
2.2 podSelector — Who Is Affected
| # Apply to pods with label app=web
spec:
podSelector:
matchLabels:
app: web
# Apply to ALL pods in the namespace
spec:
podSelector: {}
|
2.3 policyTypes
| policyTypes |
Effect |
[Ingress] |
Restricts incoming traffic; egress is unrestricted |
[Egress] |
Restricts outgoing traffic; ingress is unrestricted |
[Ingress, Egress] |
Restricts both directions |
| Not specified |
Inferred from which rules are present |
CKA Tip: Always set policyTypes explicitly to avoid ambiguity.
2.4 Ingress Rules — from
Three types of sources can be combined:
podSelector — Pods in the Same Namespace
| ingress:
- from:
- podSelector:
matchLabels:
app: frontend
|
namespaceSelector — Pods in Other Namespaces
| ingress:
- from:
- namespaceSelector:
matchLabels:
env: production
|
Important: The namespace must have the label. Label namespaces with:
| kubectl label namespace production env=production
|
ipBlock — External IP Ranges
| ingress:
- from:
- ipBlock:
cidr: 192.168.1.0/24
except:
- 192.168.1.100/32 # exclude a specific IP
|
2.5 Egress Rules — to
Same three source types, but for outgoing traffic:
| egress:
- to:
- podSelector:
matchLabels:
app: database
ports:
- port: 3306
protocol: TCP
|
2.6 Ports
| ports:
- port: 80 # specific port
protocol: TCP # TCP (default), UDP, or SCTP
- port: 443
protocol: TCP
- port: 53
protocol: UDP
- port: 8000
endPort: 9000 # port range (8000-9000), K8s 1.25+
|
3. AND vs OR Logic — Critical for the Exam
3.1 The Rule
| Multiple items in the SAME "from" array entry → AND (all must match)
Multiple entries in the "from" array → OR (any can match)
|
3.2 OR — Separate Array Entries
| ingress:
- from:
- podSelector: # entry 1
matchLabels:
app: frontend
- namespaceSelector: # entry 2
matchLabels:
env: production
|
This means: allow from pods with app=frontend OR from any pod in namespaces with env=production.
| frontend pods (any namespace) → ALLOWED
any pod in production ns → ALLOWED
|
3.3 AND — Single Array Entry
| ingress:
- from:
- podSelector: # combined in ONE entry
matchLabels:
app: frontend
namespaceSelector:
matchLabels:
env: production
|
This means: allow from pods with app=frontend AND in namespaces with env=production.
| frontend pods in production ns → ALLOWED
frontend pods in staging ns → DENIED
random pods in production ns → DENIED
|
3.4 Visual Comparison
| # OR (two dashes = two entries)
- from:
- podSelector: # ← dash = entry 1
matchLabels:
app: frontend
- namespaceSelector: # ← dash = entry 2
matchLabels:
env: production
# AND (one dash = one entry with both selectors)
- from:
- podSelector: # ← dash = single entry
matchLabels:
app: frontend
namespaceSelector: # ← NO dash = same entry
matchLabels:
env: production
|
CKA Tip: This AND vs OR distinction is the #1 source of mistakes on Network Policy questions. Count the dashes carefully.
4. Default Deny Policies
4.1 Default Deny All Ingress
Block all incoming traffic to all pods in a namespace:
| apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: deny-all-ingress
namespace: default
spec:
podSelector: {} # selects ALL pods
policyTypes:
- Ingress # no ingress rules = deny all incoming
|
After applying, no pod in the namespace can receive traffic unless another policy explicitly allows it.
4.2 Default Deny All Egress
Block all outgoing traffic from all pods in a namespace:
| apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: deny-all-egress
namespace: default
spec:
podSelector: {}
policyTypes:
- Egress # no egress rules = deny all outgoing
|
Warning: This also blocks DNS (port 53). Pods won't be able to resolve Service names. You'll usually need to allow DNS egress — see Section 4.4.
4.3 Default Deny All Traffic (Both Directions)
| apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: deny-all
namespace: default
spec:
podSelector: {}
policyTypes:
- Ingress
- Egress
|
4.4 Deny All Egress + Allow DNS
The most practical default deny egress pattern:
| apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: deny-egress-allow-dns
namespace: default
spec:
podSelector: {}
policyTypes:
- Egress
egress:
- to: [] # allow to anywhere
ports:
- port: 53
protocol: UDP
- port: 53
protocol: TCP
|
4.5 Allow All Ingress (Explicit)
Undo a default deny by allowing all ingress:
| apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: allow-all-ingress
namespace: default
spec:
podSelector: {}
policyTypes:
- Ingress
ingress:
- {} # empty rule = allow all
|
5. Combining Multiple Policies
5.1 The Union Rule
When multiple Network Policies select the same pod, the allowed traffic is the union of all policies. There is no priority or ordering.
| Policy A: allow ingress from app=frontend on port 80
Policy B: allow ingress from app=monitoring on port 9090
Result for selected pod:
frontend:80 → ALLOWED (by Policy A)
monitoring:9090 → ALLOWED (by Policy B)
anything else → DENIED
|
5.2 Example — Layered Policies
| # Policy 1: Default deny all ingress in the namespace
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: deny-all-ingress
namespace: app-ns
spec:
podSelector: {}
policyTypes:
- Ingress
---
# Policy 2: Allow frontend → backend on port 8080
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: allow-frontend-to-backend
namespace: app-ns
spec:
podSelector:
matchLabels:
app: backend
policyTypes:
- Ingress
ingress:
- from:
- podSelector:
matchLabels:
app: frontend
ports:
- port: 8080
protocol: TCP
---
# Policy 3: Allow monitoring → backend on port 9090
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: allow-monitoring-to-backend
namespace: app-ns
spec:
podSelector:
matchLabels:
app: backend
policyTypes:
- Ingress
ingress:
- from:
- podSelector:
matchLabels:
app: monitoring
ports:
- port: 9090
protocol: TCP
|
Result for app=backend pods:
| frontend:8080 → ✓ (Policy 2)
monitoring:9090 → ✓ (Policy 3)
frontend:9090 → ✗ (no policy allows this combination)
random-pod:any → ✗ (denied by default deny + no allowing policy)
|
5.3 Cross-Namespace Policy
Allow traffic from a specific namespace:
| # Label the source namespace
kubectl label namespace monitoring team=monitoring
|
| apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: allow-from-monitoring-ns
namespace: app-ns
spec:
podSelector:
matchLabels:
app: backend
policyTypes:
- Ingress
ingress:
- from:
- namespaceSelector:
matchLabels:
team: monitoring
podSelector: # AND — must be in monitoring ns AND have this label
matchLabels:
app: prometheus
ports:
- port: 9090
|
6. Common Patterns
6.1 Allow Only Same-Namespace Traffic
| apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: allow-same-namespace
namespace: app-ns
spec:
podSelector: {}
policyTypes:
- Ingress
ingress:
- from:
- podSelector: {} # any pod in the SAME namespace
|
6.2 Database Access — Only from Backend
| apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: db-access
namespace: app-ns
spec:
podSelector:
matchLabels:
app: database
policyTypes:
- Ingress
ingress:
- from:
- podSelector:
matchLabels:
app: backend
ports:
- port: 3306
protocol: TCP
|
6.3 Allow Egress to External API Only
| apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: allow-external-api
namespace: app-ns
spec:
podSelector:
matchLabels:
app: worker
policyTypes:
- Egress
egress:
- to:
- ipBlock:
cidr: 203.0.113.0/24 # external API IP range
ports:
- port: 443
protocol: TCP
- to: [] # DNS
ports:
- port: 53
protocol: UDP
|
6.4 Full Microsegmentation Example
| ┌─────────────────────────────────────────────┐
│ namespace: app-ns │
│ │
│ ┌──────────┐ :80 ┌──────────┐ :3306 │
│ │ frontend │───────▶│ backend │─────────▶│
│ │ │ │ │ ││
│ └──────────┘ └──────────┘ ┌──────▼──┐
│ │ database │
│ └─────────┘
│ Policies: │
│ 1. deny-all-ingress (namespace-wide) │
│ 2. frontend: allow ingress from external │
│ 3. backend: allow ingress from frontend:80 │
│ 4. database: allow ingress from backend:3306│
└─────────────────────────────────────────────┘
|
7. Debugging Network Policies
7.1 Testing Connectivity
| # Create a test pod
kubectl run test --image=busybox -it --rm --restart=Never -- sh
# Test TCP connectivity
nc -zv <target-pod-ip> <port>
# or
wget -qO- --timeout=3 http://<service-name>:<port>
# Test from a specific namespace
kubectl run test -n <namespace> --image=busybox -it --rm --restart=Never -- \
wget -qO- --timeout=3 http://<service>.<target-ns>:<port>
|
7.2 Debugging Checklist
| # 1. List all Network Policies in the namespace
kubectl get networkpolicies -n <namespace>
kubectl get netpol -n <namespace> # shortname
# 2. Describe a specific policy
kubectl describe netpol <name> -n <namespace>
# 3. Check which pods the policy selects
kubectl get pods -n <namespace> -l <selector-from-podSelector>
# 4. Check if the source pods match the "from" selector
kubectl get pods -n <source-namespace> --show-labels
# 5. Check if namespaces have the required labels
kubectl get namespaces --show-labels
# 6. Verify the CNI supports Network Policies
kubectl get pods -n kube-system | grep -E "calico|cilium"
# 7. Test connectivity
kubectl run test --image=busybox -it --rm --restart=Never -- \
nc -zv -w3 <target-ip> <port>
|
7.3 Common Mistakes
| Mistake |
Symptom |
Fix |
| Forgot to label the namespace |
Cross-namespace policy doesn't work |
kubectl label ns <ns> <key>=<value> |
| AND vs OR confusion |
Too much or too little traffic allowed |
Check dash placement in from/to arrays |
| Missing DNS egress |
Pods can't resolve Service names |
Add egress rule for port 53 UDP/TCP |
| Policy in wrong namespace |
Policy has no effect |
Network Policy must be in the TARGET pod's namespace |
| Flannel CNI |
Policies created but not enforced |
Switch to Calico or Cilium |
Empty podSelector: {} in from |
Allows all pods in the namespace, not all pods everywhere |
Add namespaceSelector: {} for cluster-wide |
Forgot policyTypes |
Policy type inferred incorrectly |
Always set policyTypes explicitly |
7.4 Allow All Pods in All Namespaces
| # This allows from same namespace only:
ingress:
- from:
- podSelector: {}
# This allows from ALL namespaces:
ingress:
- from:
- namespaceSelector: {}
podSelector: {}
# Or simply:
ingress:
- from:
- namespaceSelector: {} # all namespaces, all pods
|
CKA Tip: podSelector: {} alone means "all pods in the same namespace". To allow from all namespaces, you must include namespaceSelector: {}.
8. Practice Exercises
Exercise 1 — Default Deny and Allow
| # 1. Create a namespace
kubectl create namespace netpol-test
# 2. Create two deployments
kubectl create deployment web --image=nginx -n netpol-test
kubectl expose deployment web --port=80 -n netpol-test
kubectl create deployment client --image=busybox -n netpol-test \
-- sleep 3600
# 3. Test connectivity (should work — no policies yet)
kubectl exec -n netpol-test deploy/client -- wget -qO- --timeout=3 http://web
# HTML output = success
# 4. Apply default deny ingress
cat <<EOF | kubectl apply -f -
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: deny-all-ingress
namespace: netpol-test
spec:
podSelector: {}
policyTypes:
- Ingress
EOF
# 5. Test again (should fail)
kubectl exec -n netpol-test deploy/client -- wget -qO- --timeout=3 http://web
# wget: download timed out
# 6. Allow client → web
cat <<EOF | kubectl apply -f -
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: allow-client-to-web
namespace: netpol-test
spec:
podSelector:
matchLabels:
app: web
policyTypes:
- Ingress
ingress:
- from:
- podSelector:
matchLabels:
app: client
ports:
- port: 80
protocol: TCP
EOF
# 7. Test again (should work)
kubectl exec -n netpol-test deploy/client -- wget -qO- --timeout=3 http://web
# 8. Clean up
kubectl delete namespace netpol-test
|
Exercise 2 — Egress Policy with DNS
| # 1. Create namespace and pods
kubectl create namespace egress-test
kubectl create deployment app --image=busybox -n egress-test -- sleep 3600
# 2. Test DNS works
kubectl exec -n egress-test deploy/app -- nslookup google.com
# 3. Apply deny-all egress
cat <<EOF | kubectl apply -f -
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: deny-all-egress
namespace: egress-test
spec:
podSelector: {}
policyTypes:
- Egress
EOF
# 4. Test DNS (should fail)
kubectl exec -n egress-test deploy/app -- nslookup google.com
# nslookup: can't resolve
# 5. Allow DNS egress
cat <<EOF | kubectl apply -f -
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: allow-dns
namespace: egress-test
spec:
podSelector: {}
policyTypes:
- Egress
egress:
- ports:
- port: 53
protocol: UDP
- port: 53
protocol: TCP
EOF
# 6. Test DNS again (should work)
kubectl exec -n egress-test deploy/app -- nslookup google.com
# 7. Clean up
kubectl delete namespace egress-test
|
Exercise 3 — Cross-Namespace Policy
| # 1. Create two namespaces
kubectl create namespace frontend
kubectl create namespace backend
kubectl label namespace frontend tier=frontend
# 2. Create pods
kubectl create deployment api --image=nginx -n backend
kubectl expose deployment api --port=80 -n backend
kubectl create deployment web --image=busybox -n frontend -- sleep 3600
# 3. Test cross-namespace (should work — no policies)
kubectl exec -n frontend deploy/web -- wget -qO- --timeout=3 http://api.backend
# 4. Deny all ingress in backend
cat <<EOF | kubectl apply -f -
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: deny-all
namespace: backend
spec:
podSelector: {}
policyTypes:
- Ingress
EOF
# 5. Test (should fail)
kubectl exec -n frontend deploy/web -- wget -qO- --timeout=3 http://api.backend
# 6. Allow from frontend namespace
cat <<EOF | kubectl apply -f -
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: allow-from-frontend
namespace: backend
spec:
podSelector:
matchLabels:
app: api
policyTypes:
- Ingress
ingress:
- from:
- namespaceSelector:
matchLabels:
tier: frontend
ports:
- port: 80
EOF
# 7. Test (should work)
kubectl exec -n frontend deploy/web -- wget -qO- --timeout=3 http://api.backend
# 8. Clean up
kubectl delete namespace frontend backend
|
Exercise 4 — AND vs OR
| # 1. Setup
kubectl create namespace logic-test
kubectl label namespace logic-test env=test
kubectl create deployment target --image=nginx -n logic-test
kubectl expose deployment target --port=80 -n logic-test
kubectl create deployment allowed --image=busybox -n logic-test -- sleep 3600
kubectl label deployment allowed app=allowed -n logic-test
kubectl create deployment denied --image=busybox -n logic-test -- sleep 3600
# 2. Apply a policy with AND logic
cat <<EOF | kubectl apply -f -
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: and-policy
namespace: logic-test
spec:
podSelector:
matchLabels:
app: target
policyTypes:
- Ingress
ingress:
- from:
- podSelector:
matchLabels:
app: allowed
namespaceSelector:
matchLabels:
env: test
ports:
- port: 80
EOF
# 3. Test from "allowed" pod (should work — matches both selectors)
kubectl exec -n logic-test deploy/allowed -- wget -qO- --timeout=3 http://target
# 4. Test from "denied" pod (should fail — doesn't match podSelector)
kubectl exec -n logic-test deploy/denied -- wget -qO- --timeout=3 http://target
# 5. Clean up
kubectl delete namespace logic-test
|
Exercise 5 — Full Microsegmentation
| # Build the 3-tier architecture from Section 6.4
# 1. Create namespace and deny all
kubectl create namespace micro
cat <<EOF | kubectl apply -f -
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: deny-all
namespace: micro
spec:
podSelector: {}
policyTypes:
- Ingress
- Egress
EOF
# 2. Create the three tiers
kubectl create deployment frontend --image=nginx -n micro
kubectl expose deployment frontend --port=80 -n micro
kubectl create deployment backend --image=nginx -n micro
kubectl expose deployment backend --port=80 -n micro
kubectl create deployment database --image=nginx -n micro
kubectl expose deployment database --port=80 -n micro
# 3. Allow DNS for all pods
cat <<EOF | kubectl apply -f -
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: allow-dns
namespace: micro
spec:
podSelector: {}
policyTypes:
- Egress
egress:
- ports:
- port: 53
protocol: UDP
EOF
# 4. Allow frontend → backend
cat <<EOF | kubectl apply -f -
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: frontend-to-backend
namespace: micro
spec:
podSelector:
matchLabels:
app: backend
policyTypes:
- Ingress
ingress:
- from:
- podSelector:
matchLabels:
app: frontend
ports:
- port: 80
---
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: frontend-egress-backend
namespace: micro
spec:
podSelector:
matchLabels:
app: frontend
policyTypes:
- Egress
egress:
- to:
- podSelector:
matchLabels:
app: backend
ports:
- port: 80
EOF
# 5. Allow backend → database
cat <<EOF | kubectl apply -f -
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: backend-to-database
namespace: micro
spec:
podSelector:
matchLabels:
app: database
policyTypes:
- Ingress
ingress:
- from:
- podSelector:
matchLabels:
app: backend
ports:
- port: 80
---
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: backend-egress-database
namespace: micro
spec:
podSelector:
matchLabels:
app: backend
policyTypes:
- Egress
egress:
- to:
- podSelector:
matchLabels:
app: database
ports:
- port: 80
EOF
# 6. Test: frontend → backend (should work)
kubectl exec -n micro deploy/frontend -- wget -qO- --timeout=3 http://backend
# 7. Test: frontend → database (should fail)
kubectl exec -n micro deploy/frontend -- wget -qO- --timeout=3 http://database
# 8. Test: backend → database (should work)
kubectl exec -n micro deploy/backend -- wget -qO- --timeout=3 http://database
# 9. Clean up
kubectl delete namespace micro
|
9. Key Takeaways for the CKA Exam
| Point |
Detail |
| No policy = allow all |
Pods are fully open until a Network Policy selects them |
| Policies are additive (union) |
If any policy allows the traffic, it's allowed |
| Default deny first |
Start with podSelector: {} + empty policyTypes to deny all, then add allow rules |
| AND vs OR |
Single from entry = AND; multiple from entries = OR — count the dashes |
podSelector: {} in from |
Means all pods in the SAME namespace, not all pods everywhere |
Cross-namespace needs namespaceSelector |
And the namespace must have the matching label |
| Don't forget DNS |
Egress deny blocks port 53 — always add a DNS allow rule |
| Policy goes in the TARGET namespace |
The namespace where the selected pods live |
| CNI must support it |
Calico ✅, Cilium ✅, Flannel ❌ |
Test with wget --timeout=3 |
Quick way to verify connectivity from a busybox pod |
kubectl get netpol |
Shortname for networkpolicies |
Previous: 12-ingress.md — Ingress
Next: 14-storage.md — Storage