Kubernetes Network Policies Explained: Secure Pod-to-Pod Communication (2026 Guide)

how to restrict pod communication in kubernetes

By default, a Kubernetes cluster is a flat network. Any pod can reach any other pod, on any port, in any namespace โ€” a frontend pod can talk directly to a database pod, a compromised sidecar in one namespace can probe every service in another, and a single vulnerable container can become a launchpad for lateral movement across the entire cluster. Network Policies are the native Kubernetes mechanism to close that gap, and understanding them properly is one of the highest-leverage security skills for anyone operating a multi-tenant or production cluster.

This guide explains how Network Policies actually work under the hood, why they depend entirely on your CNI plugin, and walks through building a real default-deny, least-privilege network model โ€” from a single namespace lockdown to full cluster-wide segmentation.

Table of Contents

  1. Why Kubernetes Networking Is Flat by Default
  2. What a Network Policy Actually Controls
  3. CNI Requirements โ€” The Part Everyone Forgets
  4. Architecture Overview
  5. Prerequisites
  6. Step 1 โ€” Verify Your CNI Supports Network Policies
  7. Step 2 โ€” Deploy a Default-Deny Policy
  8. Step 3 โ€” Allow Specific Ingress Traffic
  9. Step 4 โ€” Allow Specific Egress Traffic
  10. Step 5 โ€” Namespace Isolation
  11. Step 6 โ€” Allow DNS Resolution
  12. Step 7 โ€” Verify Policy Enforcement
  13. Comparing CNI Plugins for Network Policy Support
  14. Common Network Policy Patterns
  15. Network Policies vs Service Mesh
  16. Monitoring and Auditing Policy Behavior
  17. Security Best Practices
  18. Performance Considerations
  19. Troubleshooting Guide
  20. Conclusion
  21. FAQ

Why Kubernetes Networking Is Flat by Default

Kubernetes was designed around the principle that every pod gets its own IP address and can, in principle, reach every other pod’s IP address directly โ€” no NAT, no port mapping, no manual routing rules. This model, defined by the Kubernetes networking requirements, makes service discovery and load balancing simple, but it comes with an implicit security trade-off: connectivity is the default state, not isolation.

That means immediately after kubectl apply-ing a fresh application stack, a pod in the dev namespace can open a raw TCP connection to a pod in the payments namespace unless something explicitly stops it. For a single-tenant hobby cluster this rarely matters. For any cluster running multiple teams, multiple trust levels, or handling sensitive data, it is a real attack surface โ€” and it is one that is entirely invisible until someone tests it.

Network Policies exist to invert that default: instead of “allow everything unless denied,” a properly configured cluster enforces “deny everything unless explicitly allowed.”

What a Network Policy Actually Controls

A NetworkPolicy object is a Kubernetes-native resource that selects a group of pods (via label selectors) and defines rules for:

  • Ingress โ€” which sources are allowed to send traffic to the selected pods, and on which ports.
  • Egress โ€” which destinations the selected pods are allowed to send traffic to, and on which ports.

Critically, Network Policies are additive and pod-scoped, not global firewall rules. A pod with no Network Policy selecting it remains fully open. The moment any policy selects that pod for a given direction (ingress or egress), that direction becomes default-deny for that pod, and only traffic matching an explicit rule is allowed. Multiple policies selecting the same pod are combined with an OR โ€” traffic is allowed if it matches any applicable policy.

CNI Requirements โ€” The Part Everyone Forgets

This is the single most common source of confusion: the Kubernetes API server will happily accept and store a NetworkPolicy object even if nothing in the cluster actually enforces it.

Network Policy enforcement is implemented by the CNI (Container Network Interface) plugin, not by kube-apiserver or kube-controller-manager. If your cluster runs a CNI without policy support โ€” the most common trap being a default flannel installation โ€” every NetworkPolicy you apply is silently ignored, and traffic keeps flowing exactly as before. There is no warning, no error, no event. The policy simply does nothing.

