SCRUM-202 — gateway_dev: trustworthy forwarded headers for admin-auth¶
Plan ref: GWD-5 (docs/11-admin-plane-plan.md). Stacked on SCRUM-201.
What changed¶
Go's httputil.NewSingleHostReverseProxy appends the peer IP to whatever
X-Forwarded-For the client sent, so admin-auth would have received
<client-supplied>, <peer> and had to parse an untrusted chain to rate-limit logins.
| Change | Where |
|---|---|
New route field SetForwarded bool |
internal/router/route.go |
Set on the three adminauth routes only: /admin-auth/, /api/admin/users, /api/admin/users/ |
internal/router/dev.go |
When set: delete client X-Forwarded-For, Forwarded, X-Real-Ip (the proxy then sets XFF to exactly the peer IP); set X-Forwarded-Proto to http/https from the listener |
internal/proxy/proxy.go |
| Every other route: unchanged (chain appended, no proto) | — |
| Docs | README.md (HTTP surface), docs/05-gateway-techspec.md (Route struct + table) |
Contract for admin-auth: on these routes, X-Forwarded-For is a single IP set by
the gateway and can be used directly as the client key. (Behind another proxy in
front of gateway_dev, it would be that proxy's IP — not the case today: gateway_dev
is reached over an SSH tunnel, so the peer is the tunnel end.)
How to verify¶
cd services/gateway_dev
go vet ./... && go test -race -count=1 ./...
go test -count=1 -run 'SetForwarded|ChainIsAppended' -v .
bash testdata/smoke/smoke.sh | grep -c FINDING # 0
New tests (gateway_dev_test.go):
| Test | Asserts |
|---|---|
TestSetForwarded_ReplacesClientChain |
client sends X-Forwarded-For: 6.6.6.6, Forwarded, X-Real-Ip, X-Forwarded-Proto: https → upstream sees XFF 127.0.0.1 exactly, proto http, no Forwarded/X-Real-Ip |
TestWithoutSetForwarded_ChainIsAppended |
same request on a normal route → upstream sees 6.6.6.6, 127.0.0.1, no proto (unchanged behaviour) |
TestOnlyAdminAuthRoutesSetForwarded |
over the real table: flag set ⇔ upstream is adminauth |
Mutation check (done): removing the two Del lines makes
TestSetForwarded_ReplacesClientChain fail with "6.6.6.6, 127.0.0.1", want 127.0.0.1.
Manual check against a running stack (after admin-auth exists): through the tunnel,
curl -H 'X-Forwarded-For: 1.2.3.4' http://localhost:8090/admin-auth/login -d '{}'
— admin-auth's access log should show the tunnel peer, never 1.2.3.4.
Results at time of writing¶
go vet,go test -race: pass. Smoke: 0 findings.
How it was built¶
DeepSeek run scoped (Landlock) to services/gateway_dev (73 s, ~7.8k output tokens).
Claude review added: stripping Forwarded and X-Real-Ip (RFC 7239 / common
alternative client-IP headers), the extra assertions, comment reflow, techspec.