feat: narrow Traefik trusted range and restrict echo ingress to Traefik

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
DongHyeonka
2026-09-03 16:15:28 +09:00
co-authored by Claude Opus 5
parent 98874b0c6c
commit 3af52bb66a
2 changed files with 69 additions and 6 deletions
+62
View File
@@ -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
@@ -28,15 +28,16 @@ spec:
# traffic and Traefik sees a pod-network address rather than the # traffic and Traefik sees a pod-network address rather than the
# host nginx address. # host nginx address.
# #
# Trusting the whole pod CIDR means any pod in the cluster could # The node/host range is deliberately absent. Because svclb SNATs,
# forge these headers. That is acceptable in this lab and is the # the host nginx address never reaches Traefik — measured, not assumed.
# reason a production setup would narrow it — see the trade-off # Trusting a range that cannot appear only widens the surface.
# section in docs/two-hop-proxy-header-contract.md. #
# 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: trustedIPs:
- 10.42.0.0/16 - 10.42.0.0/16
- 192.168.122.0/24
websecure: websecure:
forwardedHeaders: forwardedHeaders:
trustedIPs: trustedIPs:
- 10.42.0.0/16 - 10.42.0.0/16
- 192.168.122.0/24