Before writing a single policy, confirm your CNI actually enforces them. This is covered concretely in Step 1 below.

Architecture Overview

The target architecture this guide builds โ€” a three-tier application with default-deny isolation โ€” looks like this:

                        โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
                        โ”‚              Namespace: production              โ”‚
                        โ”‚                                                   โ”‚
   Internet โ”€โ”€โ–ถ Ingress โ”‚   โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”  allow   โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”          โ”‚
   Controller โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ–ถโ”‚ frontend  โ”‚โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ–ถโ”‚  backend   โ”‚          โ”‚
                        โ”‚   โ”‚  pods     โ”‚  :8080    โ”‚   pods     โ”‚          โ”‚
                        โ”‚   โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜           โ””โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”˜          โ”‚
                        โ”‚                                    โ”‚ allow :5432  โ”‚
                        โ”‚                          โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ–ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”   โ”‚
                        โ”‚                          โ”‚   postgres pod     โ”‚   โ”‚
                        โ”‚                          โ”‚  (default-deny,    โ”‚   โ”‚
                        โ”‚                          โ”‚   only backend     โ”‚   โ”‚
                        โ”‚                          โ”‚   may connect)     โ”‚   โ”‚
                        โ”‚                          โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜   โ”‚
                        โ”‚                                                   โ”‚
                        โ”‚   All other pod-to-pod traffic: DENIED by         โ”‚
                        โ”‚   default-deny-all NetworkPolicy                  โ”‚
                        โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜
                                          โ”‚
                                          โ”‚ DNS (kube-dns/CoreDNS) explicitly allowed
                                          โ–ผ
                              โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
                              โ”‚ kube-system namespace โ”‚
                              โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜

Each tier can only reach the tier immediately below it, on the specific port it needs, and nothing else โ€” including no direct frontend-to-database access, even though nothing in Kubernetes prevents that connection by default.

Prerequisites

  • A Kubernetes cluster (v1.28+) running a CNI plugin with Network Policy support โ€” Calico, Cilium, or Weave Net are the most common choices. (Managed clusters: enable it explicitly โ€” EKS requires the Calico add-on or Cilium; GKE requires enabling “Network Policy” at cluster creation; AKS supports both Azure and Calico network policy engines.)
  • kubectl access with permission to create NetworkPolicy resources.
  • A sample multi-tier application (frontend, backend, database) to test policies against โ€” or reuse the PostgreSQL StatefulSet from bckinfo.com’s PostgreSQL on Kubernetes deployment guide as the database tier.

Step 1 โ€” Verify Your CNI Supports Network Policies

kubectl get pods -n kube-system -o wide

Look for a Calico (calico-node), Cilium (cilium), or Weave Net (weave-net) DaemonSet running on every node. If you only see kube-proxy and a CNI plugin known not to enforce policies (plain flannel is the classic case), install a policy-capable CNI or layer Calico on top of flannel using Canal before continuing โ€” otherwise every step below will apply cleanly but change nothing.

A quick functional test: apply a strict default-deny policy (Step 2) and confirm that traffic actually gets blocked. If it doesn’t, the CNI is the problem, not the YAML.

Step 2 โ€” Deploy a Default-Deny Policy

Start every namespace with a deny-all baseline, then add exceptions. This inverts the insecure default and is the single most impactful change in this guide.

# default-deny-all.yaml
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
  name: default-deny-all
  namespace: production
spec:
  podSelector: {}
  policyTypes:
    - Ingress
    - Egress
kubectl apply -f default-deny-all.yaml

An empty podSelector: {} matches every pod in the namespace. With both Ingress and Egress listed under policyTypes and no rules defined, this blocks all inbound and outbound traffic for every pod in production โ€” including traffic to DNS, which must be explicitly re-allowed in Step 6.

Step 3 โ€” Allow Specific Ingress Traffic

