fix(indexer): disable IPv6 API listener by default for rate limiting (#282) #779

Merged
PlasticDigits merged 1 commit from cursor/gitlab-issue-workflow-0680 into main 2026-06-05 09:56:36 +00:00
PlasticDigits commented 2026-06-05 09:42:59 +00:00 (Migrated from gitlab.com)

Summary

Addresses https://gitlab.com/PlasticDigits/cl8y-dex-terraclassic/-/work_items/282 per the deployment topology confirmed in issue comments:

  • No trusted forwarded header (CF-Connecting-IP / X-Forwarded-For absent on Render) → rate limits key on socket peer IP via PeerIpKeyExtractor (not a spoofable SmartIp/XFF extractor).
  • IPv6 disabled by default (API_IPV6_ENABLED off) → API binds an IPv4-only socket; IPv6 API_BIND values are rejected at config parse. Avoids IPv6 /64 address-rotation abuse without /64 bucketing (per PlasticDigits feedback on !776).

Changes

  • indexer/src/api/mod.rs: bind_api_listener() (socket2, IPv4 domain when IPv6 off); PeerIpKeyExtractor replaces custom /64 extractor.
  • indexer/src/config.rs: api_ipv6_enabled + API_IPV6_ENABLED env; Ipv6BindDisabled config error.
  • Tests: IPv4-only bind, forwarded-header ignored, distinct IPv4 peer keys, config validation.
  • Docs: docs/indexer-invariants.md, skills/AGENTS_INDEXER_API_LCD_SECURITY.md, indexer/.env.example.

Acceptance criteria

Criterion Verification Result
Behind intended proxy, distinct clients get distinct buckets (spoofed XFF from trusted proxy only) N/A for this deployment — no trusted forwarded header; peer IP is the client IP on direct Render exposure. Verified PeerIpKeyExtractor keys distinct IPv4 peers independently (cargo test --lib peer_ip_extractor_keys_distinct_ipv4_peers). SKIP (topology)
Trusted-proxy header not honored from arbitrary upstreams cargo test --lib peer_ip_extractor_ignores_forwarded_headers — X-Forwarded-For / Forwarded headers do not change bucket key PASS
IPv6 abuse vector mitigated API_IPV6_ENABLED defaults off; cargo test --lib api_listener + cargo test --lib ipv6_bind_rejected PASS

Verification checklist (third parties)

cd indexer && cargo test --lib api_listener rate_limit_key ipv6
cd indexer && cargo test --lib   # full lib suite (105 tests)

Manual: set API_BIND=:: without API_IPV6_ENABLED=1 → indexer fails at startup with Ipv6BindDisabled.

Follow-ups

If deployment later sits behind a proxy that injects a trusted client-IP header, swap to a SmartIp extractor with the trusted hop configured (one-line change at apply_rate_limit_layer).

Related to #282


Note

Medium Risk
Changes how the API is exposed and how per-IP limits apply; IPv6-only or dual-stack deployments must set API_IPV6_ENABLED=1, and operators behind proxies still rely on socket peer IP (no trusted XFF).

Overview
Hardens indexer API abuse controls (#282) by making the HTTP listener IPv4-only by default and aligning rate-limit keys with the socket peer only.

Listener: Adds API_IPV6_ENABLED (off unless 1/true/yes) and bind_api_listener via socket2 so the server binds an IPv4 socket when disabled; IPv6 API_BIND values fail at config load and at bind unless the flag is set. Startup logging includes ipv6_enabled.

Rate limits: Drops the custom IPv6 /64 key extractor in favor of PeerIpKeyExtractor (peer IP from ConnectInfo only). Unit tests assert Forwarded / X-Forwarded-For are ignored so limits cannot be keyed on spoofable headers in this deployment.

Docs (invariants, .env.example, LCD security playbook) and integration test Config fixtures are updated for the new flag.

Reviewed by Cursor Bugbot for commit 17e7cfb74f. Bugbot is set up for automated code reviews on this repo. Configure here.

## Summary Addresses https://gitlab.com/PlasticDigits/cl8y-dex-terraclassic/-/work_items/282 per the deployment topology confirmed in issue comments: - **No trusted forwarded header** (`CF-Connecting-IP` / `X-Forwarded-For` absent on Render) → rate limits key on **socket peer IP** via `PeerIpKeyExtractor` (not a spoofable SmartIp/XFF extractor). - **IPv6 disabled by default** (`API_IPV6_ENABLED` off) → API binds an **IPv4-only** socket; IPv6 `API_BIND` values are rejected at config parse. Avoids IPv6 `/64` address-rotation abuse without /64 bucketing (per PlasticDigits feedback on !776). ### Changes - `indexer/src/api/mod.rs`: `bind_api_listener()` (socket2, IPv4 domain when IPv6 off); `PeerIpKeyExtractor` replaces custom /64 extractor. - `indexer/src/config.rs`: `api_ipv6_enabled` + `API_IPV6_ENABLED` env; `Ipv6BindDisabled` config error. - Tests: IPv4-only bind, forwarded-header ignored, distinct IPv4 peer keys, config validation. - Docs: `docs/indexer-invariants.md`, `skills/AGENTS_INDEXER_API_LCD_SECURITY.md`, `indexer/.env.example`. ## Acceptance criteria | Criterion | Verification | Result | |-----------|--------------|--------| | Behind intended proxy, distinct clients get distinct buckets (spoofed XFF from trusted proxy only) | **N/A for this deployment** — no trusted forwarded header; peer IP is the client IP on direct Render exposure. Verified `PeerIpKeyExtractor` keys distinct IPv4 peers independently (`cargo test --lib peer_ip_extractor_keys_distinct_ipv4_peers`). | SKIP (topology) | | Trusted-proxy header not honored from arbitrary upstreams | `cargo test --lib peer_ip_extractor_ignores_forwarded_headers` — X-Forwarded-For / Forwarded headers do not change bucket key | PASS | | IPv6 abuse vector mitigated | `API_IPV6_ENABLED` defaults off; `cargo test --lib api_listener` + `cargo test --lib ipv6_bind_rejected` | PASS | ## Verification checklist (third parties) ```bash cd indexer && cargo test --lib api_listener rate_limit_key ipv6 cd indexer && cargo test --lib # full lib suite (105 tests) ``` Manual: set `API_BIND=::` without `API_IPV6_ENABLED=1` → indexer fails at startup with `Ipv6BindDisabled`. ## Follow-ups If deployment later sits behind a proxy that injects a **trusted** client-IP header, swap to a SmartIp extractor with the trusted hop configured (one-line change at `apply_rate_limit_layer`). Related to #282 <!-- CURSOR_SUMMARY --> --- > [!NOTE] > **Medium Risk** > Changes how the API is exposed and how per-IP limits apply; IPv6-only or dual-stack deployments must set `API_IPV6_ENABLED=1`, and operators behind proxies still rely on socket peer IP (no trusted XFF). > > **Overview** > Hardens indexer API abuse controls (**#282**) by making the HTTP listener **IPv4-only by default** and aligning rate-limit keys with the socket peer only. > > **Listener:** Adds `API_IPV6_ENABLED` (off unless `1`/`true`/`yes`) and `bind_api_listener` via **`socket2`** so the server binds an IPv4 socket when disabled; IPv6 `API_BIND` values fail at config load and at bind unless the flag is set. Startup logging includes `ipv6_enabled`. > > **Rate limits:** Drops the custom IPv6 `/64` key extractor in favor of **`PeerIpKeyExtractor`** (peer IP from `ConnectInfo` only). Unit tests assert **`Forwarded` / `X-Forwarded-For` are ignored** so limits cannot be keyed on spoofable headers in this deployment. > > Docs (invariants, `.env.example`, LCD security playbook) and integration test `Config` fixtures are updated for the new flag. > > <sup>Reviewed by [Cursor Bugbot](https://cursor.com/bugbot) for commit 17e7cfb74f5ed7f550c131e412793e517effeaba. Bugbot is set up for automated code reviews on this repo. Configure [here](https://www.cursor.com/dashboard/bugbot).</sup> <!-- /CURSOR_SUMMARY -->
ghost1 commented 2026-06-05 09:43:05 +00:00 (Migrated from gitlab.com)

changed the description

changed the description
PlasticDigits commented 2026-06-05 09:43:10 +00:00 (Migrated from gitlab.com)

changed the description

changed the description
PlasticDigits commented 2026-06-05 09:43:11 +00:00 (Migrated from gitlab.com)

mentioned in issue #282

mentioned in issue #282
ghost1 commented 2026-06-05 09:43:13 +00:00 (Migrated from gitlab.com)

changed the description

changed the description
PlasticDigits commented 2026-06-05 09:45:50 +00:00 (Migrated from gitlab.com)

Security review

Commit reviewed: 17e7cfb74f5ed7f550c131e412793e517effeaba
Scope: IPv4-only API listener (bind_api_listener / API_IPV6_ENABLED), rate-limit keying switch to PeerIpKeyExtractor, config validation, docs/tests.

Outcome: FINDINGS: 0 medium+

This MR hardens abuse controls for the documented Render deployment (direct client exposure, no trusted forwarded-client header). Reviewed the diff and traced attacker-controlled inputs to sinks:

  • Rate-limit keys — PeerIpKeyExtractor keys only on ConnectInfo socket peer IP; Forwarded / X-Forwarded-For are ignored (unit-tested). No spoofable header path to another bucket or bypass.
  • IPv6 /64 rotation — default API_IPV6_ENABLED=off binds an IPv4-only socket (Domain::IPV4 via socket2); IPv6 API_BIND values fail at config parse and bind. Mitigates the prior abuse vector without introducing a new client-controlled bypass.
  • Opt-in IPv6 — setting API_IPV6_ENABLED=1 restores per-address (not /64) bucketing; this is operator-controlled and documented, not attacker-triggerable on the default path.
  • Other classes — no injection, authz, secret leakage, SSRF/XSS, or unsafe deserialization changes in the diff; socket2 dependency is a standard listen helper.

Inline threads: none (no medium+ findings).

Prior MR comments: none to re-report.

## Security review **Commit reviewed:** `17e7cfb74f5ed7f550c131e412793e517effeaba` **Scope:** IPv4-only API listener (`bind_api_listener` / `API_IPV6_ENABLED`), rate-limit keying switch to `PeerIpKeyExtractor`, config validation, docs/tests. **Outcome:** `FINDINGS: 0` medium+ This MR hardens abuse controls for the documented Render deployment (direct client exposure, no trusted forwarded-client header). Reviewed the diff and traced attacker-controlled inputs to sinks: - **Rate-limit keys** — `PeerIpKeyExtractor` keys only on `ConnectInfo` socket peer IP; `Forwarded` / `X-Forwarded-For` are ignored (unit-tested). No spoofable header path to another bucket or bypass. - **IPv6 /64 rotation** — default `API_IPV6_ENABLED=off` binds an IPv4-only socket (`Domain::IPV4` via `socket2`); IPv6 `API_BIND` values fail at config parse and bind. Mitigates the prior abuse vector without introducing a new client-controlled bypass. - **Opt-in IPv6** — setting `API_IPV6_ENABLED=1` restores per-address (not /64) bucketing; this is operator-controlled and documented, not attacker-triggerable on the default path. - **Other classes** — no injection, authz, secret leakage, SSRF/XSS, or unsafe deserialization changes in the diff; `socket2` dependency is a standard listen helper. **Inline threads:** none (no medium+ findings). Prior MR comments: none to re-report.
PlasticDigits (Migrated from gitlab.com) merged commit 2e2de53890 into main 2026-06-05 09:56:36 +00:00
PlasticDigits commented 2026-06-05 09:56:37 +00:00 (Migrated from gitlab.com)

mentioned in commit 2e2de53890

mentioned in commit 2e2de53890588abff85e491f7871bd75a219d0b5
Sign in to join this conversation.
No reviewers
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
code/cl8y-dex-terraclassic!779
No description provided.