Skip to content

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:

1
2
3
4
5
6
┌─────────┐     ┌─────────┐     ┌─────────┐
│  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.

1
2
3
4
5
6
7
8
9
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

1
2
3
4
5
6
7
8
9
# 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

1
2
3
4
5
ingress:
- from:
  - podSelector:
      matchLabels:
        app: frontend

namespaceSelector — Pods in Other Namespaces

1
2
3
4
5
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

1
2
3
4
5
6
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:

1
2
3
4
5
6
7
8
egress:
- to:
  - podSelector:
      matchLabels:
        app: database
  ports:
  - port: 3306
    protocol: TCP

2.6 Ports

1
2
3
4
5
6
7
8
9
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

1
2
3
4
5
6
7
8
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

1
2
3
4
5
6
7
8
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.

1
2
3
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:

1
2
3
4
5
6
7
8
9
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:

1
2
3
4
5
6
7
8
9
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.

1
2
3
4
5
6
7
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:

1
2
3
4
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