NetworkPolicy vs AuthorizationPolicy: One Decides If the Call Connects, the Other Decides If Anyone Answers
Table of Contents
Kubernetes gives you two resources that both look like “firewall rules for pods,” and most teams deploy one and assume they got the other for free. They didn’t. The two policies operate at different layers, speak different languages, and fail in completely different ways — and you almost certainly need both.
The Setup
Picture a cluster running a service mesh (Istio, in my case) with a typical three-tier layout: an ingress gateway, a handful of API services, and a PostgreSQL database listening on port 5432. The security requirement sounds simple: only the API services should reach the database, and only the gateway should reach the APIs. The first instinct is to reach for a policy — and Kubernetes hands you two that both claim to do this. NetworkPolicy ships with Kubernetes itself (enforced by your CNI — Cilium, Calico). AuthorizationPolicy ships with Istio (enforced by the Envoy sidecar). Same goal on paper, very different machinery underneath.
The Approach
Here’s the analogy that finally made it click for me.
NetworkPolicy is “who I want to talk to.” It’s the phone line. It decides whether a connection can even be dialed — at L3/L4, using IPs, ports, and pod label selectors. When a NetworkPolicy blocks traffic, the packets never arrive. The caller doesn’t get a busy signal or a polite refusal; the line simply doesn’t exist. The failure mode is a connection timeout — your client hangs for 30 seconds and dies with no explanation, because at layer 3 there’s nobody home to explain anything.
AuthorizationPolicy is “you can talk to me.” It’s the receptionist. The call connects, TLS completes, and then someone checks your ID badge and decides whether you get past the lobby. It operates at L7, using workload identity — the SPIFFE identity baked into the mTLS certificate (spiffe://cluster.local/ns/api/sa/orders-service), plus HTTP-level details like method and path. When an AuthorizationPolicy blocks you, you get a crisp 403 RBAC: access denied in under a millisecond. The connection worked fine; you were the problem.
The phrasing in the analogy matters more than it looks. NetworkPolicy is written from the perspective of the pod’s network plumbing: “I want to be reachable from pods labeled app=api on port 5432, and nothing else.” AuthorizationPolicy is written from the perspective of the service receiving the request: “you — the workload presenting this specific service account identity — can talk to me, but only with GET on /orders/*.” One controls reachability, the other controls permission. A pod can be reachable but not permitted, and permitted but not reachable.
Why does identity beat labels and IPs? Because labels and IPs are claims about where something is, not what it is. Pod IPs are ephemeral — they churn on every reschedule — so the CNI has to track label-to-IP mappings in real time, and a compromised pod that lands in the right namespace with the right labels inherits the right network access. The mTLS identity in an AuthorizationPolicy is cryptographic: it’s tied to the pod’s service account and verified on every single request. An attacker who steals a pod’s network position gets nothing without also stealing its certificate.
So why not skip NetworkPolicy entirely and go all-in on the mesh? Two reasons. First, AuthorizationPolicy only governs traffic that flows through the sidecar — a pod with sidecar injection disabled, or traffic on a protocol Envoy doesn’t parse, slips past the receptionist entirely. NetworkPolicy is enforced in the kernel by the CNI and catches everything, including the pods that opted out of the mesh. Second, defense in depth: if a CVE in the proxy lets someone bypass L7 checks, the L4 policy means they still can’t open a socket to the database. I run default-deny at both layers — a NetworkPolicy that drops all ingress unless explicitly allowed, and a mesh-wide AuthorizationPolicy with an empty spec that denies all requests unless a more specific policy permits them. Every new service has to earn both its phone line and its lobby badge.
What I Got Wrong
The first time I layered these, I wrote the AuthorizationPolicy, tested it, watched requests succeed — and assumed I was done. Weeks later, a batch job in another namespace connected straight to PostgreSQL on 5432. The mesh never saw it: PostgreSQL’s wire protocol isn’t HTTP, the job’s pod wasn’t meshed, and my L7 receptionist was guarding a lobby the traffic never entered. A single 12-line NetworkPolicy restricting database ingress to app=api pods would have stopped it cold. The reverse mistake bit a teammate: NetworkPolicy only, on the theory that “if you can’t connect, you can’t attack.” Then one API service got compromised via a dependency, and because it had a phone line to everything in its tier, lateral movement was free — every request it made looked legitimate at L4 because L4 has no concept of which workload is asking or what it’s asking for.
Debugging is where the two-layer model pays for itself daily. Connection timeout? That’s the phone line — check NetworkPolicy and CNI logs. Instant 403 with RBAC: access denied? That’s the receptionist — check AuthorizationPolicy and the caller’s service account identity. Knowing which layer produces which symptom turns a “networking is broken” mystery into a two-minute diagnosis.
Takeaways
- NetworkPolicy = “I want to talk to” (the phone line). L3/L4, IPs/ports/labels, enforced by the CNI in the kernel. Blocked traffic times out silently.
- AuthorizationPolicy = “you can talk to me” (the receptionist). L7, cryptographic workload identity plus HTTP method/path, enforced by the sidecar. Blocked traffic gets a fast, explicit 403.
- They are not substitutes. The mesh policy misses unmeshed pods and non-HTTP protocols; the network policy can’t see identity or verbs. Run default-deny at both layers.
- Use the failure mode as a diagnostic. Timeout → L4 problem.
RBAC: access denied→ L7 problem. This alone will save you hours.