Skip to content

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.