fix(web): Key rate limiters on the resolved client IP behind the proxy #106
No reviewers
Labels
No milestone
No project
No assignees
1 participant
Notifications
Due date
No due date set.
Dependencies
No dependencies set.
Reference
rosa/vernier!106
Loading…
Add table
Add a link
Reference in a new issue
No description provided.
Delete branch "fix/91-rate-limit-real-ip"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Closes #91.
Problem
Every governor in the web router keyed on
PeerIp— the raw TCP peer — while aRealIpLayerresolved a client IP into an extension that nothing read. Behind the Fly.io proxy the app's peer is alwaysfly-proxy, so every request shared one IP and each per-IP limiter collapsed into a single global bucket: the auth limiter's 10/min became a site-wide login cap, and no individual attacker was ever isolated below the shared budget.Topology decision recorded in ADR 0012.
Change
SmartIp, configured with the private-network CIDRs as trusted proxies.SmartIpconsultsX-Forwarded-Foronly when the TCP peer is trusted and takes the rightmost untrusted entry — the client addressfly-proxyappends, which a client can't spoof past by prepending its own entries.RealIpLayerand therealcrate.RealIpLayer::default()honors a rawX-Real-IPand the leftmostX-Forwarded-Forentry with no peer-trust gating — both client-controllable — so keying on it would let any client forge a fresh bucket per request.SmartIpis the spoof-resistant equivalent.ipnetfor the CIDR type (already in the build as a transitive dep of axum-governor).Trust boundary
Trust rests on a deployment guarantee: only
fly-proxyreaches the app's internal port, so any private peer is the proxy. Exposing the app without a proxy would require revisiting ADR 0012 and the trusted-CIDR set together — called out in the ADR.Tests
Extractor-level unit tests covering the keying contract:
UntrustedProxyformat,clippy, andciall pass.