Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

15.2 Network Policies — Microsegmentation

⏱️ ~8 min read

TL;DR: By default, every pod in a Kubernetes cluster can talk to every other pod — there are no network firewalls between them. Network Policies are Kubernetes-native firewall rules that restrict which pods can talk to which, on which ports. The golden rule for production: default-deny all traffic, then explicitly allow only what’s needed.


The Problem: Flat Network by Default

Without Network Policies:

  [frontend pod] ←──────────────→ [database pod]   ✓ (allowed)
  [redis pod]    ←──────────────→ [database pod]   ✓ (allowed)
  [attacker pod] ←──────────────→ [database pod]   ✓ (ALSO allowed!)

Any compromised pod can freely reach any other pod or service.
With Network Policies (default-deny + allow-list):

  [frontend pod] ──────────────→ [database pod]    ✓ (explicitly allowed)
  [redis pod]    ──────────────→ [database pod]    ✗ (no matching allow rule)
  [attacker pod] ──────────────→ [database pod]    ✗ (blocked)

How Network Policies Work

graph TD
    NP["NetworkPolicy\n(applied to pods via podSelector)"]
    ING["Ingress Rules\n(who can send TO these pods)"]
    EGR["Egress Rules\n(where these pods can send TO)"]
    
    NP --> ING & EGR
    
    ING --> SRC["Sources:\npodSelector\nnamespaceSelector\nipBlock (CIDR)"]
    EGR --> DST["Destinations:\npodSelector\nnamespaceSelector\nipBlock (CIDR)"]

CNI Requirement: Network Policies require a CNI plugin that enforces them — Calico, Cilium, or Weave. The default Minikube CNI (kindnet) does not enforce Network Policies. Use minikube start --cni=calico.


Network Policy Structure

apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
  name: db-policy
  namespace: production
spec:
  podSelector:            # Which pods this policy APPLIES TO
    matchLabels:
      app: postgres
  
  policyTypes:
  - Ingress               # Control incoming traffic
  - Egress                # Control outgoing traffic
  
  ingress:
  - from:                 # Allow FROM these sources
    - podSelector:        # Pods with this label (in same namespace)
        matchLabels:
          app: backend
    - namespaceSelector:  # Pods in namespaces with this label
        matchLabels:
          env: production
    ports:                # Only on these ports
    - protocol: TCP
      port: 5432
  
  egress:
  - to:
    - podSelector:
        matchLabels:
          app: redis
    ports:
    - protocol: TCP
      port: 6379

The Three Selectors

SelectorMatchesExample
podSelectorPods by label (within same namespace)app: backend
namespaceSelectorAll pods in namespaces matching labelenv: production
ipBlockExternal CIDR ranges10.0.0.0/8 (exclude 10.96.0.0/12)

AND vs OR Logic

# AND: must match BOTH selectors (same array item)
- from:
  - podSelector:
      matchLabels: {app: backend}
    namespaceSelector:       # ← same item as podSelector = AND
      matchLabels: {env: prod}
# Means: pods with app=backend AND in namespace with env=prod

# OR: either selector is sufficient (separate array items)
- from:
  - podSelector:
      matchLabels: {app: backend}   # ← separate item = OR
  - namespaceSelector:
      matchLabels: {env: prod}
# Means: pods with app=backend OR any pod in env=prod namespace

Essential Patterns

Pattern 1: Default Deny All (Start Here)

Always create this first in every application namespace:

# Deny ALL ingress and egress by default
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
  name: default-deny-all
  namespace: production
spec:
  podSelector: {}       # {} = matches ALL pods in namespace
  policyTypes:
  - Ingress
  - Egress

After applying this, nothing can talk to anything. Then add allow policies layer by layer.

Pattern 2: Allow DNS (Essential After Default Deny)

All pods need DNS resolution. Without this, no pod can resolve service names:

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

Pattern 3: Frontend → Backend → Database

# Allow frontend to reach backend on port 8080
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
  name: allow-frontend-to-backend
  namespace: production
spec:
  podSelector:
    matchLabels:
      app: backend
  policyTypes:
  - Ingress
  ingress:
  - from:
    - podSelector:
        matchLabels:
          app: frontend
    ports:
    - protocol: TCP
      port: 8080
---
# Allow backend to reach database on port 5432
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
  name: allow-backend-to-db
  namespace: production
spec:
  podSelector:
    matchLabels:
      app: postgres
  policyTypes:
  - Ingress
  ingress:
  - from:
    - podSelector:
        matchLabels:
          app: backend
    ports:
    - protocol: TCP
      port: 5432

Pattern 4: Allow Monitoring to Scrape All Pods

apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
  name: allow-prometheus-scrape
  namespace: production
spec:
  podSelector: {}        # All pods in namespace
  policyTypes:
  - Ingress
  ingress:
  - from:
    - namespaceSelector:
        matchLabels:
          kubernetes.io/metadata.name: monitoring
      podSelector:
        matchLabels:
          app: prometheus
    ports:
    - protocol: TCP
      port: 9090
    - protocol: TCP
      port: 8080

Pattern 5: Namespace Isolation

All pods in namespace A can talk to each other, but not to namespace B:

# Label your namespace first:
# kubectl label namespace team-a team=a
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
  name: namespace-isolation
  namespace: team-a
spec:
  podSelector: {}
  policyTypes:
  - Ingress
  ingress:
  - from:
    - namespaceSelector:
        matchLabels:
          team: a         # Only allow traffic from same namespace

Testing Network Policies

# Test connectivity between pods
kubectl run test-pod --image=busybox:1.36 --restart=Never -n production -- sleep 3600

# Test if frontend can reach backend (should succeed)
kubectl exec -n production test-pod -- wget -O- http://backend:8080 --timeout=3

# Test if test-pod can reach database (should be blocked)
kubectl exec -n production test-pod -- wget -O- http://postgres:5432 --timeout=3
# Expected: wget: can't connect to remote host (10.x.x.x): Connection timed out

# Check policies in a namespace
kubectl get networkpolicies -n production
kubectl describe networkpolicy default-deny-all -n production

✅ Quick Check

Q1: You apply default-deny-all to your namespace. Immediately your pods stop being able to resolve my-service by DNS. What policy do you need to add?

Answer An **egress allow rule for DNS** on port 53 (UDP and TCP) targeting the `kube-system` namespace where CoreDNS runs. Without it, all DNS resolution fails because the `default-deny-all` policy also blocks egress. This is the most common mistake when first applying default-deny — always add the DNS egress allow immediately after.

Q2: You have two rules in a from block: one podSelector and one namespaceSelector as separate list items (not nested together). What logic applies?

Answer **OR logic** — traffic is allowed if it matches EITHER the `podSelector` OR the `namespaceSelector`. To get AND logic (pod with that label AND in that namespace), you must nest both selectors within the **same list item** (as a single `from` entry with both fields on the same object). This is one of the most confusing parts of Network Policy syntax.

Q3: Does a Network Policy on the destination pod control traffic, or does the source pod’s network policy control it?

Answer Both ends independently — but typically it's the **destination pod's ingress policy** that controls who can reach it, and the **source pod's egress policy** that controls where it can connect to. For traffic to succeed, the egress policy on the source AND the ingress policy on the destination must both permit it. If either blocks it, the connection fails. This "both ends" model is what makes default-deny effective — even if a source has permissive egress, a restrictive destination ingress can still block it.