Chapter 19 — Kubernetes network security
In one sentence
By default every pod can talk to every other pod, the API server, the kubelet, and the cloud metadata endpoint — so Kubernetes network security is about imposing default-deny with NetworkPolicy (enforced by a capable CNI), plus identity on every call via the mesh.
Where we are
G1 Ch 39 and Part 2 Ch 8 covered network segmentation and cloud VPCs. Inside a cluster it's a different model: policy objects selected by labels, enforced by the CNI, and it starts wide open.
How it works
NetworkPolicy basics:
- Namespaced. Selects pods by label. Applies to Ingress, Egress, or both.
- DEFAULT: all traffic allowed. The moment ANY NetworkPolicy selects a
pod for a direction, that pod becomes DEFAULT-DENY for that direction
-- only explicitly-listed traffic is allowed.
- Peers: podSelector (same ns), namespaceSelector, ipBlock (+ except),
and ports.
- ONLY enforced if the CNI supports it: Calico, Cilium, Antrea, Weave,
Kube-router. flannel does NOT -> policies are silently inert.
The must-have baseline, per namespace:
# deny all ingress and egress
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata: { name: default-deny, namespace: core }
spec:
podSelector: {}
policyTypes: ["Ingress", "Egress"]
Then explicit allows:
- allow DNS egress to kube-dns (UDP/TCP 53) -- forgetting this breaks
everything.
- allow ingress from the specific caller pods/namespaces on the
service port.
- allow egress to the specific downstream services.
- allow egress to the API server only for pods that call it.
- DENY egress to 169.254.169.254/32 (metadata) and to the node CIDR /
kubelet port unless explicitly needed.
What plain NetworkPolicy can't do (use Cilium/Calico enterprise CRDs):
- L7 rules (HTTP method/path, gRPC service/method, Kafka topic).
- FQDN-based egress ("allow egress to api.stripe.com") -- plain policy
is IP-based, and cloud service IPs are dynamic.
- DNS-aware policy, cluster-wide policies, default policies, node
egress, identity-based (not just label-based) rules.
CiliumNetworkPolicy / CiliumClusterwideNetworkPolicy and Calico
GlobalNetworkPolicy give you these. FQDN egress is how you actually
allowlist third-party APIs.
The service mesh layer (identity on every call):
NetworkPolicy says "pod A CAN reach pod B on port 8080". It does NOT
verify WHO is calling or authenticate the connection. A mesh (Istio,
Linkerd, Cilium Service Mesh, Consul) adds:
- automatic mTLS between workloads, with SPIFFE identities (Part 5) ->
"reachable but refused" for anyone without the right identity.
- AuthorizationPolicy: "only workloads with identity
spiffe://.../core/web may call orders-api, method POST /orders".
- telemetry: every call, with source and destination identity ->
detection (Ch 54).
Use BOTH: NetworkPolicy for coarse reachability (defence in depth,
works even if the mesh sidecar is bypassed), mesh AuthorizationPolicy
for identity-based L7 authz.
Other cluster-network exposure:
[ ] Metadata endpoint: pods can hit 169.254.169.254 by default -> IMDS
creds (Part 2). Block via NetworkPolicy egress deny + IMDSv2
hop-limit-1 on nodes; on EKS use the "disable pod access to IMDS"
setting or a network policy; GKE Workload Identity blocks the
legacy metadata paths.
[ ] The kubelet (:10250) and etcd (:2379-2380) should be unreachable
from workload pods.
[ ] Ingress controllers: TLS config (Part G1-7), rate limiting, WAF,
and they often run privileged / hostNetwork -> treat as a sensitive
workload.
[ ] LoadBalancer/NodePort Services expose things outside the cluster --
gate their creation by policy (Ch 18).
[ ] DNS: CoreDNS is a high-value target (poison it -> redirect every
service lookup). Lock down its RBAC and config; consider DNS
policies.
[ ] East-west by default: kube-proxy makes every Service reachable
cluster-wide -- NetworkPolicy is the only thing narrowing it.
Worked example
Blocking pod-to-metadata and proving lateral containment.
BEFORE: `edge:web` pod is popped (RCE). Attacker:
curl http://169.254.169.254/latest/meta-data/iam/security-credentials/
-> the NODE's instance role creds (if IMDSv1 or hop-limit>1).
curl http://orders-api.core:8080/internal/orders -> reachable
(no NetworkPolicy), and if orders-api trusts network position, data.
APPLY:
1. `default-deny` (ingress+egress) in `edge` and `core`.
2. `edge`: allow egress to kube-dns:53; allow egress to
`core/orders-api:8080`; allow ingress from the ingress controller.
Explicitly deny egress to 169.254.169.254/32 (an ipBlock `except`
or a Cilium policy).
3. `core/orders-api`: allow ingress ONLY from `edge/web` and
`edge/bff` on 8080; allow egress to the DB namespace + DNS.
4. Nodes: IMDSv2 required, hop-limit 1 -> even without the policy,
a pod (not the node) can't complete the token PUT.
5. Mesh AuthorizationPolicy: `orders-api` accepts calls only from
SPIFFE identity `.../edge/bff` (not `web`), method-scoped.
AFTER: the popped `web` pod can reach DNS, `orders-api:8080`, and
nothing else. It cannot get node creds. Its calls to `orders-api` are
rejected unless they carry `bff`'s mTLS identity AND a valid end-user
context (Part 5). Blast radius: DNS lookups and refused connections.
Falco alerts on the outbound-connection-from-web and the shell.
Practice (60 min) — kind cluster with Cilium or Calico
- Install Cilium (or Calico) on kind so NetworkPolicy is actually
enforced. Confirm: deploy two pods,
curlbetween them (works), apply adefault-deny,curlagain (fails). - Build the SecureShop namespace policies:
default-denyeverywhere, then explicit DNS, service-to-service, and ingress allows. Verify each path withkubectl exec ... curl. - Block
169.254.169.254egress. From a pod, confirm the metadata endpoint is now unreachable. - Write a CiliumNetworkPolicy with FQDN egress allowing only
api.stripe.comfrom thepaymentspod; confirmcurl example.comfails andcurl api.stripe.com(or a stand-in) works. - Install Linkerd or Istio ambient; enable mTLS; write an
AuthorizationPolicysoorders-apionly acceptsbff's identity. Call it fromweb(rejected) andbff(allowed). - Run
kube-hunter(or manual probes) before/after and note what's no longer reachable.
Across the series
- The packet path through Services,
kube-proxyNAT, CNI overlays, and NetworkPolicy (including the DNS egress rule that every default-deny policy forgets): TCP/IP guide, Chapter 58. Host-level packet path and conntrack: Chapter 52.
Common mistakes
- Assuming NetworkPolicy is active. With flannel (or a misconfigured CNI) it's inert. Test it.
- No default-deny. A namespace with only "allow" policies still permits everything not otherwise selected in unselected pods.
- Forgetting DNS egress.
default-denyegress without a kube-dns allow breaks every workload — then someone deletes the policy. - IP-based egress for third-party APIs. Their IPs rotate; you need FQDN policies (Cilium/Calico).
- Relying on NetworkPolicy for authz. It's reachability, not identity. Add the mesh for "who is calling".
- Ignoring the metadata endpoint. The single highest-value pod-reachable target. Block it and use IMDSv2 hop-limit-1.
- Privileged ingress controllers left unhardened. They're internet-facing
and often
hostNetwork.
Check yourself
- What is a pod's default network posture, and what flips a pod to default-deny?
- What must the CNI do for NetworkPolicy to work, and name one that doesn't.
- What can plain NetworkPolicy not express that CiliumNetworkPolicy can?
- NetworkPolicy vs mesh AuthorizationPolicy — what does each provide, and why use both?
- Why is
169.254.169.254egress the priority block, and what node setting backs it up? - What's the one allow rule people forget in a default-deny egress namespace?
Further reading
- Docs: Kubernetes "Network Policies"; Cilium and Calico docs (NetworkPolicy
- their CRDs + FQDN policies); "Network Policy Editor" (
networkpolicy.io).
- their CRDs + FQDN policies); "Network Policy Editor" (
- Recipes:
ahmetb/kubernetes-network-policy-recipes. - Mesh: Istio "Security" (mTLS, AuthorizationPolicy, PeerAuthentication); Linkerd "Automatic mTLS" and "Policy"; Cilium Service Mesh.
- Hardening: NSA/CISA guidance (network section); "Kubernetes network security" chapters of Kubernetes Security and Observability.