This document describes the intended security posture of the Archive Node API and how to deploy it safely on a public network. It complements the setup guide; read that first for installation and the full configuration reference.
The Archive Node API is a public, read-only GraphQL service over an existing archive-node Postgres database. It exposes already-public on-chain data (blocks, events, actions, transactions) and never writes to the database or the chain.
Consequences of that model:
- No application-level authentication. The API is meant to be openly queryable, the same way a block explorer's read API is. Access control, if you need it, is enforced at the gateway in front of the service (see below) — not in the app.
- The data is not secret; availability is the asset to protect. The main threat is abuse that degrades the service or the backing Postgres for everyone. The hardening below is aimed at that.
If you require per-caller authentication or quotas, terminate it at the gateway (API keys, JWT, or mTLS). The application is intentionally kept simple and unauthenticated; gating is an operator concern.
Run the API behind a TLS-terminating reverse proxy or load balancer. The
application itself speaks plain HTTP on PORT and does not terminate TLS.
┌─────────────────────────┐
client ───▶│ TLS gateway / LB │ (HTTPS, X-Forwarded-For,
(HTTPS) │ nginx / Envoy / ALB │ request-size limits, optional auth)
└────────────┬────────────┘
│ HTTP (private network)
┌────────────▼────────────┐
│ Archive Node API │ (this service, :8080)
└────────────┬────────────┘
│ TCP (private network)
┌────────────▼────────────┐
│ Postgres (archive DB) │ read replicas, not publicly reachable
└─────────────────────────┘
Requirements:
- TLS at the gateway. Never expose the plain-HTTP app port to the internet.
- Set
X-Forwarded-ForandTRUST_PROXYtogether (from 1.0.0). A gateway should appendX-Forwarded-For, and the API derives the rate-limit client from that header only as far asTRUST_PROXYallows: it names how many proxy hops sit in front of the API, and the client is read that many entries from the right of the header — the part your own proxies appended.TRUST_PROXYhas no default; while it is unset, rate limiting is disabled with a startup warning. UseTRUST_PROXY=0only for a directly exposed server. Behind a gateway, set the real hop count for your topology. A generic single reverse proxy is often1; a GCP external Application Load Balancer commonly needs2because it appends twoX-Forwarded-Forentries. Too low collapses clients onto a proxy address; too high can trust caller-prepended entries. - Keep Postgres private. The database must not be reachable from the public internet — only from the API instances.
The service ships with abuse controls that are safe by default and tunable via the configuration:
| Protection | Default | Purpose |
|---|---|---|
| Per-IP rate limiting | on once TRUST_PROXY is set |
Bounds request volume per client; disabled with a startup warning while TRUST_PROXY is unset |
| GraphQL query-cost limits (depth / aliases / tokens / cost) | on | Rejects expensive/abusive query shapes before execution |
| Postgres statement timeout & pool limits | on | Caps how long/much a single query can consume |
| CORS | same-origin only | Cross-origin browser access is opt-in — see the caveat below before locking it down |
| Introspection | off | Schema introspection disabled unless explicitly enabled |
| Field-suggestion blocking | on | Hides Did you mean ...? suggestions while preserving GraphQL validation text |
These controls arrive in 1.0.0. On
0.0.xreleases they are absent or default-open, or have older env parsing — notablyCORS_ORIGINdefaults to*there, so cross-origin access is wide open rather than opt-in. Introspection disabling already exists on0.0.x, but any non-emptyENABLE_INTROSPECTIONvalue, includingfalse, enables it. Check your running version before relying on any row above.
Tune these to your traffic; see the configuration table for the exact environment variables and defaults.
Cross-origin browser clients cannot reach this API unless their web origin is
allowlisted in CORS_ORIGIN — and when they fail, they fail silently from the
server's point of view: the browser blocks the response and the server logs stay
clean. This catches people out, so decide deliberately:
- A genuinely public read API that any browser may call — including the
mina-explorer and third-party
dashboards — wants
CORS_ORIGIN=*. That is the correct setting here, not a lapse in hardening: the data is already public, and CORS is not an access control (it constrains browsers, notcurlor a server-side client). - A deployment with a known, fixed set of front-ends wants those origins listed explicitly. This only limits which browser pages may read responses; it does not restrict anyone else.
The API only ever issues SELECTs. Give it a read-only Postgres role rather
than a superuser or the archive ingest user:
-- one-time setup on the archive database
CREATE ROLE archive_api_ro LOGIN PASSWORD 'change-me';
GRANT CONNECT ON DATABASE archive TO archive_api_ro;
GRANT USAGE ON SCHEMA public TO archive_api_ro;
GRANT SELECT ON ALL TABLES IN SCHEMA public TO archive_api_ro;
-- so the role can also read tables added by future archive migrations
ALTER DEFAULT PRIVILEGES IN SCHEMA public GRANT SELECT ON TABLES TO archive_api_ro;Then point PG_CONN at archive_api_ro. Even in the event of a query-layer bug,
the credentials cannot modify or delete data.
- Secrets: pass
PG_CONN(and any gateway secrets) via environment / a secret manager, never bake them into the image. Prefer SSL to Postgres (?sslmode=require) when the DB is on a managed provider. - Keep
ENABLE_GRAPHIQL/ENABLE_INTROSPECTIONoff in production unless you intentionally want a public playground. - Updates: track and apply dependency and base-image security updates (supply-chain scanning is part of the production-readiness work).
- TLS terminated at a gateway; plain-HTTP app port not publicly exposed
-
TRUST_PROXYexplicitly set:0only for direct exposure, or the real hop count behind a gateway; rate limiting is disabled until this is set - Gateway sets
X-Forwarded-For - Postgres reachable only from the API, not the public internet
- API uses a read-only Postgres role
- Rate-limit and query-cost limits reviewed for your expected traffic
-
CORS_ORIGINmatches your clients:*for a public API any browser may call, or an explicit allowlist if your front-ends are known and fixed — leaving it unset blocks all cross-origin browser clients -
ENABLE_GRAPHIQLandENABLE_INTROSPECTIONoff (unless intentionally public) — on0.0.x, leave them unset; any non-empty value, includingfalse, enables introspection - Secrets injected via env / secret manager; SSL to Postgres where applicable
This document covers deploying this service securely. It does not cover securing the upstream Mina archive node or its Postgres ingest pipeline. Broader production-readiness work (observability, readiness probes, supply-chain scanning, runbooks) is tracked in the production-readiness epic.