P0: Add security & deployment hardening guide (#168) - #186
Conversation
The repo had no single place describing how to expose the API safely. Document the intended posture: a public, read-only GraphQL service meant to run behind a TLS-terminating gateway against a read-only Postgres role, with no application-level auth (gating, if needed, is an operator concern at the gateway). Adds docs/security.md covering the security model, network architecture (TLS gateway, X-Forwarded-For, private Postgres), the built-in abuse protections, a least-privilege read-only DB role (with SQL), operational practices, and a deployment checklist. Linked from the README and the setup guide. Closes #168. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01QSuak9smCHbp4N17xjjLF6
|
Thanks for writing this — a single opinionated "how to expose this safely" doc is exactly what the epic needs, and the read-only One correctness issue in the Built-in protections table is worth fixing before this lands, because it states the opposite of today's behavior and could break the mina-explorer. CORS default isn't "same-origin only". On That feeds a backwards-compat risk with the mina-explorer: it's a browser app served from its own origin that POSTs
would, if followed, block every browser Explorer/dashboard instance. Could we add a note, e.g.:
Two smaller things:
Thanks again — happy to help refine the CORS wording. |
Three corrections, all of which would have misled operators: CORS. The checklist told operators to set an allowlist "or leave unset — not *", which would block every cross-origin browser client, the mina-explorer included, with no server-side symptom. For a public read-only API over already-public data, CORS_ORIGIN=* is the correct setting rather than a lapse: CORS constrains browsers, not curl, so it is not an access control. Adds a section making the choice explicit and notes these controls arrive in 1.0.0 — on 0.0.x, CORS_ORIGIN defaults to '*', so the protections table describes a version most operators are not yet running. TRUST_PROXY. The doc described X-Forwarded-For as read "first hop", which is the behaviour removed in #185 as a rate-limit bypass. Documents the hop-count model and the deny-by-default reading instead. Read replicas. The README claimed the server "fans queries across" multiple PG_CONN hosts. It does not: postgres.js scopes hostIndex per Connection (src/connection.js:89), so every pooled connection starts at host[0] and only advances on failure — failover, not fan-out. As written it promised read scaling that adding replicas cannot deliver. Addresses review feedback on #186. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
|
Thanks @SanabriaRusso — fixed in CORS. You were right that the checklist was the dangerous part: On the "reads as current" problem — rather than a caveat naming in-flight PRs (which we'd have to remember to delete), I pinned it to a version: the table now says these controls arrive in 1.0.0, and that Introspection. Left the "keep it off" advice as-is: #193 lands in wave 1, well ahead of this, so Two things you didn't flag, found while in here:
|
What & why
Part of the production-readiness epic (#163). Closes #168.
The repo had no single place describing how to expose the API safely. This adds
docs/security.mddocumenting the intended posture, chosen for this service: a public, read-only GraphQL API meant to run behind a TLS-terminating gateway against a read-only Postgres role, with no application-level auth (per-caller gating, if needed, is an operator concern at the gateway — the app stays simple).Contents
X-Forwarded-Forfor per-client rate limiting, Postgres kept private (with a diagram).archive_api_roread-only role (SQL included).Linked from the README (new "Security & production deployment" section) and the setup guide's "Where to go next".
Note on sequencing
The "Built-in protections" table describes controls delivered by the sibling P0 PRs (#164 query-cost, #165 PG timeouts, #166 rate limiting, #167 CORS). This doc is best merged after / alongside those so every protection it references is present on
main. Cross-doc links only targetgetting-started.md#configuration, which already exists onmain, so the doc has no hard dependency on merge order.Testing
Docs-only.
npx prettier --debug-check .exits 0;npm run lintclean. Internal links verified against existing anchors.🤖 Generated with Claude Code