CancerIndex — Security
Scope: the public API (apps/api), the worker (workers/), the web app and the database on the
MacLustr node (spec §160-163).
Threat model in one paragraph
CancerIndex is a read-mostly public service over open scientific data. The valuable assets are data integrity (no silent corruption of canonical or derived data), availability, upstream credentials (NCBI, SEER), and the operator surface (admin endpoints, database). There is no end-user PII beyond API-key owner e-mails.
Controls
Network and exposure
- Only the web app (port 8250) is exposed through the ngrok tunnel for
www.cancerindex.io. The API listens on127.0.0.1:8251and is reached through the web app's/api/v1/*proxy; the worker has no listener; PostgreSQL listens on localhost only. - CORS allows browser reads from any origin (public data), exposes only rate-limit/request-id
headers, methods
GET, POST, OPTIONS. - The API sets
trustProxy: truebecause it sits behind the web proxy and ngrok; rate limiting keys on the forwarded client IP.
Read-only public API
- All
/v1public endpoints areGETand run onlySELECTstatements; the database role used in production should be granted read-only privileges for the API process where possible. - Every query parameter is validated by zod (types, enums,
limit ≤ 200); all SQL uses parameterised queries (sqltemplate) — no string interpolation of user input. - Numeric database ids are never accepted as public references (
classifyRef), so enumeration of internal ids is not possible.
Rate limiting and API keys
- Anonymous: 60 requests/minute per IP; keyed:
api_keys.rate_limit_per_minute.X-RateLimit-*headers are always sent; 429 includesretry-after. - API keys are random 192-bit values (
cix_+ base64url); only the sha256 hash and a 12-character prefix are stored. Keys are shown once at creation (apps/api/scripts/create-key.ts), can be deactivated (active = false) and recordlast_used_at(throttled write). - An invalid key returns 401 rather than silently falling back to anonymous limits.
Admin surface
/v1/admin/*requires thex-admin-tokenheader; comparison is constant-time. WhenADMIN_TOKENis unset or left at the.env.exampleplaceholder, admin endpoints answer 503 and are effectively disabled.- Admin mutations never run ingestion in the API process: they enqueue pg-boss jobs. Every
mutation writes an
audit_logrow (actor,action,before,after,reason). - The admin token must be rotated whenever an operator leaves or the value is exposed; it is
injected by
mldfrom the manifest placeholder{{ADMIN_TOKEN}}, never committed.
Secrets and configuration
.envis git-ignored;.env.examplecontains no real values. Production values live in the mld manifestenv_overrides/ process env on the node, or in~/.claude/.env-style operator files that never enter the repository.- Upstream credentials (
NCBI_API_KEY,SEER_API_KEY) are only read by connectors; a missing credential gates the connector (awaiting_credentials) instead of failing loudly with the value in logs. - Logs are structured (pino) and must not include authorization headers or keys; Fastify's default request serialiser logs method, URL and remote address only.
Data integrity
- Connectors are idempotent (payload hash), restartable (cursor), rate-limited and guarded against
destructive updates (anomaly guard: never mass-mark
source_missingon a shrunken response). - Raw payloads are retained in the data lake so any canonical value can be re-derived and audited
(
traceValue). - Schema changes go through Drizzle migrations reviewed by the integrator; no manual production mutation.
- Interrupted runs are safe: SIGTERM/SIGINT persist the cursor and mark the run
aborted; the raw lake is flushed so referenced payloads stay readable; a killed run resumes from its last checkpoint (docs/connectors/README.md). A shrunken upstream response trips the anomaly guard (ingest_runs.anomaly, critical alert) instead of propagating deletions. - Operational alerts (
system_alerts) carry connector ids, messages and small JSON details only — never credentials or request headers.GET /v1/admin/alertsis behind the admin token.
Backups and recovery (§172)
- Daily
pg_dump -Fc(PM2cancerindex-backup, 05:20 UTC) to~/apps/cancerindex/backups, verified withpg_restore --list, 14 daily + 8 weekly retained, log inlogs/backup.log. Dumps contain no end-user PII beyond API-key owner e-mails and only sha256 hashes of API keys, but they do contain the full canonical/derived data: keep the backups directory readable by the service user only and mirror it (withdata/raw) to the NAS over the LAN, never to a public bucket. deploy/restore.shalways restores into a newcancerindex_restore_<ts>database; promoting a restore is an explicit operator action (rename databases after stopping PM2). Test a restore after every schema migration.pnpm cix doctoris the post-incident checklist: environment, database, pending migrations, connector freshness and open alerts in one exit code.
Dependencies and runtime
- Node ≥ 22, pnpm with a committed lockfile (
--frozen-lockfilein deployment). - Only maintained, widely used packages: Fastify 5 and official
@fastify/*plugins, zod 4, drizzle-orm, postgres, pg-boss 12, pino. Runpnpm auditbefore releases. - Processes run under PM2 with
max_memory_restart; the worker stops gracefully onSIGTERM(finishes the active job or lets pg-boss retry after expiry).
Content and liability
- The platform states on every page and in the OpenAPI description that it is not a physician, not a diagnostic tool and offers no treatment recommendations. Survival statistics are labelled as population statistics.
- Source licenses and attribution are surfaced in every API response (
sources) and on/sources; connectors with unresolved licensing (review,blocked) never ingest.
Reporting
Report vulnerabilities privately to the maintainer (see repository README). Please include the
x-request-id of any suspicious response and do not test against production data volumes.