fix(security): stop trusting client-supplied forwarded headers in rate limiting

extract_client_ip took X-Real-IP/X-Forwarded-For from any request, so
a client talking to the backend directly (the FIPS peer listener, or
any non-proxy path) could rotate a fake IP per request and never trip
the login rate limiter. The accept loop now records the TCP peer
address in request extensions, and forwarded headers are honored only
when the connection itself is from loopback — where nginx overwrites
X-Real-IP with the real client address. Direct connections bucket
under their socket IP.

§C of the 1.8.0 hardening plan; 3 new unit tests cover the
loopback/direct/no-header matrix.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
archipelago
2026-07-04 15:48:07 -04:00
co-authored by Claude Fable 5
parent bd7edb4376
commit 9020b8526c
4 changed files with 106 additions and 12 deletions
+16 -5
View File
@@ -131,14 +131,25 @@ modules; production request/boot paths are essentially panic-free. The real risk
revalidate) instead of blocking on systemctl. `image_verifier.rs` cosign sites have no
callers yet — handled with the §A cosign item. Tests: container 155 / transport 29 /
config 29 / package 46 all green.
- [ ] 🟡 **Restrict Bitcoin RPC exposure.** `bootstrap.rs:409` writes
`rpcallowip=0.0.0.0/0`. Scope to the container subnet / `127.0.0.1`.
- [ ] 🟡 **Restrict Bitcoin RPC exposure.** INVESTIGATED 2026-07-03 — the fix is NOT
`rpcallowip`: under rootless-podman NAT every forwarded connection reaches bitcoind
with an in-subnet source IP, so scoping `rpcallowip` blocks nothing (and container
consumers use archy-net DNS anyway). The actual exposure is the host-side publish
`8332:8332` (binds 0.0.0.0 → LAN can hit RPC, auth-only barrier; 4 write sites:
`bootstrap.rs:424`, `package/config.rs:694`, `package/install.rs:1333/2450`, plus
the knots manifest). Real fix = `127.0.0.1:8332:8332` host bind (P2P 8333 stays
public; zmq 28332/28333 should get the same look — unauthenticated). ⚠ May break
external wallets pointed straight at nodeIP:8332 — needs a user call + on-node gate
re-run, so NOT changed drive-by from the dev box.
- [ ] 🟡 **Move generated secrets from env to file mounts.** `manifest.rs:1208-1226`
injects secrets as `-e KEY=value`, readable via `podman inspect` / `/proc/<pid>/environ`.
Prefer bind-mounting the existing `0600` secret file or `podman --secret`.
- [ ] 🟡 **Harden rate-limit IP extraction.** `middleware.rs:120-128` trusts
client-spoofable `X-Real-IP`/`X-Forwarded-For` → per-request bucket rotation defeats the
login limiter. Trust forwarded headers only from a configured proxy; have nginx set them.
- [x] 🟡 **Harden rate-limit IP extraction.** DONE 2026-07-03: the accept loop injects the
TCP `PeerAddr` into request extensions; `extract_client_ip` honors
`X-Real-IP`/`X-Forwarded-For` ONLY when the connection is from loopback (our nginx,
which sets `X-Real-IP $remote_addr`) — direct connections (e.g. the FIPS peer
listener) bucket under their socket IP, so per-request header rotation no longer
defeats the login limiter. 3 unit tests.
- [ ] 🟢 **Include `seq` in the mesh signed preimage.** `message_types.rs:245-288` signs
`(t,v,ts)` but sets the anti-replay `seq` after signing → a radio MITM can alter ordering
without breaking the signature.