Skip to main content
CleanStart

Knowledge Hub

Network Policies for Container Security

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  - Egress

This 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: 53

Postgres 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: 53

Kafka: 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: 53

Nginx 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: 53

Application 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: 9090

DNS 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: 53

Without 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: 9090

And 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 exporter

Testing 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 app

Test 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=drop

Cross-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  # Fluentd

Then label the shared namespace:

kubectl label namespace shared-services purpose=shared-services

Troubleshooting Network Policies

Connection Fails After Applying Policies

Symptom: Pod can't reach database after policy applied.

Check:

  1. Is the policy selector matching your pod? Check with kubectl get pod -L app
  2. Is the target pod labeled correctly? Check with kubectl get pod postgres-0 -o jsonpath='{.metadata.labels}'
  3. 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 5432

Too 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: app

Policy 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