7.3 NetworkPolicies, Zero-Trust Segmentation & the Gateway API
Key Takeaways
- Kubernetes networking is allow-all by default; a Pod becomes isolated only once at least one NetworkPolicy selects it, and then only for the policyTypes listed.
- NetworkPolicy rules are purely additive allow rules — the API has no deny rule, so isolation is achieved by writing a default-deny policy first.
- podSelector, namespaceSelector, and ipBlock choose traffic sources and destinations; combining podSelector and namespaceSelector in one rule element means AND, while separate list elements mean OR.
- Egress policies that block DNS break every workload in the namespace, so a default-deny egress policy must always allow UDP and TCP port 53 to kube-dns.
- The Gateway API is the role-oriented successor to Ingress, splitting infrastructure concerns (GatewayClass, Gateway) from application routing (HTTPRoute) and supporting protocols beyond HTTP.
7.3 NetworkPolicies, Zero-Trust Segmentation & the Gateway API
Quick Answer: A default Kubernetes cluster is flat and completely open — any Pod can reach any other Pod in any namespace. A NetworkPolicy changes that, but only for the Pods it selects and only for the directions it names. Policies contain allow rules only, so real segmentation starts with an explicit default-deny policy. Enforcement lives in the CNI plugin, not in Kubernetes. For inbound traffic, the Gateway API is the role-oriented successor to Ingress.
Section 7.1 introduced NetworkPolicy in outline. This section covers the semantics that actually decide whether a policy protects you or silently does nothing.
1. The Default Is Allow-All
Out of the box there is no network segmentation:
[ frontend/prod ] ──✓──► [ database/prod ]
[ intern-sandbox ] ─✓──► [ database/prod ] ← also allowed by default
[ any pod ] ─✓──► [ any pod, any namespace ]
Namespaces are an RBAC and naming boundary. They are not a network boundary. A compromised Pod in a sandbox namespace can open a TCP connection to a production database unless a NetworkPolicy says otherwise.
2. The Two Rules That Govern Every Policy
Rule 1 — Selection creates isolation. A Pod is non-isolated until at least one NetworkPolicy selects it. Once any policy selects it, that Pod is isolated for the directions named in policyTypes, and only traffic explicitly allowed by some policy gets through.
Rule 2 — Policies are additive allow rules. There is no deny rule in the API. Multiple policies selecting the same Pod are unioned: if any policy allows the traffic, it is permitted. You cannot write "allow everything except X" — you enumerate what is allowed.
The consequence is the default-deny pattern. An empty podSelector: {} selects every Pod in the namespace, and omitting ingress: allows nothing:
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: default-deny-all
namespace: production
spec:
podSelector: {} # every Pod in this namespace
policyTypes: [Ingress, Egress] # isolate both directions
# no ingress or egress rules ⇒ nothing is permitted
With that in place you add narrow allow policies on top.
3. Selecting Traffic
Three peer selectors exist, and a subtle AND/OR distinction between them is a reliable exam item.
spec:
podSelector:
matchLabels: { app: database }
policyTypes: [Ingress]
ingress:
- from:
- podSelector: # element 1 …
matchLabels: { app: api }
namespaceSelector: # … same element ⇒ AND
matchLabels: { env: production }
- ipBlock: # separate element ⇒ OR
cidr: 10.20.0.0/16
except: [10.20.7.0/24]
ports:
- protocol: TCP
port: 5432
podSelectorandnamespaceSelectorinside the same list element means "Pods labelledapp: apithat are in a namespace labelledenv: production".- Two separate list elements are OR'd: this policy allows the api Pods or the CIDR range.
podSelectoralone always means "in the policy's own namespace".namespaceSelector: {}means "all namespaces".
4. Egress and the DNS Trap
Egress policies are where teams break their own clusters. Once a Pod is isolated for egress, all outbound traffic is blocked — including DNS lookups to CoreDNS. Every application then fails with name-resolution errors that look nothing like a firewall problem.
Always pair a default-deny egress with a DNS allowance:
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 }
podSelector:
matchLabels: { k8s-app: kube-dns }
ports:
- { protocol: UDP, port: 53 }
- { protocol: TCP, port: 53 }
5. Enforcement Lives in the CNI
This is the point that most often catches people out in production. kube-apiserver happily accepts and stores a NetworkPolicy regardless of whether anything can enforce it. Enforcement is performed by the CNI plugin's data plane.
| CNI plugin | NetworkPolicy support |
|---|---|
| Calico | Full, plus its own extended GlobalNetworkPolicy |
| Cilium | Full via eBPF, plus L7-aware CiliumNetworkPolicy |
| Weave Net | Supported |
| Flannel | None — policies are accepted by the API and silently ignored |
So kubectl get networkpolicies showing a tidy list proves nothing. On Flannel, every one of those policies is decorative. Verifying enforcement means actually attempting the connection from a test Pod.
Beyond L3/L4
Standard NetworkPolicy operates at layer 3/4 — IPs, ports, protocols. It cannot express "allow GET /health but deny POST /admin". Layer 7 authorisation requires a service mesh (covered in Chapter 9) or a CNI-specific policy CRD such as CiliumNetworkPolicy.
6. The Gateway API
NetworkPolicy governs east-west traffic between Pods. Ingress and the Gateway API govern north-south traffic entering the cluster.
Ingress worked, but it accumulated well-known limits: HTTP/HTTPS only, no standard way to express header-based routing or traffic splitting, and a proliferation of controller-specific annotations that made manifests non-portable. The Gateway API — developed by Kubernetes SIG-Network and now the strategic direction — fixes this by splitting one object into a role-oriented set:
| Resource | Owned by | Purpose |
|---|---|---|
GatewayClass | Infrastructure provider | Which implementation backs a Gateway (Envoy, NGINX, a cloud LB) |
Gateway | Cluster operator | An actual listener: ports, protocols, TLS certificates, allowed routes |
HTTPRoute (also GRPCRoute, TCPRoute, TLSRoute) | Application developer | Hostnames, paths, headers, backends, and weights |
apiVersion: gateway.networking.k8s.io/v1
kind: HTTPRoute
metadata:
name: api-route
namespace: production
spec:
parentRefs:
- name: public-gateway
namespace: infra
hostnames: ["api.example.com"]
rules:
- matches:
- path: { type: PathPrefix, value: /v2 }
backendRefs:
- { name: api-v2, port: 80, weight: 90 } # native traffic splitting
- { name: api-v3, port: 80, weight: 10 } # canary, no annotations
| Aspect | Ingress | Gateway API |
|---|---|---|
| Protocols | HTTP/HTTPS | HTTP, HTTPS, gRPC, TCP, TLS, UDP |
| Traffic splitting | Controller annotations | Native weight field |
| Header/method matching | Annotations | Native matches |
| Role separation | One object, one owner | Three objects, three owners |
| Cross-namespace routing | Awkward | Explicit, with ReferenceGrant |
Ingress is not deprecated and remains widely deployed, but new platform work is expected to target the Gateway API.
A cluster has no NetworkPolicy objects at all. What traffic is permitted between Pods?
A team applies a default-deny egress NetworkPolicy to a namespace and every application immediately fails with name-resolution errors. What was omitted?
Which Gateway API resource is intended to be owned by the application developer rather than the cluster operator?