Safeguard
Container Security

Kubernetes network policies for zero trust

Kubernetes network policies default to allow-all. Here is how default-deny rules, CNI enforcement, and policy testing build real zero-trust segmentation.

Michael
Cloud Security Architect
7 min read

Kubernetes ships with an open network by default: every pod in a cluster can reach every other pod, every namespace, and often the node's metadata service, unless you tell it otherwise. That default has been true since Kubernetes 1.0 in 2015, and it's still true in 1.33 today — the NetworkPolicy API just gives you a way to opt out of it, namespace by namespace. In February 2018, researchers at RedLock found an exposed, unauthenticated Kubernetes dashboard at Tesla that let attackers reach internal pods and run cryptomining software, a direct consequence of flat pod networking with no ingress or egress restrictions in place. Zero trust in Kubernetes means treating every pod-to-pod connection as untrusted until a policy explicitly allows it — and getting there requires more than dropping in a single default-deny YAML file. This post covers what NetworkPolicy resources actually enforce, where they fall short, and how to verify that the rules you wrote are the rules your cluster is running.

What does a Kubernetes NetworkPolicy actually control?

A NetworkPolicy is a namespace-scoped Kubernetes object that uses label selectors — not IP addresses — to allow or deny TCP, UDP, and SCTP traffic between pods, matching on podSelector, namespaceSelector, and ipBlock fields for Ingress and Egress rules. The resource has been part of the stable networking.k8s.io/v1 API since Kubernetes 1.7 shipped in June 2017, but the API only defines the schema — enforcement is delegated entirely to whichever Container Network Interface (CNI) plugin the cluster runs. Apply a NetworkPolicy on kubenet, on Flannel's default VXLAN backend, or on an unconfigured cloud add-on, and kubectl get networkpolicy will happily confirm the object was created, with zero packets actually being blocked. That gap between "object exists" and "traffic is filtered" is the single most common misconfiguration teams run into when they start building network segmentation, and it's invisible unless someone runs a connectivity test from inside a pod that should be blocked.

Why isn't default-deny enough to call your cluster zero trust?

Default-deny closes off traffic you haven't explicitly allowed, but it doesn't verify who is on the other end of an allowed connection — it operates on IP addresses and labels, not cryptographic identity. A podSelector: {} deny-all policy stops a compromised pod from reaching an arbitrary service, but if that pod's labels match an allow-from-frontend rule (because an attacker relabeled it, or because the selector was written too broadly, e.g. app: frontend matching three unrelated deployments), the policy grants access anyway. Native NetworkPolicy also can't do TLS termination, mutual authentication, or Layer 7 filtering by HTTP method or path. CVE-2020-8554, a Kubernetes design flaw disclosed in December 2020, showed how a low-privileged user able to create a ClusterIP service with an ExternalIP field could man-in-the-middle traffic between other pods — a class of attack that label-based ingress/egress rules alone don't address. Real zero trust for east-west traffic needs identity-aware enforcement: Cilium's identity-based policies, keyed to a cryptographic pod identity rather than just labels, or a service mesh like Istio or Linkerd layering mTLS on top of NetworkPolicy's coarser segmentation. Treat NetworkPolicy as the perimeter fence, not the guard checking ID at the door.

How many network policies do you actually need, and how fast does complexity grow?

Most production namespaces need somewhere between 3 and 6 policies per workload — a default-deny-all baseline, an allow-ingress-from-controller rule, an allow-egress-to-DNS rule on UDP/TCP 53 to kube-system, and one allow rule per legitimate downstream dependency — and that count multiplies fast as services grow. Without any policy, a namespace with just 50 pods has up to 2,450 possible pod-to-pod paths (50 × 49 ÷ 2, doubled for bidirectional traffic), every one of which is open by default; add a second, similarly sized namespace with no policy between them and that number roughly quadruples. Teams that write policies reactively, one exception at a time, typically end up with 200-plus NetworkPolicy objects per cluster within a year and no clear mapping of which rule corresponds to which real dependency — a common finding auditors flag under SOC 2 CC6.1 and CC6.6 access-control and change-management controls. The fix isn't more manual YAML; it's generating a baseline from observed traffic — Cilium's Hubble or Calico's flow logs can produce an allow-list automatically — and tightening from there, rather than starting from allow-all and hoping someone locks it down before the next audit.

Which CNI plugin should you run to make policies actually take effect?

You need a CNI plugin that explicitly implements the NetworkPolicy API — Calico, Cilium, Antrea, and Weave Net all enforce it, while plain kubenet and Flannel's default VXLAN backend do not. This is a concrete gap, not a theoretical one: Amazon's VPC CNI didn't support native NetworkPolicy enforcement until version 1.14 shipped in October 2022, adding an eBPF-based --enable-network-policy flag, meaning any EKS cluster built before that release was silently accepting NetworkPolicy objects with no enforcement unless a second CNI like Calico was layered in alongside it. Google's GKE requires Dataplane V2, a Cilium-based dataplane that reached general availability in April 2021, for native enforcement — clusters still on the older Dataplane V1 need Calico as an add-on to get the same behavior. Cilium enforces policy in the kernel via eBPF rather than iptables, which scales better past a few thousand rules and adds Layer 7 policy for HTTP, gRPC, and Kafka on top of Layer 3/4 filtering. Before writing a single policy, confirm which CNI version you're running and whether policy enforcement is actually active — this one check catches the most common false sense of security in Kubernetes clusters today.

How do you verify that a deployed policy is actually being enforced?

You verify enforcement by generating real traffic from inside the cluster and confirming it's blocked or allowed exactly as the policy states, not by reading the YAML back. Cilium ships a cilium connectivity test suite that runs dozens of pod-to-pod, pod-to-world, and DNS scenarios against your live policy set; Calico has an equivalent calicoctl policy check; and both can run in CI so a pull request that changes a NetworkPolicy gets tested automatically before merge instead of being validated for the first time in production. A minimal manual check — exec into a pod that should be denied and run curl --max-time 3 <target>:<port> — is enough to catch the CNI-not-enforcing scenario described above, and it's worth running on every CNI or control-plane upgrade: CVE-2019-11247, disclosed in August 2019, showed how a Kubernetes API server bug let cluster-scoped resources be reached through namespaced RBAC roles, a reminder that control-plane changes can quietly shift what a "restricted" path actually allows. Treat a NetworkPolicy edit the same way you'd treat a firewall rule change: log the diff, test the specific path it was meant to open or close, and keep that test in the pipeline so drift gets caught the next time someone edits the rule.

How Safeguard Helps

Safeguard maps the workloads your NetworkPolicy rules protect back to the vulnerabilities and packages actually running inside them, so segmentation work gets prioritized around what's truly reachable instead of every CVE sitting in an SBOM. Our reachability analysis traces whether a vulnerable function in a container image is invoked in the code path exposed by an allowed network route, cutting through noise from policies written to guard code nothing ever calls. Griffin, Safeguard's AI security analyst, reviews NetworkPolicy manifests and Kubernetes RBAC configuration alongside SBOMs — generated automatically from every build or ingested from existing SPDX/CycloneDX pipelines — to flag over-broad label selectors, missing default-deny baselines, and CNI enforcement gaps like the ones above. When Griffin finds a fix, Safeguard opens an auto-fix pull request with the corrected policy or ingress rule, so tightening a namespace's zero-trust posture takes a code review, not a change-management ticket.

Never miss an update

Weekly insights on software supply chain security, delivered to your inbox.