Kubernetes Network Policies: Securing Pod-to-Pod Communication (2026)

By default, every pod in a Kubernetes cluster can communicate with every other pod across all namespaces — an open network model appropriate for development but dangerous in production. Kubernetes Network Policies provide a namespace-scoped firewall mechanism that controls which pods can send and receive traffic. This guide covers the full policy model from deny-all baselines to complex multi-namespace patterns, explains CNI requirements, and provides practical debugging workflows for when policies do not behave as expected.

How Network Policies Work

Network Policies are additive and whitelist-based. A pod with no NetworkPolicy selecting it is completely open — all ingress and egress is allowed. Once any NetworkPolicy selects a pod, only traffic explicitly permitted by that (or any other matching) policy is allowed. Multiple policies are unioned, not intersected.

The three types of policy targets are:

  • Ingress — controls incoming traffic to selected pods
  • Egress — controls outgoing traffic from selected pods
  • Both — specify both in the policyTypes array
Note: Network Policies are enforced by the CNI plugin, not by kube-proxy or the Kubernetes API server. If your cluster uses a CNI that does not support NetworkPolicy (such as Flannel without a policy add-on), the policies will be accepted by the API server but silently ignored. Always verify enforcement is active.

CNI Requirements: Calico, Cilium, and Others

The following CNI plugins support Kubernetes NetworkPolicy enforcement:

CNI PluginNetworkPolicyExtended PolicieseBPF Dataplane
CalicoYesGlobalNetworkPolicy, NetworkSetOptional (eBPF mode)
CiliumYesCiliumNetworkPolicy (L7)Yes (default)
Weave NetYesNoNo
AntreaYesClusterNetworkPolicyOptional
FlannelNoNoNo
AWS VPC CNIYes (v1.14+)NoNo

Cilium's CiliumNetworkPolicy extends standard policies to Layer 7, allowing you to restrict HTTP methods, paths, or gRPC service names — not just IP/port. This is particularly powerful for zero-trust microservice architectures.

Default Deny-All Pattern

The security baseline for any production namespace should be a deny-all policy applied first, then explicit allow policies layered on top. Apply these two policies to every new namespace:

# 1. Deny all ingress in the namespace
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
  name: default-deny-ingress
  namespace: payments
spec:
  podSelector: {}      # Selects ALL pods in the namespace
  policyTypes:
    - Ingress
  # No ingress rules = deny all ingress

---
# 2. Deny all egress in the namespace
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
  name: default-deny-egress
  namespace: payments
spec:
  podSelector: {}
  policyTypes:
    - Egress
  # No egress rules = deny all egress

Or combine them into a single policy:

apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
  name: default-deny-all
  namespace: payments
spec:
  podSelector: {}
  policyTypes:
    - Ingress
    - Egress
Tip: Apply deny-all policies using a namespace controller or GitOps automation so every newly created namespace gets the baseline immediately. Without automation, there is a race window between namespace creation and policy application during which pods are fully open.

podSelector and namespaceSelector

Selectors are the core matching mechanism. They use standard Kubernetes label selectors:

apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
  name: allow-frontend-to-backend
  namespace: payments
spec:
  podSelector:
    matchLabels:
      app: payments-backend     # This policy applies TO these pods
  policyTypes:
    - Ingress
  ingress:
    - from:
        - podSelector:
            matchLabels:
              app: payments-frontend   # Allow FROM these pods
          namespaceSelector:
            matchLabels:
              kubernetes.io/metadata.name: payments  # In THIS namespace
      ports:
        - protocol: TCP
          port: 8080
Important: When both podSelector and namespaceSelector appear in the same from list item (as above), they are ANDed — the source must match both. If you put them as separate items in the list, they are ORed. This is a frequent source of bugs.
# AND (pod in specific namespace):
ingress:
  - from:
      - podSelector:
          matchLabels: {app: frontend}
        namespaceSelector:
          matchLabels: {env: production}

# OR (pod anywhere, OR any pod in specific namespace):
ingress:
  - from:
      - podSelector:
          matchLabels: {app: frontend}
      - namespaceSelector:
          matchLabels: {env: production}

Common Production Patterns

Pattern 1: Allow monitoring namespace to scrape all pods

apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
  name: allow-prometheus-scrape
  namespace: payments
spec:
  podSelector: {}    # All pods in payments 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  # metrics endpoint

Pattern 2: Allow only the API gateway to reach backend services

apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
  name: backend-ingress-from-gateway
  namespace: payments
spec:
  podSelector:
    matchLabels:
      tier: backend
  policyTypes:
    - Ingress
  ingress:
    - from:
        - namespaceSelector:
            matchLabels:
              kubernetes.io/metadata.name: ingress-nginx
          podSelector:
            matchLabels:
              app.kubernetes.io/name: ingress-nginx

Pattern 3: Database tier — only accept connections from app pods

apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
  name: postgres-allow-app
  namespace: payments
