By default, Kubernetes allows all Pods to talk to all Pods — a flat, open network. NetworkPolicies are firewall rules for Pod traffic: they let you restrict which Pods can communicate, implementing zero-trust networking at the workload level. This is a critical CKS topic and a production security requirement.
1. Default Behavior & Default-Deny
Without NetworkPolicies
All Pods accept traffic from everywhere and can send traffic everywhere. No restrictions. This is the K8s default.
How NetworkPolicies Change This
Default-Deny All (The Starting Point for Zero-Trust)
# Deny ALL ingress to all Pods in this namespace:
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: default-deny-ingress
namespace: production
spec:
podSelector: {} # ← Empty = selects ALL Pods in namespace
policyTypes:
- Ingress # ← Only ingress is restricted
# No ingress rules = deny all inbound traffic
# Deny ALL egress from all Pods in this namespace:
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: default-deny-egress
namespace: production
spec:
podSelector: {}
policyTypes:
- Egress
# No egress rules = deny all outbound traffic
# Deny BOTH ingress and egress (full lockdown):
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: default-deny-all
namespace: production
spec:
podSelector: {}
policyTypes:
- Ingress
- Egress
Allow DNS (Required with Egress Deny)
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: allow-dns
namespace: production
spec:
podSelector: {}
policyTypes:
- Egress
egress:
- to:
- namespaceSelector:
matchLabels:
kubernetes.io/metadata.name: kube-system
podSelector:
matchLabels:
k8s-app: kube-dns
ports:
- protocol: UDP
port: 53
- protocol: TCP
port: 53
2. NetworkPolicy Anatomy
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: api-policy
namespace: production
spec:
podSelector: # WHO this policy applies to
matchLabels:
app: api
policyTypes: # WHAT directions are controlled
- Ingress
- Egress
ingress: # ALLOW rules for inbound traffic
- from: # Source(s) — OR logic between items
- podSelector:
matchLabels:
app: web
- namespaceSelector:
matchLabels:
env: production
ports: # Destination port(s) on the selected Pods
- protocol: TCP
port: 8080
egress: # ALLOW rules for outbound traffic
- to:
- podSelector:
matchLabels:
app: postgres
ports:
- protocol: TCP
port: 5432
Key Components
| Field | Purpose | Empty = ? |
|---|---|---|
podSelector | Which Pods the policy applies to | {} = all Pods in namespace |
policyTypes | Directions to restrict (Ingress, Egress, or both) | Inferred from which rules exist |
ingress[].from | Allowed sources | Omitted = allow from everywhere |
egress[].to | Allowed destinations | Omitted = allow to everywhere |
ports | Allowed ports/protocols | Omitted = all ports |
The AND / OR Logic
• Multiple items in the
from/to array → OR (any one match allows)• Multiple selectors within one item → AND (all must match)
• Multiple
ingress/egress rules → OR (any rule can permit the traffic)
# OR logic — two separate "from" items:
ingress:
- from:
- podSelector: # ← Pods with app=web in same namespace
matchLabels:
app: web
- namespaceSelector: # ← OR any Pod in namespace with env=staging
matchLabels:
env: staging
# AND logic — combined in ONE item:
ingress:
- from:
- podSelector: # ← Pods with app=web
matchLabels:
app: web
namespaceSelector: # ← AND in namespace with env=production
matchLabels:
env: production
# Both conditions must be true (Pod must have app=web AND be in a namespace labeled env=production)
-) in the YAML. Misplacing it changes the policy from "allow from web Pods OR staging namespace" to "allow from web Pods IN staging namespace." Practice this until it's automatic.
3. Selector Types
| Selector | Matches | Example |
|---|---|---|
podSelector | Pods in the same namespace as the policy | Allow from Pods with app=web |
namespaceSelector | All Pods in namespaces matching labels | Allow from any Pod in env=prod namespaces |
| Both combined (AND) | Specific Pods in specific namespaces | Allow from app=web Pods in env=prod namespaces |
ipBlock | CIDR ranges (external traffic) | Allow from 10.0.0.0/8 but not 10.0.1.0/24 |
podSelector Examples
# Allow ingress ONLY from Pods labeled "role=frontend" in same namespace:
ingress:
- from:
- podSelector:
matchLabels:
role: frontend
ports:
- port: 8080
namespaceSelector Examples
# Allow ingress from ANY Pod in namespaces labeled "team=platform":
ingress:
- from:
- namespaceSelector:
matchLabels:
team: platform
# Allow from ALL namespaces (empty namespaceSelector):
ingress:
- from:
- namespaceSelector: {} # All namespaces
ipBlock — CIDR Ranges
# Allow ingress from corporate network, excluding a specific subnet:
ingress:
- from:
- ipBlock:
cidr: 10.0.0.0/8 # Allow this range
except:
- 10.0.1.0/24 # Except this subnet
ports:
- port: 443
# Allow egress to external API:
egress:
- to:
- ipBlock:
cidr: 203.0.113.0/24 # External API IP range
ports:
- port: 443
ipBlock is for traffic entering/leaving the cluster. Pod-to-Pod traffic within the cluster uses Pod IPs which are dynamic — use podSelector and namespaceSelector for internal traffic. ipBlock is for external clients, external APIs, or known infrastructure IPs.
Namespace Labels (Important!)
For namespaceSelector to work, namespaces must have labels. K8s 1.21+ automatically adds kubernetes.io/metadata.name: <namespace-name> to every namespace. For custom labels:
# Label a namespace for policy selection:
kubectl label namespace monitoring purpose=observability
# Now you can target it:
namespaceSelector:
matchLabels:
purpose: observability
4. Practical Patterns
Pattern: Three-Tier Application
# Policy for API tier: only accept from frontend, only connect to DB
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: api-policy
namespace: production
spec:
podSelector:
matchLabels:
role: api
policyTypes:
- Ingress
- Egress
ingress:
- from:
- podSelector:
matchLabels:
role: frontend
ports:
- port: 8080
egress:
- to:
- podSelector:
matchLabels:
role: db
ports:
- port: 5432
- to: # Allow DNS
- namespaceSelector:
matchLabels:
kubernetes.io/metadata.name: kube-system
podSelector:
matchLabels:
k8s-app: kube-dns
ports:
- port: 53
protocol: UDP
# Policy for DB: only accept from API
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: db-policy
namespace: production
spec:
podSelector:
matchLabels:
role: db
policyTypes:
- Ingress
ingress:
- from:
- podSelector:
matchLabels:
role: api
ports:
- port: 5432
Pattern: Allow Monitoring from Prometheus
# Allow Prometheus (in monitoring namespace) to scrape metrics:
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: allow-prometheus-scrape
namespace: production
spec:
podSelector: {} # All Pods in this namespace
policyTypes:
- Ingress
ingress:
- from:
- namespaceSelector:
matchLabels:
kubernetes.io/metadata.name: monitoring
podSelector:
matchLabels:
app: prometheus
ports:
- port: 9090 # Metrics port
Pattern: Allow Egress to External API Only
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: allow-external-api
namespace: production
spec:
podSelector:
matchLabels:
role: api
policyTypes:
- Egress
egress:
- to:
- ipBlock:
cidr: 203.0.113.50/32 # Specific external API IP
ports:
- port: 443
- to: # DNS (always needed)
- namespaceSelector:
matchLabels:
kubernetes.io/metadata.name: kube-system
podSelector:
matchLabels:
k8s-app: kube-dns
ports:
- port: 53
protocol: UDP
5. Gotchas & Common Mistakes
| Gotcha | Problem | Fix |
|---|---|---|
| CNI doesn't enforce | Flannel ignores NetworkPolicies — no enforcement | Use Calico, Cilium, or Weave |
| Forgot DNS egress | Pods can't resolve Service names → connectivity appears broken | Always add DNS egress rule when using egress deny |
| OR vs AND confusion | Policy is too open or too restrictive | Separate - items = OR; combined selectors = AND |
| Namespace not labeled | namespaceSelector doesn't match | Label the namespace: kubectl label ns monitoring purpose=obs |
| Policy in wrong namespace | NetworkPolicy is namespaced — must be in same ns as target Pods | Verify metadata.namespace matches target Pods |
| No policyTypes specified | K8s infers from which rules exist — may not restrict what you expect | Always explicitly set policyTypes |
| Policies are additive | Can't write a "deny specific traffic" rule — only allow | Start with deny-all, then add allow rules |
6. Testing NetworkPolicies
# Deploy test Pods: kubectl run test-web --image=busybox --labels="role=frontend" -- sleep 3600 kubectl run test-api --image=busybox --labels="role=api" -- sleep 3600 # Test connectivity (should succeed if policy allows): kubectl exec test-web -- wget -qO- --timeout=2 http://api-svc:8080 # If blocked: wget: download timed out # Test from a Pod that should be denied: kubectl run test-attacker --image=busybox -- sleep 3600 kubectl exec test-attacker -- wget -qO- --timeout=2 http://api-svc:8080 # Should timeout (denied by policy) # Use netshoot for more detailed testing: kubectl run netshoot --image=nicolaka/netshoot -it --rm -- \ curl -v --connect-timeout 3 http://10.244.1.5:8080
kubectl exec ... -- wget --timeout=2 from both allowed and denied Pods. The --timeout=2 is crucial — without it, blocked requests hang for 30+ seconds wasting exam time.
Summary
| Concept | Key Point |
|---|---|
| Default behavior | No policies = all traffic allowed (flat open network) |
| Selection effect | Once selected by ANY policy, non-allowed traffic is denied |
| Default-deny | podSelector: {} with empty rules = deny all (start here) |
| Additive only | Can't deny specific traffic — only add allow rules |
| OR vs AND | Separate array items = OR; combined selectors = AND |
| podSelector | Targets Pods in same namespace |
| namespaceSelector | Targets all Pods in matching namespaces |
| ipBlock | CIDR ranges for external traffic |
| DNS rule | Always allow UDP/TCP 53 to kube-dns when egress is denied |
| CNI requirement | Must use a CNI that enforces policies (Calico, Cilium, NOT Flannel) |
📝 Quiz: NetworkPolicies
Q1: You create a NetworkPolicy with podSelector: {}, policyTypes: [Ingress], and no ingress rules. What happens?
podSelector matches all Pods. policyTypes: [Ingress] means ingress is controlled. No ingress rules = no traffic allowed in. Egress is unaffected (not listed in policyTypes).Q2: What's the difference between these two from blocks?
# Version A: # Version B:
from: from:
- podSelector: - podSelector:
matchLabels: matchLabels:
app: web app: web
- namespaceSelector: namespaceSelector:
matchLabels: matchLabels:
env: prod env: prod
app=web in the same namespace OR from any Pod in namespaces labeled env=prod.Version B (one item, AND): Allow from Pods labeled
app=web that are also in namespaces labeled env=prod. The difference is one YAML dash (-).Q3: You applied default-deny-egress. Now Pods can't resolve DNS names. What's wrong and how do you fix it?
kube-dns Pods in kube-system namespace on port 53. This is required whenever you deny egress.Q4: You're using Flannel as your CNI. You create NetworkPolicies but they don't seem to block anything. Why?
Q5: A NetworkPolicy has podSelector: {matchLabels: {app: api}}. A Pod without any labels — is it affected by this policy?
podSelector (Pods with app=api). Pods without that label are not selected, so the policy doesn't affect them — they remain in the default-open state (unless selected by another policy).Q6: Can you write a NetworkPolicy that denies traffic from a specific Pod while allowing everything else?