Allow the backend pods to receive traffic only from frontend pods on port 8080:

# allow-frontend-to-backend.yaml
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
  name: allow-frontend-to-backend
  namespace: production
spec:
  podSelector:
    matchLabels:
      app: backend
  policyTypes:
    - Ingress
  ingress:
    - from:
        - podSelector:
            matchLabels:
              app: frontend
      ports:
        - protocol: TCP
          port: 8080
kubectl apply -f allow-frontend-to-backend.yaml

The podSelector under spec chooses which pods this policy applies to (the destination); the podSelector nested under from chooses which pods are allowed to initiate traffic (the source). Mixing these two up is one of the most common Network Policy authoring mistakes.

Step 4 โ€” Allow Specific Egress Traffic

Allow backend pods to reach the postgres pod on port 5432, and nothing else outbound:

# allow-backend-to-postgres.yaml
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
  name: allow-backend-to-postgres
  namespace: production
spec:
  podSelector:
    matchLabels:
      app: backend
  policyTypes:
    - Egress
  egress:
    - to:
        - podSelector:
            matchLabels:
              app: postgres
      ports:
        - protocol: TCP
          port: 5432
kubectl apply -f allow-backend-to-postgres.yaml

Combined with Step 2’s default deny, backend pods can now only send traffic to postgres on 5432 and receive traffic from frontend on 8080 โ€” every other path is closed.

Step 5 โ€” Namespace Isolation

To restrict traffic based on which namespace it comes from โ€” useful for multi-tenant clusters โ€” combine namespaceSelector with podSelector. First, label the namespace:

kubectl label namespace production environment=prod

Then reference it:

# allow-from-prod-namespace-only.yaml
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
  name: allow-from-prod-namespace-only
  namespace: production
spec:
  podSelector:
    matchLabels:
      app: backend
  policyTypes:
    - Ingress
  ingress:
    - from:
        - namespaceSelector:
            matchLabels:
              environment: prod
          podSelector:
            matchLabels:
              app: frontend

When both namespaceSelector and podSelector are listed together under the same from entry, Kubernetes requires both to match โ€” the source pod must have the app: frontend label and live in a namespace labeled environment: prod. Listing them as separate entries in the from list, by contrast, would mean either condition matching is sufficient โ€” a subtle but important distinction.

Step 6 โ€” Allow DNS Resolution

This step is skipped constantly and causes the most confusing “everything is broken” support tickets. Under a default-deny egress policy, pods can no longer resolve DNS, because DNS queries to CoreDNS in kube-system are themselves blocked.

# allow-dns.yaml
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
  name: allow-dns
  namespace: production
spec:
  podSelector: {}
  policyTypes:
    - Egress
  egress:
    - to:
        - namespaceSelector: {}
      ports:
        - protocol: UDP
          port: 53
        - protocol: TCP
          port: 53
kubectl apply -f allow-dns.yaml

Applying this immediately after Step 2, before testing anything else, avoids hours of debugging pods that appear to hang on every outbound connection.

Step 7 โ€” Verify Policy Enforcement

Test from inside a pod that should be blocked:

kubectl run test-pod --rm -it --image=busybox -n production -- \
  wget -qO- --timeout=3 http://postgres-svc:5432

This should time out if test-pod has no label granting it access. Then confirm the allowed path still works:

kubectl exec -it <backend-pod-name> -n production -- \
  nc -zv postgres-svc 5432
kubectl describe networkpolicy -n production

The describe output lists exactly which pods each policy selects and which rules apply โ€” the fastest way to sanity-check a policy set without guessing from raw YAML.

Comparing CNI Plugins for Network Policy Support

CNI PluginPolicy EnforcementLayer 7 RulesPerformance OverheadBest For
Flannel (default)NoneNoN/ASimple clusters with no isolation needs
CalicoFull (native + extended CRDs)Limited (via Calico Enterprise)LowGeneral-purpose production clusters
CiliumFull (eBPF-based)Yes (HTTP, gRPC, Kafka-aware)Very low (eBPF)High-performance, security-focused clusters
Weave NetFull (basic K8s NetworkPolicy)NoModerateSimple production clusters wanting policy support without extra CRDs
Azure CNI + Azure NPMFullNoLowAKS clusters staying within Azure-native tooling

