Why Network Policies Matter
Kubernetes clusters by default operate in an "allow-all" mode: any pod can freely communicate with any other pod, regardless of namespace or purpose. This permissive default violates zero-trust security principles and creates significant risk. Network policies are the mechanism to enforce network segmentation at the pod level, transforming Kubernetes from an open network to a properly segmented environment where access is explicitly allowed and everything else is implicitly denied.
Network policies prevent lateral movement when one container is compromised. If a web application container is hacked, attackers can normally move laterally to access the database container, cache, or other internal services—all of which are reachable because everything can communicate with everything. Network policies restrict the hacked web container so it can only reach legitimate endpoints (the load balancer for responses, the database for queries), making lateral movement impossible.
Network policies reduce attack surface by blocking unnecessary connections. A catalog service has no legitimate reason to connect to the payment database. A logging service has no legitimate reason to access customer data services. Network policies enforce these restrictions at the network level, preventing misconfigured services from accessing data they shouldn't have.
Network policies ensure compliance with regulatory requirements. PCI-DSS (payment processing), HIPAA (healthcare), and FedRAMP (government systems) all explicitly require network segmentation. Network policies are the mechanism to demonstrate segmentation to regulators and auditors. Without them, organizations cannot claim compliance with these frameworks.
Network policies isolate noisy neighbors and prevent resource exhaustion attacks. A badly-behaved pod cannot consume all cluster bandwidth by making unlimited requests to all other pods. Policies can restrict bandwidth per connection, preventing denial-of-service scenarios.
Network policies work at the kernel level through iptables or eBPF, providing true network isolation that applications cannot bypass. If a container is compromised, the kernel enforces network policies regardless of what the attacker does inside the container. This is container-native security—the platform itself prevents certain attacks rather than relying on application-level controls.
The philosophical benefit is that a single misconfigured pod cannot accidentally expose the entire cluster. If a pod is accidentally misconfigured to expose credentials or internal APIs to the internet, network policies can limit the damage. The exposed service might receive requests from the internet, but network policies can restrict its access to legitimate backend services, preventing the attacker from pivoting to the database or other sensitive systems.
Default-Deny Baseline Policy
Start with deny-all, then allow specific flows. This is the safest approach:
apiVersion: networking.k8s.io/v1kind: NetworkPolicymetadata: name: default-deny-all namespace: productionspec: podSelector: {} policyTypes: - Ingress - EgressThis blocks everything. Now allow specific traffic.
Per-Application Network Policies
PostgreSQL: Database Access Only
Allow ingress only from app pods on port 5432. Deny everything else.
apiVersion: networking.k8s.io/v1kind: NetworkPolicymetadata: name: postgres-access namespace: productionspec: podSelector: matchLabels: app: postgres policyTypes: - Ingress - Egress ingress: # Allow from app pods on port 5432 - from: - podSelector: matchLabels: app: app ports: - protocol: TCP port: 5432 # Allow from backup tools - from: - podSelector: matchLabels: app: postgres-backup ports: - protocol: TCP port: 5432 egress: # Allow DNS (needed for hostname resolution) - to: - namespaceSelector: {} podSelector: matchLabels: k8s-app: kube-dns ports: - protocol: UDP port: 53Postgres cannot initiate outbound connections (except DNS). Only app pods reach it on 5432.
Redis: Cache with Sentinel
apiVersion: networking.k8s.io/v1kind: NetworkPolicymetadata: name: redis-access namespace: productionspec: podSelector: matchLabels: app: redis policyTypes: - Ingress - Egress ingress: # Client connections on 6379 - from: - podSelector: matchLabels: app: app ports: - protocol: TCP port: 6379 # Sentinel connections on 26379 (Redis Sentinel) - from: - podSelector: matchLabels: app: redis-sentinel ports: - protocol: TCP port: 26379 # Inter-Redis replication (if using cluster) - from: - podSelector: matchLabels: app: redis ports: - protocol: TCP port: 6379 egress: # DNS only - to: - namespaceSelector: {} podSelector: matchLabels: k8s-app: kube-dns ports: - protocol: UDP port: 53Kafka: Broker-to-Broker + Client Access
apiVersion: networking.k8s.io/v1kind: NetworkPolicymetadata: name: kafka-access namespace: productionspec: podSelector: matchLabels: app: kafka policyTypes: - Ingress - Egress ingress: # Client access on 9092 (PLAINTEXT) / 9093 (SASL_SSL) - from: - podSelector: matchLabels: app: app ports: - protocol: TCP port: 9092 - protocol: TCP port: 9093 # Producer/consumer clients - from: - namespaceSelector: matchLabels: name: analytics podSelector: matchLabels: app: data-pipeline ports: - protocol: TCP port: 9092 # Inter-broker replication on 9094 - from: - podSelector: matchLabels: app: kafka ports: - protocol: TCP port: 9094 # Zookeeper coordination (if applicable) - from: - podSelector: matchLabels: app: zookeeper ports: - protocol: TCP port: 2181 egress: # Zookeeper - to: - podSelector: matchLabels: app: zookeeper ports: - protocol: TCP port: 2181 # DNS - to: - namespaceSelector: {} podSelector: matchLabels: k8s-app: kube-dns ports: - protocol: UDP port: 53Nginx Ingress Controller
apiVersion: networking.k8s.io/v1kind: NetworkPolicymetadata: name: nginx-ingress namespace: ingress-nginxspec: podSelector: matchLabels: app.kubernetes.io/name: ingress-nginx policyTypes: - Ingress - Egress ingress: # Public HTTP/HTTPS from anywhere - from: - namespaceSelector: {} ports: - protocol: TCP port: 80 - protocol: TCP port: 443 egress: # To backend services in any namespace - to: - namespaceSelector: {} ports: - protocol: TCP port: 8080 # Common app port - protocol: TCP port: 5432 # Database - protocol: TCP port: 6379 # Cache # DNS - to: - namespaceSelector: {} podSelector: matchLabels: k8s-app: kube-dns ports: - protocol: UDP port: 53Application Pod: Restricted Egress
apiVersion: networking.k8s.io/v1kind: NetworkPolicymetadata: name: app-access namespace: productionspec: podSelector: matchLabels: app: app policyTypes: - Ingress - Egress ingress: # Accept from Ingress controller only - from: - namespaceSelector: matchLabels: name: ingress-nginx podSelector: matchLabels: app.kubernetes.io/name: ingress-nginx ports: - protocol: TCP port: 8080 egress: # To PostgreSQL - to: - podSelector: matchLabels: app: postgres ports: - protocol: TCP port: 5432 # To Redis - to: - podSelector: matchLabels: app: redis ports: - protocol: TCP port: 6379 # To Kafka - to: - podSelector: matchLabels: app: kafka ports: - protocol: TCP port: 9092 # To external APIs (specific domains via Egress) - to: - namespaceSelector: {} # Allow to external ports: - protocol: TCP port: 443 # HTTPS only # DNS (required for name resolution) - to: - namespaceSelector: {} podSelector: matchLabels: k8s-app: kube-dns ports: - protocol: UDP port: 53 # Metrics scraping (for Prometheus) - to: - namespaceSelector: matchLabels: name: monitoring podSelector: matchLabels: app: prometheus ports: - protocol: TCP port: 9090DNS Policy
All pods need egress to kube-dns (cluster DNS) for hostname resolution. Include this in every policy that allows egress:
egress:# Always include this for DNS- to: - namespaceSelector: {} podSelector: matchLabels: k8s-app: kube-dns ports: - protocol: UDP port: 53Without this, pods can't resolve service names like postgres.production.svc.cluster.local.
Monitoring and Prometheus Scraping Exception
Prometheus scrapes metrics from pods across the cluster. Allow egress for metrics collection:
apiVersion: networking.k8s.io/v1kind: NetworkPolicymetadata: name: app-with-metrics namespace: productionspec: podSelector: matchLabels: app: app policyTypes: - Egress egress: # ... other rules ... # Allow Prometheus scraping (egress to monitoring) - to: - namespaceSelector: matchLabels: name: monitoring podSelector: matchLabels: app: prometheus ports: - protocol: TCP port: 9090And on the Prometheus side, allow scraping:
apiVersion: networking.k8s.io/v1kind: NetworkPolicymetadata: name: prometheus-scrape namespace: monitoringspec: podSelector: matchLabels: app: prometheus policyTypes: - Egress - Ingress ingress: # Accept metrics from all pods - from: - namespaceSelector: {} ports: - protocol: TCP port: 9090 egress: # Scrape targets in all namespaces - to: - namespaceSelector: {} ports: - protocol: TCP port: 8080 # Common app metrics port - protocol: TCP port: 9100 # Node exporterTesting Network Policies
Verify Policy is Active
# List all network policieskubectl get networkpolicies -n production # Describe a specific policykubectl describe networkpolicy app-access -n production # Check if a policy selector matches your podkubectl get pod -n production -L appTest Connectivity
Use kubectl run with a test pod:
# Create temporary test podkubectl run test-pod \ --image=gcr.io/distroless/base \ --rm -it \ --namespace=production \ -- /bin/bash # Inside test pod, try connectingnc -zv postgres.production.svc.cluster.local 5432 # Should succeednc -zv redis.production.svc.cluster.local 6379 # Should succeednc -zv google.com 80 # Should fail (blocked)Audit Denied Connections
Check kubelet logs for denied connections:
# On the node running the podjournalctl -u kubelet -f | grep "NetworkPolicy" # Or check iptables rules (on the node)iptables-save | grep -i "policy\|chain"Some CNI plugins (Calico, Cilium) provide better audit logging:
# Calico: View denied flowskubectl logs -n calico-system deployment/calico-node | grep "DENY" # Cilium: Check Hubble flow logshubble observe --type=dropCross-Namespace Communication
Allow traffic between namespaces using namespaceSelector:
apiVersion: networking.k8s.io/v1kind: NetworkPolicymetadata: name: app-to-shared-services namespace: productionspec: podSelector: matchLabels: app: app policyTypes: - Egress egress: # Allow to shared services in different namespace - to: - namespaceSelector: matchLabels: purpose: shared-services podSelector: matchLabels: app: logging ports: - protocol: TCP port: 24224 # FluentdThen label the shared namespace:
kubectl label namespace shared-services purpose=shared-servicesTroubleshooting Network Policies
Connection Fails After Applying Policies
Symptom: Pod can't reach database after policy applied.
Check:
- Is the policy selector matching your pod? Check with
kubectl get pod -L app - Is the target pod labeled correctly? Check with
kubectl get pod postgres-0 -o jsonpath='{.metadata.labels}' - Are you missing DNS egress? Add UDP 53 to kube-dns.
# Verify policy matcheskubectl get pod app-123abc -o jsonpath='{.metadata.labels}'# Output should include labels matching podSelector # Verify target is reachablekubectl exec pod/app-123abc -- nc -zv postgres.production.svc.cluster.local 5432Too Permissive After Testing
Start restrictive, then relax step by step. Never commit overly permissive policies to production.
# BAD: allows all ingressingress:- from: [] # Empty = allow from anywhere # GOOD: allow only specific podsingress:- from: - podSelector: matchLabels: app: appPolicy Not Enforcing
Ensure your CNI supports network policies:
# Check CNIkubectl get nodes -o wide | grep -i cni # Flannel doesn't support network policies# Use Calico, Cilium, Weave, or other CNI with policy support