diff --git a/deploy/lab/k8s/echo-network-policy.yaml b/deploy/lab/k8s/echo-network-policy.yaml new file mode 100644 index 0000000..e72f062 --- /dev/null +++ b/deploy/lab/k8s/echo-network-policy.yaml @@ -0,0 +1,62 @@ +# Restrict who may reach the echo pods. +# +# Traefik is configured to trust X-Forwarded-* from the whole pod CIDR, and the +# app's Tomcat valve trusts every private range by default. Both are IP-range +# decisions, so any pod in the cluster can forge those headers by talking to the +# Service directly and bypassing Traefik entirely. Measured, not hypothetical: +# +# kubectl -n header-lab run t --rm -i --restart=Never --image=curlimages/curl -- \ +# curl -s http://echo:8081/api/echo -H 'X-Forwarded-Host: evil.example.com' +# → serverName evil.example.com, remoteAddr 1.2.3.4 +# +# A NetworkPolicy closes that path. It selects by label rather than IP, so it +# survives pod restarts and rescheduling — unlike the trustedIPs list, which +# could not name Traefik because its IP changes. +# +# "Trusting forwarded headers" and "guaranteeing a proxy sits in front" are a +# pair. Doing only the first leaves this hole. +apiVersion: networking.k8s.io/v1 +kind: NetworkPolicy +metadata: + name: echo-allow-traefik-only + namespace: header-lab +spec: + podSelector: + matchLabels: + app: echo + policyTypes: + - Ingress + ingress: + # The proxy itself. namespaceSelector and podSelector in one list item are + # ANDed, so this is "traefik pods in kube-system" and nothing else. + - from: + - namespaceSelector: + matchLabels: + kubernetes.io/metadata.name: kube-system + podSelector: + matchLabels: + app.kubernetes.io/name: traefik + ports: + - protocol: TCP + port: 8081 + + # kubelet readiness/liveness probes originate from the node, not from a pod, + # so they need their own rule. Without it the probes fail and the pods are + # restarted in a loop. + # + # The probe's source address is the node's flannel bridge (cni0), which + # holds the first address of that node's /24: + # kc-lab-1 10.42.0.1 kc-lab-2 10.42.1.1 + # Listing them as /32 keeps this rule from re-admitting arbitrary pods, + # which a broader 10.42.0.0/16 block would do and would undo the policy. + # + # Adding a node means adding its gateway here. Verify with: + # kubectl get nodes -o jsonpath='{range .items[*]}{.spec.podCIDR}{"\n"}{end}' + - from: + - ipBlock: + cidr: 10.42.0.1/32 + - ipBlock: + cidr: 10.42.1.1/32 + ports: + - protocol: TCP + port: 8081 diff --git a/deploy/lab/k8s/traefik-forwarded-headers.yaml b/deploy/lab/k8s/traefik-forwarded-headers.yaml index 7d17369..d57e4fe 100644 --- a/deploy/lab/k8s/traefik-forwarded-headers.yaml +++ b/deploy/lab/k8s/traefik-forwarded-headers.yaml @@ -28,15 +28,16 @@ spec: # traffic and Traefik sees a pod-network address rather than the # host nginx address. # - # Trusting the whole pod CIDR means any pod in the cluster could - # forge these headers. That is acceptable in this lab and is the - # reason a production setup would narrow it — see the trade-off - # section in docs/two-hop-proxy-header-contract.md. + # The node/host range is deliberately absent. Because svclb SNATs, + # the host nginx address never reaches Traefik — measured, not assumed. + # Trusting a range that cannot appear only widens the surface. + # + # Trusting the whole pod CIDR still means any pod in the cluster could + # forge these headers, which is why echo-network-policy.yaml restricts + # who may reach the application at all. trustedIPs: - 10.42.0.0/16 - - 192.168.122.0/24 websecure: forwardedHeaders: trustedIPs: - 10.42.0.0/16 - - 192.168.122.0/24