← Writing

Sep 2026 · By Somesh Motupally · 6 min read

Network Policies: Default-Deny Without Breaking Everything

Kubernetes NetworkPolicy Zero Trust Cluster Hardening

The problem

Kubernetes networking is flat by default. Every pod can reach every other pod on any port — and beyond the cluster, kube-dns, the API server, and cloud metadata endpoints are all reachable unless something says otherwise. A compromised public-facing pod isn't contained to its own workload; it's a beachhead with a network view of everything.

We saw this directly during a review: a frontend pod with no business touching the data layer could open a connection to the database port, and nothing in the network path objected. The same pod could reach the cloud metadata endpoint — the classic path to credential theft in a hijacked container. None of this violated any rule, because there were no rules. The policy-as-code gates we'd built controlled what could deploy; they said nothing about who could talk to whom once it was running.

Why lockdown-by-default wasn't enough

The instinctive fix — write NetworkPolicies that allow only what we could think of, apply them everywhere — failed the first time we tried it, and failed loudly. DNS resolution broke the moment kube-dns traffic (UDP/TCP port 53) wasn't in the allow-list. Health checks started failing. Legitimate inter-service calls dropped, and the pages came in at 2 AM. Within days, teams were asking for blanket exceptions, and some policies were quietly removed to stop the bleeding — which left us worse off than before: a network policy story on paper, and a flat network in practice.

The deeper problem was that we were writing rules from guesses about traffic instead of from evidence of it. Nobody had a complete, current map of which service talks to which — and even if we had one, it would have gone stale the next sprint. There was also an uncomfortable discovery along the way: NetworkPolicies are only enforced if your CNI actually implements them. Apply a beautiful policy set on a CNI that silently ignores it, and you have the worst outcome — a false sense of containment.

The solution: default-deny, rolled out in stages

We went back at it with a staged approach: default-deny ingress and egress on every namespace, but rolled out one namespace at a time, with an observation phase before enforcement.

The rollout ran in four steps:

None of this was one big YAML drop. The policy set is versioned alongside the rest of the platform config, and the exception process is narrow and logged: if a workload genuinely needs a new path, the request names the source, destination, port, and reason, and it expires on a timer.

Results

Lessons learned

What's next

IP-and-port rules get us containment, but they're still coarse: they say nothing about which workload is on the other end of an allowed connection. The next step is identity-aware policy — using workload identity (via the service mesh's mTLS identities, or Cilium's identity-based policies) so rules say "the checkout service may talk to the payments API" instead of "anything in namespace A may reach port 443 in namespace B." That's the move from network segmentation to real zero-trust inside the cluster — and the SLSA provenance work from the previous post's roadmap is still queued behind it.


If you've rolled out default-deny NetworkPolicies without taking down production, I'd like to hear how you handled the observation phase — get in touch.

Share this post: X LinkedIn

More from PipelineClear