Cilium’s eBPF-based enforcement generally has the lowest per-packet overhead and adds Layer 7-aware policies (e.g., “allow only HTTP GET requests to /api/v1/health“) beyond what the base Kubernetes NetworkPolicy API supports โ€” though that extra capability requires Cilium-specific CiliumNetworkPolicy CRDs rather than the portable native API.

Common Network Policy Patterns

  • Deny-all baseline per namespace โ€” always the first policy applied, as shown in Step 2.
  • Allow DNS โ€” always the second policy applied, as shown in Step 6.
  • Tier-to-tier allow rules โ€” frontend โ†’ backend โ†’ database, each restricted to the specific port needed.
  • Deny egress to the internet โ€” for pods that should never make outbound calls (e.g., a database pod), omit any egress rule beyond DNS and intra-cluster traffic.
  • Allow monitoring scrape traffic โ€” a dedicated policy allowing ingress from the monitoring namespace on the metrics port, so Prometheus can scrape pods without opening them to everything else.
  • Egress allow-list to external APIs โ€” using ipBlock CIDR ranges instead of podSelector when a pod must reach a specific external service.

Network Policies vs Service Mesh

A frequent point of confusion: Network Policies and a service mesh (Istio, Linkerd, Cilium Service Mesh) solve related but distinct problems.

AspectNetwork PolicyService Mesh
OSI LayerLayer 3/4 (IP, port)Layer 7 (HTTP, gRPC paths, headers)
EncryptionNot providedmTLS between pods, typically automatic
Traffic shapingNoYes (retries, timeouts, circuit breaking, canary routing)
Enforcement pointCNI plugin (kernel/eBPF level)Sidecar proxy or eBPF datapath
Operational costLowHigher โ€” sidecars, control plane, more moving parts

They are complementary, not competing: Network Policies decide whether a connection is allowed to exist at all, while a service mesh decides what happens within an allowed connection. Many production clusters run both โ€” Network Policies as the coarse-grained network boundary, and a service mesh for encryption and fine-grained traffic control within that boundary.

Monitoring and Auditing Policy Behavior

  • Cilium Hubble provides real-time flow visibility, showing exactly which connections are allowed or dropped by policy, per pod, in a live UI.
  • Calico’s calicoctl exposes policy hit counters and can run in “audit mode,” logging what would be dropped before switching a policy to enforcing mode โ€” a safer rollout path for tightening an existing cluster.
  • Combine policy audit logs with the monitoring stack described in bckinfo.com’s Prometheus and Grafana setup guide to alert when a pod suddenly starts generating a spike in denied connections, which often signals either a misconfiguration or a compromised container probing the network.

Security Best Practices

  • Apply a default-deny-all policy to every namespace as a baseline, not just the sensitive ones โ€” attackers look for the namespace someone forgot.
  • Never rely on podSelector: {} with no policyTypes restriction thinking it denies traffic; an empty policy with no policyTypes listed has no effect at all.
  • Label pods and namespaces consistently and deliberately โ€” Network Policies are only as precise as the labels they select on.
  • Combine Network Policies with Kubernetes RBAC and Pod Security Standards; policies control network reachability, not what a pod is authorized to do once connected.
  • Test policies in a staging namespace with mirrored labels before rolling them out to production โ€” an overly strict policy silently breaks functionality rather than throwing a visible error.
  • Periodically audit for pods with no policy selecting them at all โ€” these remain fully open even in an otherwise locked-down cluster.

Performance Considerations

