Public: anyone can read this; the crew can change it.

Limits for strangers see each outside client, from trusted proxy headers only #726

Merged opened this in answer to

Check · Review · Land · Live done — Landed and live in production.

Merged

Closes #697

shipped production is serving it for issue #697 — its issue is already done deploy 4183511

opened this

Closes #697

Limits for strangers, and the signup audit, now see each outside client instead of the ingress in front of the forge.

What changed

  • src/http/peer.ts resolves the client once, when the listener notes a request. Forwarded headers are read only when the socket peer is a trusted proxy: this machine (as before) or a range in FORGE_TRUSTED_PROXIES. In that case:

- a header the edge sets, FORGE_CLIENT_IP_HEADER, is read first when it holds an address; - otherwise X-Forwarded-For is walked from the right, past each trusted hop, to the first address no trusted proxy vouches for. The left end, which the client wrote, cannot choose its bucket. With no setting, a public peer is the client and headers are ignored, as today.

  • One change on this machine: when the peer is loopback, the forwarded hop is now read from the right rather than the left. A local proxy appends what it saw at the right end, so the left end was the client's own claim.
  • src/kernel/cidr.ts: strict IPv4 and IPv6 ranges, including IPv4-mapped IPv6 addresses. A malformed range in FORGE_TRUSTED_PROXIES fails the boot; nothing is skipped silently.
  • Both limiters (reports from outside, OAuth registration) and the MCP path already read clientAddress, so they inherit this. Their whole-forge caps are unchanged.

What to set for production (documented in the README). The forge sits behind cloudflared → Traefik, and Traefik has no forwardedHeaders.trustedIPs, so it rewrites X-Forwarded-For to cloudflared's pod address. The real client is in Cloudflare's CF-Connecting-IP, which Cloudflare overwrites at its edge. Set:

FORGE_TRUSTED_PROXIES=10.42.0.0/16
FORGE_CLIENT_IP_HEADER=cf-connecting-ip

(10.42.0.0/16 is the k3s pod range; Traefik is 10.42.0.166 today.) An alternative is giving Traefik forwardedHeaders.trustedIPs=10.42.0.0/16, after which the right-to-left X-Forwarded-For walk needs no named header. That is an edge change, so I left it to the owner.

Two outside clients seen apart: test/client-address.test.ts runs the forge behind a trusted proxy range. One forwarded client spends the OAuth registration minute (429), and a second client behind the same proxy is still served (201). Other tests: a trusted proxy's header separates two clients; the same header from an untrusted peer is ignored; a client-written leading hop is ignored; a chain of trusted proxies is walked; a named header is read only from a trusted proxy; CIDR parsing, and malformed entries refused. 10 tests, in the gate. The reports, report-discovery and OAuth suites still pass.

🤖 Generated with Claude Code

Sign in to join this conversation.

↑↓ navigate ↵ open ⌘↵ its app esc close

Keyboard shortcuts

Docs