API rate limiter keys on peer IP with no forwarded-header extractor #282
Labels
No labels
agent:fix_bugfix
agent:fix_conflicts
agent:fix_security
agent:gap_analysis
agent:implement
agent:implement
agent:implement
agent:open_issues
agent:ready
agent:research
agent:security_audit
agent:verify
architecture
backend
blocker:hybrid
blocker:launch
blocker:limit-orders
blocker:v2
block:log_only
block:security
bug
ci
contracts
correctness
deploy
dev
devops
docs
documentation
duplicate
e2e
enhancement
epic
feature
frontend
functional-completion
gas
good first issue
governance
help wanted
high-risk
hooks
hybrid
indexer
infra
infrastructure
integrators
invalid
launch-blocker
limit-orders
localnet
localterra
low priority
missing-implementation
needs-design
ops
performance
priority
high
priority
medium
product
qa
QA
question
ready
ready
research
scripts
security
security-hardening
smartcontracts
tech-debt
testing
ux
UX
v2
verification
wontfix
No milestone
No project
No assignees
1 participant
Notifications
Due date
No due date set.
Dependencies
No dependencies set.
Reference
code/cl8y-dex-terraclassic#282
Loading…
Add table
Add a link
Reference in a new issue
No description provided.
Delete branch "%!s()"
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?
Severity: Medium (depends on the deployment topology)
Reachability: Affects every client of the public API.
Affected:
apply_rate_limit_layer(indexer/src/api/mod.rs).Root cause: the rate limiter keys on the direct socket peer IP and never reads a forwarded-client header, so behind a reverse proxy every client shares one bucket.
Summary
apply_rate_limit_layerbuildsGovernorConfigBuilder::default(), whose default key extractor is the peer IP..use_headers()is set, but that only addsX-RateLimit-*to responses — it does not make the extractor readX-Forwarded-For/ a real-client header.Two failure modes depending on how it's deployed:
Either way the limit isn't doing what it's meant to. This also undercuts the lcd-heavy throttle that other reports lean on.
Current codebase
mod.rsapply_rate_limit_layer:GovernorConfigBuilder::default().per_second(rps).burst_size(...).use_headers().finish()— default (peer-IP) key extractor, no forwarded-header extractor.Recommended direction
Acceptance criteria
X-Forwarded-Forfrom the trusted proxy only).Test plan (abuse)
Not sure but we are planning to deploy on render which uses cloudflare - in the past ip detection has not been an issue - but need to investigate specifically how this is handled. Research requried
Holding this on your infra call, per your note — quick confirmation of why it can't be implemented blind.
The limiter keys on the socket peer IP (tower_governor
PeerIpKeyExtractordefault;into_make_service_with_connect_info::<SocketAddr>), no forwarded-header extractor. The fix is small in code (swap in a SmartIp/XFF key extractor) but its SECURITY correctness depends entirely on the trusted-proxy boundary: on Render-behind-Cloudflare every request arrives from a small set of CF/Render egress IPs, which is exactly the shared-bucket-collapse this finding warns about — so that topology makes the risk MORE likely, not less. And trusting a forwarded header from the wrong hop lets anyone spoof their bucket key, which is worse than today.So: I need from you the client-IP propagation Render/Cloudflare actually uses (
CF-Connecting-IPvsX-Forwarded-For, and which hop is trusted) before wiring the extractor. Once you confirm the trusted hop I'll do the (small) code change + an IPv6 /64 bucket. This pairs with #278 (both about whether the public API throttle is actually effective per-client). No code from me until the topology is pinned. @PlasticDigitsWe do not have a CG-Connecting-IP or X-Forwarded-For as far as Im aware.
Proceed with the implementation for rate limiting IP addresses
mentioned in merge request !776
Done per your call (no CG-Connecting-IP / X-Forwarded-For -> rate limit on IP). MR !776.
Since there's no trusted forwarded header, I kept the key on the socket peer IP — a forwarded-header extractor would only add a spoofable bucket key on the wrong trust boundary. The gap left was the default extractor keying on the FULL IPv6 address, so a client can rotate through its own /64 (smallest block a host gets) to multiply its limit. Added a custom KeyExtractor that collapses IPv6 to its /64 prefix; IPv4 keeps the full address. It runs through the single apply_rate_limit_layer chokepoint, so both the general limiter and the lcd-heavy one (#278) get it.
Tests: rate_limit_key_tests (same /64 -> one key; different /64 and IPv4 -> distinct; the extractor reads ConnectInfo and buckets by /64). Existing burst->429 integration tests stay green.
If the deployment later sits behind a proxy that injects a trusted client-IP header, it's a one-line swap to a SmartIp extractor with the trusted hop — flag me. @PlasticDigits
mentioned in commit
e70f2f9e75Approved, but we should just disable ipv6 by default as it seems to be introducing uncessary risks, and ipv4 is quite cheap anyway
mentioned in commit
17e7cfb74fmentioned in merge request !779
Implementation complete — MR !779
Per your direction (no
CF-Connecting-IP/X-Forwarded-For; disable IPv6 by default instead of /64 bucketing):What changed
PeerIpKeyExtractor— no forwarded-header extractor (would be spoofable without a trusted hop).API_IPV6_ENABLEDoff); IPv6API_BINDrejected at config parse.docs/indexer-invariants.md,skills/AGENTS_INDEXER_API_LCD_SECURITY.md.Acceptance criteria
cargo test --lib peer_ip_extractor_keys_distinct_ipv4_peerscargo test --lib peer_ip_extractor_ignores_forwarded_headerscargo test --lib api_listener+cargo test --lib ipv6_bind_rejectedFull lib suite:
cd indexer && cargo test --lib— 105 passed.MR: https://gitlab.com/PlasticDigits/cl8y-dex-terraclassic/-/merge_requests/61
Issue: https://gitlab.com/PlasticDigits/cl8y-dex-terraclassic/-/work_items/282
mentioned in merge request !783
mentioned in commit
2e2de53890mentioned in merge request !785
Verification — issue #282 (agent:verify)
Issue: https://gitlab.com/PlasticDigits/cl8y-dex-terraclassic/-/work_items/282
Implementation: MR !779 (merged) — peer-IP rate limiting, no forwarded-header extractor, IPv4-only API by default.
Deployment decision (from issue thread)
Render exposure has no trusted
CF-Connecting-IP/X-Forwarded-For. Rate limits therefore key on socket peer IP (PeerIpKeyExtractor); a SmartIp/XFF extractor would be spoofable. IPv6 is disabled by default (API_IPV6_ENABLEDoff) to mitigate/64address-rotation abuse.Acceptance criteria
cargo test --lib peer_ip_extractor_keys_distinct_ipv4_peers— okcargo test --lib peer_ip_extractor_ignores_forwarded_headers— ok/64rotation abuse mitigatedAPI_IPV6_ENABLEDdefaults off;cargo test --lib ipv6_bind_rejected+ipv6_bind_allowed_when_ipv6_enabled+api_listener— okcargo test --test security rate_limit— 3 tests ok (rate_limit_returns_429_when_exceeded, LCD-heavy + CG/CMC paths)docs/indexer-invariants.md,skills/AGENTS_INDEXER_API_LCD_SECURITY.md,indexer/.env.exampledocument peer-IP keying + IPv4-only defaultCommands (2026-06-05)
Overall: PASS (with topology SKIP on proxy/XFF criterion per product direction).
mentioned in merge request !818