Native NetworkPolicy enforcement via iptables (used by some CNI implementations) can add measurable per-packet overhead as the number of rules grows into the hundreds, particularly under high connection-churn workloads. eBPF-based CNIs like Cilium avoid most of this cost by evaluating policy in the kernel data path without traversing long iptables chains, making them the better choice for large clusters with dense policy sets. For most mid-sized clusters, however, the security benefit of default-deny substantially outweighs the marginal latency cost regardless of CNI choice.

Troubleshooting Guide

SymptomLikely CauseFix
Policy applied but traffic still flows freelyCNI does not enforce Network Policies (e.g., plain flannel)Install or switch to a policy-capable CNI (Calico, Cilium, Weave Net)
All outbound traffic suddenly hangs after default-denyDNS not explicitly allowedApply the allow-dns policy from Step 6
Pod can’t reach a service even though a policy allows itpodSelector labels don’t actually match the pod’s labelsRun kubectl get pods --show-labels and compare against the policy’s selector
Two policies conflict / unclear which rule winsMisunderstanding โ€” policies are additive (OR’d), never subtractiveRemember: any matching policy grants access; there is no explicit “deny” rule type in the native API
namespaceSelector isolation not workingNamespace missing the expected labelConfirm with kubectl get ns --show-labels; labels must be applied manually
Monitoring/Prometheus can’t scrape pods after lockdownNo ingress rule allowing the monitoring namespace on the metrics portAdd an explicit ingress rule for the scrape port, sourced from the monitoring namespace
Policy YAML applies with no error but does nothingpolicyTypes omitted, so Kubernetes infers it from ingress/egress fields present โ€” can silently produce unintended scopeAlways explicitly list policyTypes: [Ingress, Egress] even when only one direction has rules

Conclusion

Kubernetes’ flat-network default is convenient for getting a cluster running quickly, but it is not a security posture โ€” it is the absence of one. Network Policies turn that implicit “allow everything” into an explicit, auditable, least-privilege model, and the pattern to get there is consistent regardless of cluster size: confirm your CNI actually enforces policies, apply a default-deny baseline per namespace, immediately re-allow DNS, then add narrow, purpose-specific ingress and egress rules tier by tier.

Treat Network Policies as a mandatory layer alongside RBAC and Pod Security Standards, not an optional hardening step reserved for “later.” The earlier default-deny is established in a namespace’s lifecycle, the fewer legacy “temporarily allow everything” exceptions accumulate โ€” and those exceptions are exactly what attackers rely on.

For related reading on bckinfo.com, see the guides on deploying PostgreSQL on Kubernetes with persistent storage, Docker network security best practices, and Prometheus and Grafana monitoring stack setup for observing policy behavior in production.

FAQ

Do Network Policies work on every Kubernetes cluster?
No. Network Policies are enforced by the cluster’s CNI plugin, not the Kubernetes API server. A cluster running a CNI without policy support โ€” the default flannel configuration being the most common case โ€” will accept NetworkPolicy objects but silently ignore them.

What happens if no Network Policy is applied to a pod?
By default, all traffic between all pods is allowed. A pod becomes restricted only once at least one Network Policy selects it for a given direction (ingress or egress); at that point, only explicitly allowed traffic in that direction passes.

Is a Network Policy the same as a service mesh?
No. Network Policies operate at Layer 3/4, controlling which pods can reach which pods over IP and port. A service mesh like Istio or Linkerd operates at Layer 7, adding mutual TLS, retries, and fine-grained request routing on top of the connectivity Network Policies allow.

Can I use Network Policies to restrict traffic to external IP addresses?
Yes, using ipBlock with a CIDR range instead of a podSelector in the to/from field, though except sub-ranges and dynamic external IPs require careful maintenance as those addresses change.

Which CNI should I choose specifically for Network Policy support?
Calico is the most widely adopted, well-documented choice for standard Kubernetes-native policies. Cilium is the better choice when you also want eBPF-level performance and optional Layer 7-aware policies via its extended CRDs.

(Visited 2 times, 2 visits today)

You may also like