From the audit against the canonical REPO_POLICIES.md. Independently verified against main at 61f42e6 — grep for X-Forwarded-For|X-Real-IP|RealIP across internal/ and cmd/ returns zero matches.
The policy requires:
> True client IP detection when behind a reverse proxy (X-Forwarded-For, X-Real-IP). The application must accept forwarded headers only from a configured set of trusted proxy addresses — never trust X-Forwarded-For unconditionally.
and separately states that services "are always deployed behind a TLS-terminating reverse proxy" — so this is not a hypothetical deployment, it is the expected one.
Current behavior:
ipFromHostPort() (internal/middleware/middleware.go:47-57) parses only r.RemoteAddr, and middleware.go:99 logs "remoteIP", ipFromHostPort(r.RemoteAddr).
internal/handlers/auth.go:48 and :62 log "remote_addr", r.RemoteAddr for failed and successful logins.
chi's middleware.RealIP is not in the chain either (internal/server/routes.go:18-29 uses Recoverer, RequestID, SecurityHeaders, Logging, Metrics, CORS, Timeout).
So in the mandated deployment, every access log line and every login-attempt record shows the reverse proxy's address. Failed-login logs — the only signal there is today that someone is guessing the signing key — are useless for identifying a source or building a blocklist.
Note the policy's "never trust unconditionally" half matters as much as the detection half: naively enabling RealIP without a trusted-proxy allowlist lets any client spoof its own address by sending the header, which is worse than the status quo because the logs then look authoritative while being attacker-controlled.
Definition of done
A trusted_proxies config key taking a CIDR list. Strict parsing: an invalid entry aborts startup naming the key and value, per repo policy. Empty/absent means trust nothing and use RemoteAddr — the safe default.
Forwarded headers are honored only when the immediate peer is in that list; otherwise RemoteAddr wins. Handle X-Forwarded-For chains correctly (rightmost untrusted entry), not by blindly taking the leftmost value.
The resolved client IP is used consistently by the logging middleware, the auth logs, and anything else that records a client address.
Failing tests first: forwarded header honored from a trusted source; ignored from an untrusted source; a spoofed chain from an untrusted peer cannot influence the result.
Document the key in README.md and config.example.yml; it is a prerequisite for a correct reverse-proxy setup, so it also belongs in the deployment guide issue.
make check green.
Priority
Blocks 1.0.0, and it is a hard prerequisite for #66 (login rate limiting): without it, a per-IP limiter behind a proxy keys every request to the proxy's address and would lock out all users at once the moment one attacker trips it. Do this before#66.
From the audit against the canonical `REPO_POLICIES.md`. Independently verified against `main` at `61f42e6` — grep for `X-Forwarded-For|X-Real-IP|RealIP` across `internal/` and `cmd/` returns **zero matches**.
The policy requires:
> True client IP detection when behind a reverse proxy (`X-Forwarded-For`, `X-Real-IP`). The application must accept forwarded headers only from a configured set of trusted proxy addresses — never trust `X-Forwarded-For` unconditionally.
and separately states that services "are always deployed behind a TLS-terminating reverse proxy" — so this is not a hypothetical deployment, it is the expected one.
Current behavior:
- `ipFromHostPort()` (`internal/middleware/middleware.go:47-57`) parses only `r.RemoteAddr`, and `middleware.go:99` logs `"remoteIP", ipFromHostPort(r.RemoteAddr)`.
- `internal/handlers/auth.go:48` and `:62` log `"remote_addr", r.RemoteAddr` for failed and successful logins.
- chi's `middleware.RealIP` is not in the chain either (`internal/server/routes.go:18-29` uses `Recoverer`, `RequestID`, `SecurityHeaders`, `Logging`, `Metrics`, `CORS`, `Timeout`).
So in the mandated deployment, **every access log line and every login-attempt record shows the reverse proxy's address**. Failed-login logs — the only signal there is today that someone is guessing the signing key — are useless for identifying a source or building a blocklist.
Note the policy's "never trust unconditionally" half matters as much as the detection half: naively enabling `RealIP` without a trusted-proxy allowlist lets any client spoof its own address by sending the header, which is worse than the status quo because the logs then look authoritative while being attacker-controlled.
## Definition of done
1. A `trusted_proxies` config key taking a CIDR list. Strict parsing: an invalid entry aborts startup naming the key and value, per repo policy. Empty/absent means trust nothing and use `RemoteAddr` — the safe default.
2. Forwarded headers are honored **only** when the immediate peer is in that list; otherwise `RemoteAddr` wins. Handle `X-Forwarded-For` chains correctly (rightmost untrusted entry), not by blindly taking the leftmost value.
3. The resolved client IP is used consistently by the logging middleware, the auth logs, and anything else that records a client address.
4. Failing tests first: forwarded header honored from a trusted source; ignored from an untrusted source; a spoofed chain from an untrusted peer cannot influence the result.
5. Document the key in `README.md` and `config.example.yml`; it is a prerequisite for a correct reverse-proxy setup, so it also belongs in the deployment guide issue.
6. `make check` green.
## Priority
**Blocks 1.0.0**, and it is a hard prerequisite for #66 (login rate limiting): without it, a per-IP limiter behind a proxy keys every request to the proxy's address and would lock out all users at once the moment one attacker trips it. Do this **before** #66.
clawbot
added this to the 1.0.0 milestone 2026-08-09 07:03:11 +02:00
New trusted_proxies config key: a list of CIDR ranges (YAML list or comma-separated string). Strict parsing via netip.ParsePrefix; an invalid entry aborts startup naming the key and the offending value. Absent or empty means trust nothing, which keeps RemoteAddr as the client address.
New internal/clientip package with a pure resolver. Forwarded headers are honored only when the immediate peer (RemoteAddr) falls inside a trusted range; otherwise the peer address wins and X-Forwarded-For is ignored entirely, so an untrusted client cannot spoof its address. When the peer is trusted, the chain is walked right-to-left and the resolver returns the rightmost entry that is not itself a trusted proxy.
The resolved address replaces the raw remote address in the request-logging middleware and in both login-attempt logs.
Tests first: honored from a trusted peer; ignored from an untrusted peer; a spoofed chain from an untrusted peer cannot influence the result; rightmost-untrusted selection across a mixed chain; plus config parsing (valid CIDRs, invalid entry aborts, absent means trust nothing). Docs in README.md and config.example.yml.
Scope: the config.go change is limited to the new key; #91 and #110 touch middleware.go and config.go in parallel.
model: claude-opus-4-8
Plan.
- New `trusted_proxies` config key: a list of CIDR ranges (YAML list or comma-separated string). Strict parsing via `netip.ParsePrefix`; an invalid entry aborts startup naming the key and the offending value. Absent or empty means trust nothing, which keeps `RemoteAddr` as the client address.
- New `internal/clientip` package with a pure resolver. Forwarded headers are honored **only** when the immediate peer (`RemoteAddr`) falls inside a trusted range; otherwise the peer address wins and `X-Forwarded-For` is ignored entirely, so an untrusted client cannot spoof its address. When the peer is trusted, the chain is walked right-to-left and the resolver returns the rightmost entry that is not itself a trusted proxy.
- The resolved address replaces the raw remote address in the request-logging middleware and in both login-attempt logs.
- Tests first: honored from a trusted peer; ignored from an untrusted peer; a spoofed chain from an untrusted peer cannot influence the result; rightmost-untrusted selection across a mixed chain; plus config parsing (valid CIDRs, invalid entry aborts, absent means trust nothing). Docs in `README.md` and `config.example.yml`.
- Scope: the `config.go` change is limited to the new key; https://git.eeqj.de/sneak/pixa/issues/91 and https://git.eeqj.de/sneak/pixa/issues/110 touch `middleware.go` and `config.go` in parallel.
model: claude-opus-4-8
A trusted_proxies CIDR list (parsed by the same parser as blocked_networks) gates a new internal/clientip resolver: X-Forwarded-For is honored only when the direct peer is a trusted proxy, and the chain is walked right-to-left to the rightmost entry that is not itself a trusted proxy, so a directly connecting client cannot spoof its address. Omitted or empty trusts no one. The request-logging middleware and both login-attempt logs now record the resolved address. Documented in README.md and config.example.yml.
One reading taken: the resolver uses X-Forwarded-For only, not X-Real-IP, since that header carries the verifiable chain the definition of done describes.
Model: opus-4-8
Implemented in https://git.eeqj.de/sneak/pixa/pulls/127 (base `next`).
A `trusted_proxies` CIDR list (parsed by the same parser as `blocked_networks`) gates a new `internal/clientip` resolver: `X-Forwarded-For` is honored only when the direct peer is a trusted proxy, and the chain is walked right-to-left to the rightmost entry that is not itself a trusted proxy, so a directly connecting client cannot spoof its address. Omitted or empty trusts no one. The request-logging middleware and both login-attempt logs now record the resolved address. Documented in `README.md` and `config.example.yml`.
One reading taken: the resolver uses `X-Forwarded-For` only, not `X-Real-IP`, since that header carries the verifiable chain the definition of done describes.
Model: opus-4-8
The default was changed per the ruling in this thread: when trusted_proxies is omitted, the trusted set is the RFC 1918 private ranges (10.0.0.0/8, 172.16.0.0/12, 192.168.0.0/16) rather than trusting no one. An explicitly empty list still trusts nothing, and an explicit list replaces the default. Loopback is not included (not an RFC 1918 range, and no test expects it). Updated in #127.
Model: opus-4-8
The default was changed per the ruling in this thread: when `trusted_proxies` is omitted, the trusted set is the RFC 1918 private ranges (`10.0.0.0/8`, `172.16.0.0/12`, `192.168.0.0/16`) rather than trusting no one. An explicitly empty list still trusts nothing, and an explicit list replaces the default. Loopback is not included (not an RFC 1918 range, and no test expects it). Updated in https://git.eeqj.de/sneak/pixa/pulls/127.
Model: opus-4-8
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.
From the audit against the canonical
REPO_POLICIES.md. Independently verified againstmainat61f42e6— grep forX-Forwarded-For|X-Real-IP|RealIPacrossinternal/andcmd/returns zero matches.The policy requires:
> True client IP detection when behind a reverse proxy (
X-Forwarded-For,X-Real-IP). The application must accept forwarded headers only from a configured set of trusted proxy addresses — never trustX-Forwarded-Forunconditionally.and separately states that services "are always deployed behind a TLS-terminating reverse proxy" — so this is not a hypothetical deployment, it is the expected one.
Current behavior:
ipFromHostPort()(internal/middleware/middleware.go:47-57) parses onlyr.RemoteAddr, andmiddleware.go:99logs"remoteIP", ipFromHostPort(r.RemoteAddr).internal/handlers/auth.go:48and:62log"remote_addr", r.RemoteAddrfor failed and successful logins.middleware.RealIPis not in the chain either (internal/server/routes.go:18-29usesRecoverer,RequestID,SecurityHeaders,Logging,Metrics,CORS,Timeout).So in the mandated deployment, every access log line and every login-attempt record shows the reverse proxy's address. Failed-login logs — the only signal there is today that someone is guessing the signing key — are useless for identifying a source or building a blocklist.
Note the policy's "never trust unconditionally" half matters as much as the detection half: naively enabling
RealIPwithout a trusted-proxy allowlist lets any client spoof its own address by sending the header, which is worse than the status quo because the logs then look authoritative while being attacker-controlled.Definition of done
trusted_proxiesconfig key taking a CIDR list. Strict parsing: an invalid entry aborts startup naming the key and value, per repo policy. Empty/absent means trust nothing and useRemoteAddr— the safe default.RemoteAddrwins. HandleX-Forwarded-Forchains correctly (rightmost untrusted entry), not by blindly taking the leftmost value.README.mdandconfig.example.yml; it is a prerequisite for a correct reverse-proxy setup, so it also belongs in the deployment guide issue.make checkgreen.Priority
Blocks 1.0.0, and it is a hard prerequisite for #66 (login rate limiting): without it, a per-IP limiter behind a proxy keys every request to the proxy's address and would lock out all users at once the moment one attacker trips it. Do this before #66.
Plan.
trusted_proxiesconfig key: a list of CIDR ranges (YAML list or comma-separated string). Strict parsing vianetip.ParsePrefix; an invalid entry aborts startup naming the key and the offending value. Absent or empty means trust nothing, which keepsRemoteAddras the client address.internal/clientippackage with a pure resolver. Forwarded headers are honored only when the immediate peer (RemoteAddr) falls inside a trusted range; otherwise the peer address wins andX-Forwarded-Foris ignored entirely, so an untrusted client cannot spoof its address. When the peer is trusted, the chain is walked right-to-left and the resolver returns the rightmost entry that is not itself a trusted proxy.README.mdandconfig.example.yml.config.gochange is limited to the new key; #91 and #110 touchmiddleware.goandconfig.goin parallel.model: claude-opus-4-8
Implemented in #127 (base
next).A
trusted_proxiesCIDR list (parsed by the same parser asblocked_networks) gates a newinternal/clientipresolver:X-Forwarded-Foris honored only when the direct peer is a trusted proxy, and the chain is walked right-to-left to the rightmost entry that is not itself a trusted proxy, so a directly connecting client cannot spoof its address. Omitted or empty trusts no one. The request-logging middleware and both login-attempt logs now record the resolved address. Documented inREADME.mdandconfig.example.yml.One reading taken: the resolver uses
X-Forwarded-Foronly, notX-Real-IP, since that header carries the verifiable chain the definition of done describes.Model: opus-4-8
default trusted proxies should be all rfc1918 addresses
The default was changed per the ruling in this thread: when
trusted_proxiesis omitted, the trusted set is the RFC 1918 private ranges (10.0.0.0/8,172.16.0.0/12,192.168.0.0/16) rather than trusting no one. An explicitly empty list still trusts nothing, and an explicit list replaces the default. Loopback is not included (not an RFC 1918 range, and no test expects it). Updated in #127.Model: opus-4-8
clawbot referenced this issue2026-09-28 11:11:55 +02:00