fix(pg-pkg): rate-limit on real client IP (X-Forwarded-For) behind a trusted proxy#230
Conversation
…trusted proxy Per-IP rate limiting (#226) keys on the TCP peer address, which is always the proxy when the PKG runs behind ingress-nginx, so every client shares one bucket. Add ClientIpKeyExtractor and an opt-in --ratelimit-trust-forwarded-for / PKG_RATELIMIT_TRUST_FORWARDED_FOR flag (default false) that keys on the rightmost (trusted, proxy-appended) X-Forwarded-For entry, falling back to the peer address. The rightmost entry is used so client-supplied entries cannot spoof the key.
There was a problem hiding this comment.
Dobby consolidated review — APPROVE ✅
Solid, well-scoped fix. The rightmost-X-Forwarded-For trust model is the correct choice behind an appending proxy (ingress-nginx), the peer-address fallback on a missing/unparseable rightmost entry avoids left-walking into spoofable client data, and the /56 IPv6 normalization mirrors PeerIpKeyExtractor. Off-by-default is the safe posture for a directly-exposed PKG.
Verification (this HEAD):
cargo fmt --manifest-path pg-pkg/Cargo.toml --all -- --check→ passes. The earlier long-#[clap(...)]formatting concern is already resolved bye078b26 style(pg-pkg): wrap long clap attribute (rustfmt).- pg-pkg suite: 53 tests pass, incl. the 10 ratelimit unit tests; clippy clean.
- Rule checks (fmt-before-push, tests-required-on-fixes, conventional-commit title, promised-vs-delivered, core-security): all compliant. Title is conventional; every promised piece (extractor, flag, governor wiring, startup log, tests) is delivered.
One non-blocking defense-in-depth note inline. Good to merge.
|
|
||
| /// The rightmost address in `X-Forwarded-For`, if present and parseable. | ||
| fn forwarded_ip(req: &ServiceRequest) -> Option<IpAddr> { | ||
| let value = req.headers().get(X_FORWARDED_FOR)?.to_str().ok()?; |
There was a problem hiding this comment.
Non-blocking (defense-in-depth): HeaderMap::get returns only the first X-Forwarded-For header line. The rightmost-entry trust model is therefore sound only because ingress-nginx merges hops into one comma-separated header (proxy_add_x_forwarded_for) — correct for the stated infra. If a proxy ever emitted XFF as a separate second header line after a client-supplied one, get() would return the spoofable client line. Fine as-is behind ingress-nginx; noting for future defense-in-depth (get_all + take last).
What
Add an opt-in
X-Forwarded-For-aware key extractor for the/v2rate limiterso per-client limiting works when the PKG runs behind a reverse proxy.
#226introduced per-IP rate limiting keyed on the TCP peer address(
PeerIpKeyExtractor). Behind a reverse proxy (our ingress-nginx) the peer isalways the proxy, so every client shares one bucket — the sensitive
key-issuing endpoints (2 req/s, burst 10) throttle the whole deployment as if
it were a single user. The code comment already noted peer-IP limiting is
"meaningless" behind a proxy.
Change
middleware/ratelimit.rswithClientIpKeyExtractor, a customactix_governor::KeyExtractor(actix-governor 0.10 ships noSmartIpextractor). When
trust_forwarded_foris set it keys on the rightmostX-Forwarded-Forentry — the hop appended by the trusted proxy — fallingback to the peer address when the header is absent/unparseable. It applies
the same IPv6
/56normalization asPeerIpKeyExtractor.--ratelimit-trust-forwarded-for/PKG_RATELIMIT_TRUST_FORWARDED_FORflag (default
false). Off by default so a directly-exposed PKG never trustsclient-supplied headers; operators opt in when they front the PKG with a
trusted proxy.
Why the rightmost entry (security)
X-Forwarded-Foris a list each proxy appends to. Withuse-forwarded-headers: trueon ingress-nginx, a client-supplied XFF is keptand the real client IP is appended last — so only the rightmost entry is
trustworthy; anything left of it is spoofable. We take strictly the rightmost
entry and never walk left (a garbage rightmost falls back to the peer). This
mirrors the
XFF_DEPTH=1handling used elsewhere on the platform behind thesame ingress.
Tests
middleware/ratelimit.rsunit tests cover: peer mode ignores XFF; trustedsingle entry; trusted rightmost wins over a spoofed left entry; fallback to
peer when the header is missing; fallback to peer on an unparseable rightmost
(no left-walk); IPv6
/56normalization.Ops / deployment note
This flag is off by default; deployments that front the PKG with a trusted
proxy must set
PKG_RATELIMIT_TRUST_FORWARDED_FOR=truefor per-client limitingto take effect. (postguard-ops change tracked separately.)