Kubernetes NetworkPolicies: why east-west traffic is wide open by default
A freshly installed Kubernetes cluster, with no NetworkPolicy applied anywhere, has no network restrictions between pods at all. Any pod can open a TCP or UDP connection to any other pod in the cluster, regardless of which namespace each one lives in, with no identity check and no authorization at the network layer. A pod in the frontend namespace can connect directly to a database running in the data namespace, and nothing at the networking layer will stop it. This isn't a misconfiguration someone forgot to fix — it's the behavior the Kubernetes networking model defines as its starting point.
The reason is that Kubernetes specifies the networking model itself — every pod gets its own IP, every pod can talk to every other pod with no NAT in between — but it says nothing about who is allowed to talk to whom. That restriction is the explicit job of the NetworkPolicy resource, and a NetworkPolicy only has any effect if the cluster's CNI plugin knows how to enforce it. Some CNI plugins commonly used in simple setups, Flannel in its default configuration among them, don't enforce NetworkPolicies at all: the resource can be created without any error, and it blocks nothing. Before designing any policy, the first thing we check is whether the cluster's CNI — Calico, Cilium, or the cloud provider's managed networking plugin — actually supports and enforces NetworkPolicies, because writing policies the plugin silently ignores is worse than writing none at all: it creates the false impression that traffic is under control.
What "east-west" means here
East-west traffic is traffic between services inside the cluster, as opposed to north-south traffic, which enters or leaves the cluster from the outside world. Most of an organization's network security effort goes toward the perimeter: firewalls, WAFs, ingress rules. East-west traffic almost never gets the same attention, and it's exactly what an attacker who has already compromised one pod — through a vulnerable dependency, an image running an outdated library, a container with excess permissions — will use to move laterally toward services holding more sensitive data. If every pod can talk to every other pod, compromising the cluster's most exposed and least critical service is, in practical terms, equivalent to having network access to everything else.
Building a default-deny policy
The reasonable starting point isn't writing a policy per service — it's inverting the model: deny all east-west traffic by default, then explicitly open only the connections each service actually needs.
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: default-deny-all
namespace: production
spec:
podSelector: {}
policyTypes:
- Ingress
- Egress
An empty podSelector selects every pod in the namespace. With no ingress or egress rules defined, and both policy types active, the effect is that no pod in that namespace can initiate or receive any connection, except for whatever other NetworkPolicies explicitly allow. NetworkPolicies are additive: if even one policy selecting a pod allows a specific kind of traffic, that traffic is allowed, no matter how many deny policies also apply to the same pod.
What broke the first time
The first time we applied a policy like this in a namespace carrying real traffic, the symptom wasn't a connection cleanly rejected with an obvious error. It was worse: DNS timeouts started showing up in pods that, until that point, had been working fine. Any call that depended on resolving a name — whether an internal service by its short name, or an external domain like a payment provider's — would hang for several seconds before failing.
The cause is that the default-deny policy blocks egress indiscriminately, and resolving a DNS name is itself a network connection: a UDP packet (or TCP, if the response doesn't fit in a single datagram) sent to CoreDNS, which normally runs as a Deployment in the kube-system namespace. With no rule explicitly allowing that traffic, every pod in the namespace lost the ability to resolve any name at all, internal or external. The cluster kept working fine from the control plane's point of view, but any application in that namespace that didn't already have an open connection and a cached IP simply stopped being able to talk to anything outside itself.
The fix is an additional policy, additive to the deny rule, that explicitly allows egress traffic to CoreDNS:
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: allow-dns-egress
namespace: production
spec:
podSelector: {}
policyTypes:
- Egress
egress:
- to:
- namespaceSelector:
matchLabels:
kubernetes.io/metadata.name: kube-system
ports:
- protocol: UDP
port: 53
- protocol: TCP
port: 53
This rule needs to exist before any default-deny policy goes live in a namespace with active traffic, not after. Applying the deny first and the DNS allow only afterward means accepting, in practice, a window of total outage while someone scrambles to fix it.
Other flows that quietly fall through the cracks
DNS is the first thing to break because it affects everything, but it isn't the only traffic a default-deny policy cuts off without warning. Prometheus metrics scraping, when the server runs in a separate observability namespace from the application, needs an explicit ingress rule allowing it from that namespace into each pod's metrics port. Admission webhooks and service-mesh sidecars that depend on cross-namespace calls run into the same problem. And traffic to the Kubernetes API server itself — needed by any controller or application using the Kubernetes client to read or modify cluster resources — also gets blocked if egress is denied by default with no exception carved out for the API server's IP range.
None of these cases produce an immediate, explicit error the moment the policy is applied. The error shows up minutes or even hours later, when something that depended on that traffic actually needs to use it: a pod restarts and can't resolve the database's hostname, or Prometheus marks the service as a down target even though the service itself has nothing wrong with it.
What we check before applying a default-deny policy
Before applying a default-deny-all policy in a namespace with active traffic, we first confirm the cluster's CNI actually enforces NetworkPolicies rather than just accepting them with no effect; we write and apply the egress rule to CoreDNS in the same change as the deny, never as a follow-up step; we explicitly list every cross-namespace dependency the service has — metrics scraping, webhooks, API server calls, service-mesh sidecars — and write the matching allow rule before turning the deny on; and we roll the change out first in a staging namespace carrying synthetic traffic that exercises those same flows, because a production namespace with no prior NetworkPolicy gives no warning signal until the policy is already live and something has already stopped responding.