Harden internet-facing indexer production checklist and CORS bind guard #326

Closed
opened 2026-06-14 22:31:57 +00:00 by PlasticDigits · 5 comments
PlasticDigits commented 2026-06-14 22:31:57 +00:00 (Migrated from gitlab.com)

Parent

GitLab #325 — Security review (audit finding F-04, approved recommendation #2).

Current codebase

  • indexer/src/cors_config.rs: when INDEXER_PRODUCTION is unset/false, the API uses CorsLayer::permissive() — any browser origin can read responses.
  • When INDEXER_PRODUCTION=1 / true / yes, startup requires non-empty CORS_ALLOWED_ORIGINS (comma-separated exact origins).
  • indexer/src/config.rs: production mode already fail-closes on placeholder DATABASE_URL substrings (#142) and invalid ADDRESS_REGISTRY (#156).
  • Default LISTEN_ADDR is 127.0.0.1:3100; operators can bind 0.0.0.0 for public exposure.
  • indexer/README.md documents INDEXER_PRODUCTION rules but lacks a single internet-facing deploy checklist tying CORS, bind address, and production flag together.
  • indexer/.env.example mentions production CORS but does not warn about public bind without production mode.

Why needed

An operator who exposes the indexer on a public URL without INDEXER_PRODUCTION=1 enables permissive CORS. That does not bypass onchain auth (API is read-only) but increases phishing/social-engineering surface: third-party sites can embed live arena and wallet-scoped public stats in deceptive UIs.

Parent #325 approved mandatory production configuration for any internet-facing indexer URL.

Constraints / guardrails

  • Do not break local Anvil stacks: scripts/start-local-anvil-stack.sh must continue to work with postgres://yieldomega:password@… and permissive CORS when INDEXER_PRODUCTION is omitted.
  • Preserve existing invariants INV-INDEXER-142, INV-INDEXER-156, and CORS behavior under INDEXER_PRODUCTION=1.
  • Prefer fail-loud warnings over hard-failing localhost dev; consider hard fail only when binding non-loopback without production mode (discuss in MR).
  • AGPL; no secrets in docs.

Relevant files

Area Paths
CORS indexer/src/cors_config.rs
Config / listen indexer/src/config.rs, indexer/src/main.rs
Docs indexer/README.md, indexer/.env.example, docs/operations/deployment-guide.md
Verify scripts/verify-cloud-postgres.sh, indexer/tests/integration_stage2.rs
Invariants docs/testing/invariants-and-business-logic.md
  1. Operator checklist (docs): add a short “Internet-facing indexer” section covering:
    • Set INDEXER_PRODUCTION=1
    • Set CORS_ALLOWED_ORIGINS to production frontend origin(s) only
    • Use non-placeholder DATABASE_URL and validated ADDRESS_REGISTRY
    • Link from docs/operations/deployment-guide.md indexer section
  2. Startup guard: when listen_addr is not loopback (127.0.0.1 / ::1) and INDEXER_PRODUCTION is false, emit a high-visibility tracing::warn! (or exit non-zero if team agrees) naming F-04 and pointing to the checklist.
  3. .env.example: prominent comment that public LISTEN_ADDR requires INDEXER_PRODUCTION=1 + CORS_ALLOWED_ORIGINS.
  4. Optional: add invariant ID (e.g. INV-INDEXER-325-PUBLIC-CORS) and unit test for the bind-address + production-mode check.

Acceptance criteria

  • Documented internet-facing checklist in indexer/README.md (and cross-link from deployment guide).
  • Indexer logs a clear warning (or fails startup) when listening on a non-loopback address without INDEXER_PRODUCTION.
  • Local Anvil stack (bash scripts/start-local-anvil-stack.sh) unchanged: starts with default env, permissive CORS OK on 127.0.0.1.
  • With INDEXER_PRODUCTION=1 + valid CORS origins + real DATABASE_URL, startup still succeeds.
  • New/updated tests cover the bind-address guard logic.

Test plan

Path Command
Indexer unit cd indexer && cargo clippy --all-targets -- -D warnings && cargo test
Integration cd indexer && cargo test --test integration_stage2 (with YIELDOMEGA_PG_TEST_URL or native Postgres per AGENTS.md)
Local stack smoke bash scripts/verify-cloud-postgres.sh then bash scripts/verify-podium-live-anvil.sh or stack start on :3100
Doc anchors bash scripts/check-doc-anchors.sh (if docs touched)

Attack / abuse test plan

  • Deploy indexer with LISTEN_ADDR=0.0.0.0:3100, no INDEXER_PRODUCTION: confirm warning appears in logs; from another origin in browser/curl, confirm Access-Control-Allow-Origin: * behavior (documents risk).
  • Repeat with INDEXER_PRODUCTION=1 + CORS_ALLOWED_ORIGINS=https://allowed.example: only allowed origin receives CORS headers on preflight.
  • Confirm no regression: production fail-closed still rejects :password@ in DATABASE_URL.

Verification criteria

  • MR checklist row: Indexer public deploy guard → commands above → PASS
  • Parent #325 recommendation #2 closed when checklist + startup guard ship.
## Parent [GitLab #325 — Security review](https://gitlab.com/PlasticDigits/yieldomega/-/issues/325) (audit finding **F-04**, approved recommendation **#2**). ## Current codebase - [`indexer/src/cors_config.rs`](indexer/src/cors_config.rs): when `INDEXER_PRODUCTION` is unset/false, the API uses `CorsLayer::permissive()` — any browser origin can read responses. - When `INDEXER_PRODUCTION=1` / `true` / `yes`, startup requires non-empty `CORS_ALLOWED_ORIGINS` (comma-separated exact origins). - [`indexer/src/config.rs`](indexer/src/config.rs): production mode already fail-closes on placeholder `DATABASE_URL` substrings (#142) and invalid `ADDRESS_REGISTRY` (#156). - Default `LISTEN_ADDR` is `127.0.0.1:3100`; operators can bind `0.0.0.0` for public exposure. - [`indexer/README.md`](indexer/README.md) documents `INDEXER_PRODUCTION` rules but lacks a single **internet-facing deploy checklist** tying CORS, bind address, and production flag together. - [`indexer/.env.example`](indexer/.env.example) mentions production CORS but does not warn about public bind without production mode. ## Why needed An operator who exposes the indexer on a public URL without `INDEXER_PRODUCTION=1` enables permissive CORS. That does not bypass onchain auth (API is read-only) but increases phishing/social-engineering surface: third-party sites can embed live arena and wallet-scoped public stats in deceptive UIs. Parent #325 approved mandatory production configuration for any internet-facing indexer URL. ## Constraints / guardrails - **Do not** break local Anvil stacks: [`scripts/start-local-anvil-stack.sh`](scripts/start-local-anvil-stack.sh) must continue to work with `postgres://yieldomega:password@…` and permissive CORS when `INDEXER_PRODUCTION` is omitted. - Preserve existing invariants **`INV-INDEXER-142`**, **`INV-INDEXER-156`**, and CORS behavior under `INDEXER_PRODUCTION=1`. - Prefer **fail-loud warnings** over hard-failing localhost dev; consider hard fail only when binding non-loopback without production mode (discuss in MR). - AGPL; no secrets in docs. ## Relevant files | Area | Paths | |------|-------| | CORS | `indexer/src/cors_config.rs` | | Config / listen | `indexer/src/config.rs`, `indexer/src/main.rs` | | Docs | `indexer/README.md`, `indexer/.env.example`, `docs/operations/deployment-guide.md` | | Verify | `scripts/verify-cloud-postgres.sh`, `indexer/tests/integration_stage2.rs` | | Invariants | `docs/testing/invariants-and-business-logic.md` | ## Recommended direction 1. **Operator checklist** (docs): add a short “Internet-facing indexer” section covering: - Set `INDEXER_PRODUCTION=1` - Set `CORS_ALLOWED_ORIGINS` to production frontend origin(s) only - Use non-placeholder `DATABASE_URL` and validated `ADDRESS_REGISTRY` - Link from `docs/operations/deployment-guide.md` indexer section 2. **Startup guard**: when `listen_addr` is not loopback (`127.0.0.1` / `::1`) and `INDEXER_PRODUCTION` is false, emit a **high-visibility `tracing::warn!`** (or exit non-zero if team agrees) naming F-04 and pointing to the checklist. 3. **`.env.example`**: prominent comment that public `LISTEN_ADDR` requires `INDEXER_PRODUCTION=1` + `CORS_ALLOWED_ORIGINS`. 4. Optional: add invariant ID (e.g. `INV-INDEXER-325-PUBLIC-CORS`) and unit test for the bind-address + production-mode check. ## Acceptance criteria - [ ] Documented internet-facing checklist in `indexer/README.md` (and cross-link from deployment guide). - [ ] Indexer logs a clear warning (or fails startup) when listening on a non-loopback address without `INDEXER_PRODUCTION`. - [ ] Local Anvil stack (`bash scripts/start-local-anvil-stack.sh`) unchanged: starts with default env, permissive CORS OK on `127.0.0.1`. - [ ] With `INDEXER_PRODUCTION=1` + valid CORS origins + real `DATABASE_URL`, startup still succeeds. - [ ] New/updated tests cover the bind-address guard logic. ## Test plan | Path | Command | |------|---------| | Indexer unit | `cd indexer && cargo clippy --all-targets -- -D warnings && cargo test` | | Integration | `cd indexer && cargo test --test integration_stage2` (with `YIELDOMEGA_PG_TEST_URL` or native Postgres per `AGENTS.md`) | | Local stack smoke | `bash scripts/verify-cloud-postgres.sh` then `bash scripts/verify-podium-live-anvil.sh` or stack start on `:3100` | | Doc anchors | `bash scripts/check-doc-anchors.sh` (if docs touched) | ## Attack / abuse test plan - Deploy indexer with `LISTEN_ADDR=0.0.0.0:3100`, **no** `INDEXER_PRODUCTION`: confirm warning appears in logs; from another origin in browser/curl, confirm `Access-Control-Allow-Origin: *` behavior (documents risk). - Repeat with `INDEXER_PRODUCTION=1` + `CORS_ALLOWED_ORIGINS=https://allowed.example`: only allowed origin receives CORS headers on preflight. - Confirm no regression: production fail-closed still rejects `:password@` in `DATABASE_URL`. ## Verification criteria - MR checklist row: *Indexer public deploy guard → commands above → PASS* - Parent #325 recommendation #2 closed when checklist + startup guard ship.
PlasticDigits commented 2026-06-14 22:32:09 +00:00 (Migrated from gitlab.com)

mentioned in issue #325

mentioned in issue #325
PlasticDigits commented 2026-06-14 22:50:40 +00:00 (Migrated from gitlab.com)

mentioned in merge request !499

mentioned in merge request !499
PlasticDigits commented 2026-06-15 01:01:12 +00:00 (Migrated from gitlab.com)

mentioned in commit 2d0251d297

mentioned in commit 2d0251d297ea0fc79d8bc52b408b0accdb675580
PlasticDigits commented 2026-06-15 01:20:05 +00:00 (Migrated from gitlab.com)

Verification — GitLab #326 (PASS)

Verified Harden internet-facing indexer production checklist and CORS bind guard on main (no repo changes; implementation already merged).

Acceptance criteria

Item Result How verified
Internet-facing checklist in indexer/README.md PASS Section § Internet-facing indexer documents INDEXER_PRODUCTION, CORS_ALLOWED_ORIGINS, DATABASE_URL, ADDRESS_REGISTRY, bind guidance
Cross-link from deployment guide PASS docs/operations/deployment-guide.md links to checklist anchor
Startup warning on non-loopback without INDEXER_PRODUCTION PASS LISTEN_ADDR=0.0.0.0:3199 (no production flag) → log: LISTEN_ADDR is not loopback but INDEXER_PRODUCTION is unset — API uses permissive CORS… naming F-04 and checklist links
Local Anvil stack unchanged (loopback + permissive CORS) PASS bash scripts/verify-podium-live-anvil.sh → OK; loopback 127.0.0.1:3201 bind emits no public-bind warning
INDEXER_PRODUCTION=1 + CORS + non-placeholder DATABASE_URL startup PASS Strong-credential URL + CORS_ALLOWED_ORIGINS=https://allowed.example → indexer starts; no public-bind warning when production set
Bind-address guard tests PASS public_bind_guard_tests in indexer/src/config.rs (loopback detection, warn skip paths)

Test plan

Command Result
cd indexer && cargo clippy --all-targets -- -D warnings PASS
cd indexer && cargo test PASS (74 unit tests incl. public_bind_guard_tests, CORS parse tests, INV-INDEXER-142 DB URL guards)
cd indexer && cargo test --test integration_stage2 -- --test-threads=1 PASS (4/4; parallel default can race on shared test DB — use --test-threads=1 if flaky)
bash scripts/verify-cloud-postgres.sh PASS
bash scripts/verify-podium-live-anvil.sh PASS
bash scripts/check-doc-anchors.sh PASS (incl. INV-INDEXER-326-PUBLIC-BIND-CORS anchor)

Attack / abuse checks

Scenario Result
LISTEN_ADDR=0.0.0.0, no INDEXER_PRODUCTION PASS — high-visibility tracing::warn!; preflight Origin: https://evil.example → access-control-allow-origin: *
INDEXER_PRODUCTION=1, CORS_ALLOWED_ORIGINS=https://allowed.example PASS — allowed origin receives access-control-allow-origin: https://allowed.example; disallowed origin omits allow-origin
Production fail-closed on :password@ in DATABASE_URL PASS — startup error before DB connect (INV-INDEXER-142)

Invariants

  • INV-INDEXER-326-PUBLIC-BIND-CORS documented in docs/testing/invariants-and-business-logic.md with cross-links to warn_public_bind_without_production and README.
  • INV-INDEXER-142 / INV-INDEXER-156 unchanged (unit tests pass).

Closes recommendation #2 from parent #325 (audit F-04).

## Verification — GitLab #326 (PASS) Verified **Harden internet-facing indexer production checklist and CORS bind guard** on `main` (no repo changes; implementation already merged). ### Acceptance criteria | Item | Result | How verified | |------|--------|--------------| | Internet-facing checklist in `indexer/README.md` | **PASS** | Section [§ Internet-facing indexer](indexer/README.md#internet-facing-indexer-gitlab-326) documents `INDEXER_PRODUCTION`, `CORS_ALLOWED_ORIGINS`, `DATABASE_URL`, `ADDRESS_REGISTRY`, bind guidance | | Cross-link from deployment guide | **PASS** | `docs/operations/deployment-guide.md` links to checklist anchor | | Startup warning on non-loopback without `INDEXER_PRODUCTION` | **PASS** | `LISTEN_ADDR=0.0.0.0:3199` (no production flag) → log: `LISTEN_ADDR is not loopback but INDEXER_PRODUCTION is unset — API uses permissive CORS…` naming F-04 and checklist links | | Local Anvil stack unchanged (loopback + permissive CORS) | **PASS** | `bash scripts/verify-podium-live-anvil.sh` → OK; loopback `127.0.0.1:3201` bind emits no public-bind warning | | `INDEXER_PRODUCTION=1` + CORS + non-placeholder `DATABASE_URL` startup | **PASS** | Strong-credential URL + `CORS_ALLOWED_ORIGINS=https://allowed.example` → indexer starts; no public-bind warning when production set | | Bind-address guard tests | **PASS** | `public_bind_guard_tests` in `indexer/src/config.rs` (loopback detection, warn skip paths) | ### Test plan | Command | Result | |---------|--------| | `cd indexer && cargo clippy --all-targets -- -D warnings` | **PASS** | | `cd indexer && cargo test` | **PASS** (74 unit tests incl. `public_bind_guard_tests`, CORS parse tests, `INV-INDEXER-142` DB URL guards) | | `cd indexer && cargo test --test integration_stage2 -- --test-threads=1` | **PASS** (4/4; parallel default can race on shared test DB — use `--test-threads=1` if flaky) | | `bash scripts/verify-cloud-postgres.sh` | **PASS** | | `bash scripts/verify-podium-live-anvil.sh` | **PASS** | | `bash scripts/check-doc-anchors.sh` | **PASS** (incl. `INV-INDEXER-326-PUBLIC-BIND-CORS` anchor) | ### Attack / abuse checks | Scenario | Result | |----------|--------| | `LISTEN_ADDR=0.0.0.0`, no `INDEXER_PRODUCTION` | **PASS** — high-visibility `tracing::warn!`; preflight `Origin: https://evil.example` → `access-control-allow-origin: *` | | `INDEXER_PRODUCTION=1`, `CORS_ALLOWED_ORIGINS=https://allowed.example` | **PASS** — allowed origin receives `access-control-allow-origin: https://allowed.example`; disallowed origin omits allow-origin | | Production fail-closed on `:password@` in `DATABASE_URL` | **PASS** — startup error before DB connect (`INV-INDEXER-142`) | ### Invariants - **`INV-INDEXER-326-PUBLIC-BIND-CORS`** documented in `docs/testing/invariants-and-business-logic.md` with cross-links to `warn_public_bind_without_production` and README. - **`INV-INDEXER-142`** / **`INV-INDEXER-156`** unchanged (unit tests pass). Closes recommendation **#2** from parent [#325](https://gitlab.com/PlasticDigits/yieldomega/-/issues/325) (audit F-04).
PlasticDigits (Migrated from gitlab.com) closed this issue 2026-06-15 01:20:06 +00:00
PlasticDigits commented 2026-06-21 03:43:53 +00:00 (Migrated from gitlab.com)

mentioned in issue #341

mentioned in issue #341
Sign in to join this conversation.
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/yieldomega#326
No description provided.