Hedronite · Ops Synthesis Lesson · 01-Earth-DevOps · Track K8s Day 10 · Sat 2026-08-01 · Bundle Trio #2

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.

Lesson Class: Ops (Kubernetes network security)
Track / Day: K8s (Deep K8s, CKA + CKS) — round-robin day 10, fourth K8s visit
Domains: DevOps · Kubernetes · Security · Networking · AWS
Cloud Referent: EKS — VPC CNI network-policy agent, eBPF enforcement, PolicyEndpoint
Word Count: ~2,100
Grounding: Rice, Container Security — Ch 10 (pp. 147-148) · Burns et al., K8s: Up & Running 3rd ed — Ch 14 Network Policy (pp. 251-253)
Paired Dev: A Kubernetes Operator in Python with kopf — the Policy-Baseline Operator
Paired Cert: CKS — Cluster Setup: kube-bench, Ingress TLS, Metadata Protection
Discipline: ROD v3 · earth-accent meta-card · three-discipline grid · bundle shape (Maghrib fills quiz + lab-ref)
The Flat Network
Kubernetes ships with no walls between pods. Flatness made the platform adoptable, and it is a policy silently in force wherever nobody wrote a better one.
The Isolation Switch
A pod selected by no policy allows everything. The first policy that selects it flips that direction to deny-except. Isolation is thrown, never written.
Walls Are Labels
Policies select by what a workload is, never where it sits. A stable wall over churning addresses, the Services idea played in defense.
The Services lesson celebrated the network where anything can dial anything. Today's lesson corrects it.

§ 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.

🫡 ⚖️ 📜
Leo.Syri — Praetor Consulate, Imperium Luminaura
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)