spec:
  podSelector:
    matchLabels:
      app: postgres
  policyTypes:
    - Ingress
    - Egress
  ingress:
    - from:
        - podSelector:
            matchLabels:
              needs-db: "true"
      ports:
        - protocol: TCP
          port: 5432
  egress: []   # No egress allowed from postgres pods

Egress Rules and External Access

Egress policies are often overlooked but critical for preventing data exfiltration and lateral movement. A common need is to allow DNS resolution and specific external API calls while blocking everything else:

apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
  name: payments-egress
  namespace: payments
spec:
  podSelector:
    matchLabels:
      app: payments-backend
  policyTypes:
    - Egress
  egress:
    # Allow DNS (critical — without this, name resolution fails)
    - ports:
        - protocol: UDP
          port: 53
        - protocol: TCP
          port: 53

    # Allow access to in-cluster services
    - to:
        - namespaceSelector:
            matchLabels:
              kubernetes.io/metadata.name: payments
      ports:
        - protocol: TCP
          port: 5432    # PostgreSQL
        - protocol: TCP
          port: 6379    # Redis

    # Allow Stripe API
    - to:
        - ipBlock:
            cidr: 54.187.174.169/32
      ports:
        - protocol: TCP
          port: 443

ipBlock for External CIDRs

The ipBlock field allows or denies traffic to external IP ranges. It supports except for excluding sub-ranges:

apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
  name: allow-corporate-vpn-ingress
  namespace: payments
spec:
  podSelector:
    matchLabels:
      app: admin-panel
  policyTypes:
    - Ingress
  ingress:
    - from:
        - ipBlock:
            cidr: 10.0.0.0/8          # Allow entire RFC1918 private range
            except:
              - 10.99.0.0/16          # Except the DMZ subnet
      ports:
        - protocol: TCP
          port: 443
Tip: ipBlock applies to traffic after NAT. In cloud environments, pod-to-pod traffic within the cluster uses pod CIDRs, while traffic from load balancers may arrive with the node IP. Test your ipBlock rules with actual traffic rather than relying solely on spec reasoning.

Debugging Network Policies

When policies block traffic unexpectedly, use this systematic approach:

# Step 1: Check what policies apply to a pod
kubectl get netpol -n payments -o yaml

# Step 2: Run a debug pod in the same namespace
kubectl run netdebug --image=nicolaka/netshoot -n payments \
  --labels="app=payments-frontend" -- sleep 3600

# Step 3: Test connectivity from the debug pod
kubectl exec -n payments netdebug -- curl -v http://payments-backend:8080/health

# Step 4: With Cilium, inspect policy enforcement
kubectl exec -n kube-system cilium-xxxxx -- cilium policy get
kubectl exec -n kube-system cilium-xxxxx -- \
  cilium endpoint list | grep "payments"

# Step 5: With Calico, use calicoctl
calicoctl get networkpolicy -n payments -o yaml
# Check effective policy for a specific endpoint
calicoctl get workloadendpoint -n payments -o yaml | grep payments-backend

Cilium provides a policy verdict tool for real-time traffic inspection:

# Monitor policy verdicts (allows and drops)
kubectl exec -n kube-system cilium-xxxxx -- \
  cilium monitor --type policy-verdict --from-pod payments/netdebug

Frequently Asked Questions

Do Network Policies apply to host-networked pods?

No. Pods using hostNetwork: true bypass Network Policies entirely — they share the node's network namespace and use node IP addresses. This is one reason to avoid hostNetwork pods except for infrastructure components like CNI agents. Control access to host-network pods at the node firewall level (iptables/nftables) instead.

Why is my policy blocking DNS even though I didn't target DNS?

When you apply an egress deny-all policy, you must explicitly allow UDP/TCP port 53 to the kube-dns service, otherwise all DNS resolution fails and your pods cannot connect to anything by name. Always include a DNS allow rule in any egress policy. Some teams use a namespaceSelector targeting kube-system with a podSelector for k8s-app: kube-dns for a more targeted rule.

Can I write a policy that applies across all namespaces?

Standard Kubernetes NetworkPolicy is namespace-scoped and cannot span namespaces from a single resource. Calico's GlobalNetworkPolicy and Cilium's CiliumClusterwideNetworkPolicy are cluster-scoped CRDs that address this. Use them to enforce baseline security rules (like blocking all pod-to-node-metadata-service traffic) without duplicating policies in every namespace.

How do I allow all pods within a namespace to communicate freely?

Add a policy that allows all ingress from the same namespace: set from to a namespaceSelector matching the current namespace's name label. Since Kubernetes 1.22, all namespaces automatically get the kubernetes.io/metadata.name label equal to the namespace name, which makes this selector reliable.

What happens to existing connections when I apply a new policy?

Network Policy enforcement is connection-level, and the behavior varies by CNI. Calico and Cilium typically enforce new policies on new connection attempts without dropping established TCP connections mid-stream. However, never rely on this — during policy updates, existing flows may be disrupted depending on the CNI version and kernel dataplane mode. Plan policy rollouts during low-traffic windows for critical services.