Resolve real client IP behind trusted proxies (closes #94) #127

Open
clawbot wants to merge 6 commits from issue-94-trusted-proxies into next
Collaborator

Behind the TLS-terminating proxy pixa is deployed with, every access-log line and login record showed the proxy's address, and naively trusting X-Forwarded-For would let any client forge one.

What changed:

  • New trusted_proxies config key: a CIDR list parsed by the same net/netip list parser as blocked_networks (generalized to take the key name), so there is one parser, not two. An invalid entry aborts startup naming the key and value; omitted or empty trusts no one.
  • New internal/clientip package resolves the client address, honoring X-Forwarded-For only when the direct peer is inside a trusted range, then walking the chain right-to-left to the rightmost entry that is not itself a trusted proxy. A client connecting directly cannot influence the result.
  • A ClientIP middleware resolves once per request and stores the address in the context; the request-logging middleware and both login-attempt logs now read it instead of the raw peer.
  • Documented in README.md and config.example.yml.

Not shown by the diff: the upstream RemoteAddr fields in the fetcher and cache are the origin server's address, not a client, and were left alone.

Disclosures:

  • Judgement call: resolves from X-Forwarded-For only, not X-Real-IP; that header carries the verifiable chain the definition of done walks.
  • Verification: local docker build --target builder gate green; hosted CI is down (sneak/vaultik#133).

Model: opus-4-8

Behind the TLS-terminating proxy pixa is deployed with, every access-log line and login record showed the proxy's address, and naively trusting `X-Forwarded-For` would let any client forge one. What changed: - New `trusted_proxies` config key: a CIDR list parsed by the same `net/netip` list parser as `blocked_networks` (generalized to take the key name), so there is one parser, not two. An invalid entry aborts startup naming the key and value; omitted or empty trusts no one. - New `internal/clientip` package resolves the client address, honoring `X-Forwarded-For` only when the direct peer is inside a trusted range, then walking the chain right-to-left to the rightmost entry that is not itself a trusted proxy. A client connecting directly cannot influence the result. - A `ClientIP` middleware resolves once per request and stores the address in the context; the request-logging middleware and both login-attempt logs now read it instead of the raw peer. - Documented in `README.md` and `config.example.yml`. Not shown by the diff: the upstream `RemoteAddr` fields in the fetcher and cache are the origin server's address, not a client, and were left alone. Disclosures: - Judgement call: resolves from `X-Forwarded-For` only, not `X-Real-IP`; that header carries the verifiable chain the definition of done walks. - Verification: local `docker build --target builder` gate green; hosted CI is down (https://git.eeqj.de/sneak/vaultik/issues/133). Model: opus-4-8
clawbot added 6 commits 2026-09-22 01:29:26 +02:00
Check that a valid CIDR list lands in TrustedProxies in order, an omitted
key trusts no one, and an invalid or null value aborts startup naming the
key and value. The list parser is shared with blocked_networks, whose
tests exercise the remaining shapes.

Model: opus-4-8
Add a trusted_proxies CIDR-list config key alongside blocked_networks.
Generalize the blocked_networks parser into parseCIDRList and
cidrListEntries, which take the key name as a parameter, so both keys
share one parser rather than a second copy. An invalid entry aborts
startup naming the key and value; an omitted or empty key leaves the
list empty.

Model: opus-4-8
New internal/clientip package. Resolve honors X-Forwarded-For only when
the direct peer falls inside a configured trusted-proxy range; otherwise
it returns the peer address and ignores the header, so a client
connecting directly cannot spoof its address. When the peer is trusted,
the chain is walked right to left and the first entry that is not itself
a trusted proxy is returned. Also provides request-context helpers so the
resolved address can be shared across middleware and handlers.

Tests cover the honored, ignored, and spoofed-chain cases, rightmost
selection across a mixed chain, multi-line headers, IPv6 and
IPv4-mapped peers, and the context round-trip.

Model: opus-4-8
Assert the ClientIP middleware stores the resolved address in the
request context for trusted and untrusted peers, that the request log
records the forwarded client rather than the proxy peer, and that a
failed-login record carries the resolved address.

Model: opus-4-8
Add a ClientIP middleware that resolves the client address once per
request and stores it in the context, placed ahead of logging in the
route chain. The request-logging middleware and both login-attempt logs
now read the resolved address instead of the raw peer. Behind a trusted
proxy these records show the real client; a direct, untrusted client
still shows its own address and cannot forge one. Removes the local
host:port helper now that the clientip package owns that parsing.

Model: opus-4-8
Describe the trusted_proxies key in README.md and config.example.yml,
including the rightmost-non-proxy selection rule and the safe default of
trusting no one. Record the unit in TODO.md.

Model: opus-4-8
clawbot added the needs-review label 2026-09-22 01:29:37 +02:00
clawbot self-assigned this 2026-09-22 01:29:37 +02:00
Some required checks failed
check / check (push) Failing after 1s
You are not authorized to merge this pull request.
This pull request can be merged automatically.
View command line instructions

Checkout

From your project repository, check out a new branch and test the changes.
git fetch -u origin issue-94-trusted-proxies:issue-94-trusted-proxies
git checkout issue-94-trusted-proxies
Sign in to join this conversation.
No Reviewers
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: sneak/pixa#127