Kubernetes NetworkPolicy on EKS — the flat network gets its walls
Every pod can reach every pod. That is the shipped default, and the shipped default is a decision someone else made for you.
§ IFrame
The K8s arc has climbed four rungs. The reconciliation loop taught how the cluster acts. The admission chain taught how it judges. The Services lesson taught how it names: a stable virtual IP over churning pods, so anything in the cluster can dial postgres.trading.svc and land on a live backend.
Read that with a security eye. Anything in the cluster can dial it. A compromised sidecar in staging can open a socket to the production database and nothing in the platform objects. Call this the flat network. NetworkPolicy is the correction: today covers the object's grammar, the semantics that trip people, the default-deny pair, and the machinery enforcing the wall on EKS. The coin to carry: walls are labels, not addresses.
§ IIFoundations — What a NetworkPolicy Is
A NetworkPolicy is a namespaced object with two halves: the podSelector chooses which pods the policy applies to; the ingress and egress lists say what traffic those pods may receive and send. Burns et al. compress it: policies select pods; rules admit traffic (K8sUR 3rd ed., ch. 14, pp. 251-253).
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: allow-app-to-db
namespace: trading
spec:
podSelector:
matchLabels:
app: postgres
policyTypes: [Ingress]
ingress:
- from:
- podSelector:
matchLabels:
app: pricing-engine
ports:
- protocol: TCP
port: 5432
Four facts govern everything. The model is allow-list only: there is no deny rule; deny arrives implicitly the moment any policy selects a pod. Selection flips the default: one policy with policyTypes: [Ingress] selecting a pod turns that pod's ingress into deny-everything-except-what-some-policy-admits. Policies are additive: ten policies selecting one pod union their allowances, no ordering, no priority. And the walls are labels: ipBlock exists for traffic crossing the cluster boundary, and reaching for it inside the cluster is a design smell because pod IPs churn with every reschedule.
§ IIIMechanism — The Grammar That Trips People, and the Agent That Enforces It
Empty versus absent
An ingress key with an empty rule list admits nothing; a missing ingress key with Ingress in policyTypes also admits nothing; a rule item with an empty from admits everything on its ports. And podSelector: {} selects every pod in the namespace. The default-deny pair is built from exactly these semantics:
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: default-deny-all
namespace: trading
spec:
podSelector: {}
policyTypes: [Ingress, Egress]
Every pod selected, both directions isolated, zero rules admitting anything. This object is the floor the namespace's other policies stand on; writing it should be muscle memory.
AND versus OR
Inside a from or to list, separate items are OR'd; selectors inside one item are AND'd. Two items admit pods matching either. One item carrying both a namespaceSelector and a podSelector admits only pods matching that label in namespaces matching that label. One dash of difference, opposite security posture.
The DNS footgun
The first team to apply the deny floor discovers their pods no longer resolve names: egress isolation blocks kube-dns like everything else. Every namespace that takes the floor needs the DNS door cut immediately:
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: allow-dns-egress
namespace: trading
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
Note the AND shape: kube-dns pods, in kube-system, nothing else.
Who enforces the wall
Rice lands the fact platform texts soft-pedal: NetworkPolicy is an API contract, and enforcement belongs entirely to the CNI plugin (Container Security, ch. 10, p. 147). A policy on a cluster whose network plugin ignores policies behaves as if the policy does not exist. On EKS the answer is native since VPC CNI v1.14: set enableNetworkPolicy: "true" on the aws-node addon and the network-policy agent compiles each policy into PolicyEndpoint custom resources and attaches eBPF programs to each pod's ENI to execute the verdicts in the kernel. The 07-23 shape returns: policy objects as desired state, kernel programs as actual state. The wall is a reconciliation loop wearing a firewall's face.
§ IVWorked Example — Walling the Trading Namespace
Three workloads: pricing-engine, postgres, and a metrics sidecar scraped from the monitoring namespace. Four objects wall it: the deny pair, the DNS door, allow-app-to-db from §II, and the scrape door inward:
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: allow-monitoring-scrape
namespace: trading
spec:
podSelector:
matchLabels:
metrics: enabled
policyTypes: [Ingress]
ingress:
- from:
- namespaceSelector:
matchLabels:
kubernetes.io/metadata.name: monitoring
ports:
- protocol: TCP
port: 9090
One egress rule is still missing: pricing-engine must reach postgres on 5432 through the floor. The habit worth building is to write every admitted flow twice, ingress on the receiver and egress on the sender, so each side's policy reads as a complete statement of that workload's contacts.
Then prove the wall with the same probe before and after:
kubectl -n trading run probe --rm -it --image=busybox --restart=Never -- \
wget -qO- --timeout=2 http://postgres:5432
Before: connection. After: timeout, because the probe carries neither the right label nor an admitting policy. Relabel the probe and the door opens. That is walls are labels, not addresses made testable.
§ VThe Allow-List Discipline
Rice's best practices (ch. 10, p. 148), refracted through this arc, compress to five. Deny at birth: the pair plus the DNS door goes into every namespace the day it is created. Name policies for what they admit: allow-app-to-db is documentation, policy-7 is archaeology. Keep walls on labels: ipBlock is for the world outside, including the metadata address today's Cert lesson walls off. Write both directions: egress rules are where exfiltration dies. Roll out in stages: allow rules first, watch flows, then drop the floor.
The first practice explains today's Dev lesson. Hand-applied discipline erodes at the rate teams onboard; the kopf operator next door turns deny-at-birth into a controller. Label a namespace and the baseline appears, reconciled forever. The wall becomes desired state.
§ VIConnection to Today's Dev and Cert Lessons
The Dev lesson industrializes §V: a Python operator on kopf reconciling the deny pair plus DNS door into every labeled namespace, with a drift timer. The Cert lesson takes CKS Cluster Setup, where network policy is the first bullet, and spends its depth on the rest of the perimeter: kube-bench against CIS, ingress TLS, metadata protection. One perimeter, three altitudes: the wall's grammar, the wall's installer, the wall's audit.
§ VIIClosing
The flat network is a policy you did not write. Replace it namespace by namespace: throw the isolation switch, cut the DNS door, admit each real flow by label, both directions, named for what it allows. On EKS, turn enforcement on before trusting the YAML, because a policy the CNI does not execute protects nothing.
Pick one namespace you operate. List every flow it actually needs: who calls in, what it calls out to, on which ports. If you cannot write that list in five minutes, that is the finding. If you can, the four objects are an afternoon's work.
Filed 2026-08-01 Fajr · Ops lesson · K8s deep-mastery track (day 10, visit 4) · first K8s day under the cloud-as-medium overlay (EKS referent)