By default, every pod in a Kubernetes cluster can talk to every other pod — regardless of namespace, team, or sensitivity. Network Policies are Kubernetes firewall rules that restrict this. They define which pods are allowed to send traffic to which other pods, and which external IPs can reach your services. Without them, a compromised frontend pod can directly connect to your production database.
+++
The Default Problem — Every Pod Talks to Every Pod
When you create a cluster without any NetworkPolicy resources, Kubernetes operates in full-open mode. Every pod, in every namespace, can reach every other pod by IP.
Default cluster — no NetworkPolicy: [frontend pod] ──────────────────→ [payments-db pod] ← Direct DB access[user-service] ──────────────────→ [payments-db pod] ← Should not be allowed[logging-agent] ──────────────────→ [payments-db pod] ← Definitely not allowed A compromised frontend can attempt a SQL injection or data dump directly.Network Policies change this to an explicit allowlist model.
How Network Policies Work
A NetworkPolicy is applied to a set of pods (the target) and defines two things:
- Ingress rules: Which pods/IPs are allowed to send traffic to the target
- Egress rules: Which pods/IPs the target is allowed to send traffic to
As soon as a NetworkPolicy selects a pod, that pod moves from "allow all" to "deny all except what the policy explicitly permits."
Before NetworkPolicy:[any pod] → [payments-db] ✓ allowed After applying a NetworkPolicy that selects payments-db:[any pod] → [payments-db] ✗ denied by default[payments-api pod] → [payments-db] ✓ allowed (explicitly permitted in the policy)COMMON MISTAKE / WARNING**Requirement**: NetworkPolicy enforcement requires a CNI plugin that supports it — Calico, Cilium, or Weave. The default CNI on many clusters (Flannel) does not enforce NetworkPolicy. The policies exist in the API but are silently ignored.
The Most Important Policy — Default Deny All
Start with a deny-all policy for your namespace, then add explicit allow rules. This is the production-safe baseline.
# default-deny-all.yamlapiVersion: networking.k8s.io/v1kind: NetworkPolicymetadata: name: default-deny-all namespace: productionspec: podSelector: {} # Empty selector = applies to ALL pods in this namespace policyTypes: - Ingress - Egress # No ingress or egress rules = deny everythingkubectl apply -f default-deny-all.yaml # After this, ALL pods in 'production' namespace:# - Cannot receive any traffic (ingress blocked)# - Cannot send any traffic (egress blocked)# Now add explicit allow rules for each serviceAllowing Specific Pod-to-Pod Traffic
# allow-payments-api-to-db.yamlapiVersion: networking.k8s.io/v1kind: NetworkPolicymetadata: name: allow-payments-api-to-db namespace: productionspec: podSelector: matchLabels: app: payments-db # This policy protects the DB pods policyTypes: - Ingress ingress: - from: - podSelector: matchLabels: app: payments-api # Only payments-api pods can connect ports: - protocol: TCP port: 5432 # Only on port 5432 (PostgreSQL)Result: [payments-api pod] → port 5432 → [payments-db pod] ✓ ALLOWED[frontend pod] → port 5432 → [payments-db pod] ✗ DENIED[user-service pod] → port 5432 → [payments-db pod] ✗ DENIED[any other pod] → any port → [payments-db pod] ✗ DENIEDCross-Namespace Traffic
At Razorpay or Zerodha, different teams deploy into different namespaces. An API gateway in the gateway namespace needs to reach services in the production namespace.
# allow-gateway-namespace.yamlapiVersion: networking.k8s.io/v1kind: NetworkPolicymetadata: name: allow-from-gateway-ns namespace: productionspec: podSelector: matchLabels: app: payments-api policyTypes: - Ingress ingress: - from: - namespaceSelector: matchLabels: name: gateway # Allow from any pod in the 'gateway' namespace podSelector: matchLabels: app: nginx-ingress # But only from nginx-ingress pods specifically ports: - protocol: TCP port: 8080# Namespaces need a label for namespaceSelector to work# Add a label to the gateway namespace firstkubectl label namespace gateway name=gateway # Verify the labelkubectl get namespace gateway --show-labelsEgress Rules — Controlling Outbound Traffic
Egress rules control what a pod can connect to. This is critical for preventing data exfiltration — a compromised pod calling out to an attacker's server.
# payments-api-egress.yamlapiVersion: networking.k8s.io/v1kind: NetworkPolicymetadata: name: payments-api-egress namespace: productionspec: podSelector: matchLabels: app: payments-api policyTypes: - Egress egress: - to: - podSelector: matchLabels: app: payments-db # Allow outbound to DB ports: - protocol: TCP port: 5432 - to: - podSelector: matchLabels: app: redis-cache # Allow outbound to Redis ports: - protocol: TCP port: 6379 - ports: # Allow DNS resolution (critical — without this, - protocol: UDP # the pod cannot resolve any hostname) port: 53 - protocol: TCP port: 53COMMON MISTAKE / WARNING**Always allow DNS (port 53)** in egress policies. If you block UDP port 53, the pod cannot resolve any DNS names — including `kubernetes.default.svc.cluster.local`. This silently breaks all service discovery and HTTP calls even to pods you've explicitly allowed.
Allowing External Traffic (from Outside the Cluster)
Traffic from outside the cluster arrives through a LoadBalancer or Ingress controller. The Ingress controller's pod needs to be allowed to reach your application pods.
# allow-ingress-controller.yamlapiVersion: networking.k8s.io/v1kind: NetworkPolicymetadata: name: allow-ingress-controller namespace: productionspec: podSelector: matchLabels: app: payments-api policyTypes: - Ingress ingress: - from: - namespaceSelector: matchLabels: kubernetes.io/metadata.name: ingress-nginx # The ingress controller namespace ports: - protocol: TCP port: 8080Allowing Traffic from Specific IP Ranges
For integrating with external payment gateways or banking APIs that have fixed IP ranges:
ingress: - from: - ipBlock: cidr: 103.21.244.0/22 # Cloudflare IP range example except: - 103.21.244.100/32 # Exclude a specific IP within the range ports: - protocol: TCP port: 443Testing That Your Policy Works
Never assume a NetworkPolicy is working. Test it explicitly.
# Deploy a test pod in the same namespacekubectl run test-pod \ --image=busybox \ --rm -it \ --restart=Never \ -n production \ -- sh # Inside the test pod — try connecting to payments-db# Replace 10.0.1.50 with the actual pod IPnc -zv 10.0.1.50 5432# Expected if policy is working: nc: 10.0.1.50 (10.0.1.50:5432): Connection refused# or it simply hangs and times out # To test allowed traffic, run the test pod with the allowed labelkubectl run test-pod \ --image=busybox \ --rm -it \ --restart=Never \ --labels="app=payments-api" \ -n production \ -- sh nc -zv 10.0.1.50 5432# Expected: Connection to 10.0.1.50 5432 port [tcp/postgresql] succeeded!Complete Production Setup — Layer by Layer
# Step 1: Apply default deny to the namespacekubectl apply -f default-deny-all.yaml # Step 2: Allow DNS so pods can resolve service nameskubectl apply -f allow-dns-egress.yaml # Step 3: Allow Ingress controller → app podskubectl apply -f allow-ingress-controller.yaml # Step 4: Allow app pods → databasekubectl apply -f allow-payments-api-to-db.yaml # Step 5: Verify all policies are in placekubectl get networkpolicies -n production # Output:# NAME POD-SELECTOR AGE# default-deny-all <none> 5m# allow-ingress-controller app=payments-api 4m# allow-payments-api-to-db app=payments-db 3m# allow-dns-egress app=payments-api 4mDebugging NetworkPolicy Denials
NetworkPolicy denials are silent by default — a blocked connection just times out, no error message says "blocked by NetworkPolicy." This makes debugging hard.
# Step 1: Check what policies exist and what they selectkubectl get networkpolicies -n production -o wide # Step 2: Describe a specific policy to see its full ruleskubectl describe networkpolicy allow-payments-api-to-db -n production # Step 3: Check pod labels match the policy selectorskubectl get pod payments-api-7d9f8b-xk2p9 -n production --show-labels# Verify 'app=payments-api' label is present # Step 4: If using Cilium, use its built-in observability# kubectl exec -it -n kube-system cilium-xxxxx -- cilium monitor --type drop# This shows real-time dropped packets with the reason🔴 Common Mistake: Applying a NetworkPolicy with a podSelector that does not match any pods due to a label mismatch. The policy is active but protects nothing — all traffic still flows freely. After applying any policy, always run kubectl get networkpolicies -n <namespace> -o wide and verify the POD-SELECTOR column shows the labels you intended.
💡 Tip: At Hotstar or PhonePe scale, use Cilium instead of Calico if you're on a modern cluster. Cilium's network policies extend beyond port/IP rules to HTTP-layer policies — you can write rules like "allow only GET requests to /api/v1/products from the frontend namespace." This is called Layer 7 policy and it is far more expressive than standard Kubernetes NetworkPolicy.