Secrets loader
Source: src/security/secrets.loader.ts
Single source of truth for all sensitive material. Nothing in the codebase reads secrets from process.env.
Two backends
Selected by NODE_ENV:
| Mode | Path | How files arrive |
|---|---|---|
production | /run/credentials/<unit>/<NAME> | systemd-creds encrypt writes encrypted blobs to /etc/deneva-mcp/creds/; systemd decrypts them into a tmpfs at service start. |
| anything else | ./secrets/<NAME> | Generated by scripts/dev-secrets.sh. Mode 600, gitignored. |
Required secrets
The set is closed and small:
| Name | Purpose |
|---|---|
CREDENTIAL_KEK | Key-encryption key. Used by credentials.service.ts to wrap per-tenant DEKs. The on-disk file is base64-encoded 32 bytes (openssl rand -base64 32); credentials.service.ts base64-decodes at module load and validates the length. |
API_KEY_HMAC_SECRET | HMAC key for hashing API keys. |
DB_PASSWORD | Postgres password for the mcp_app runtime role. |
INNGEST_SIGNING_KEY | Verifies inbound Inngest webhook signatures (Phase 4). |
GOOGLE_CLIENT_SECRET | OAuth client secret from Google Cloud Console (Phase 2 PR-3). Loaded by adapters/google/auth.ts at module load; used for both code exchange and refresh. |
GOOGLE_DEVELOPER_TOKEN | Google Ads API developer token from ads.google.com/aw/apicenter (Phase 2 PR-3). Required at startup so the process refuses to boot without it, but only actually consumed by the GAQL layer (PR-5). |
DB_ADMIN_PASSWORD is consumed only by drizzle.config.ts and seed-tenant.mjs, not by the runtime — it is intentionally NOT in the required list.
Behaviour
- First read fetches from disk; subsequent reads come from an in-process Map. Cheap to call repeatedly.
verifyAllSecretsLoadable()is invoked once at startup, before the HTTP port opens. If any required secret cannot be read, the process exits non-zero immediately — better to fail fast than to discover a missing file at first request.
Dev bootstrap
bash scripts/dev-secrets.shIdempotent: if a file already exists it is left untouched. The directory is mode 700 and listed in .gitignore.
Production bootstrap
See docs/setup-ubuntu.md §“Encrypt secrets” for the full procedure. Summary:
sudo bash scripts/encrypt-prod-secrets.sh
sudo systemctl daemon-reload && sudo systemctl restart deneva-mcpWhy not .env?
.envfiles end up committed by accident.secrets/is gitignored and mode 700; even if someone tries to commit, the contents tend to look obviously secret.process.envis visible in any debugger snapshot, error log, or core dump. Reading from a tmpfs file at the precise moment we need a secret keeps the value out of those vectors.- systemd-creds binds the encrypted blob to the host’s TPM (or host key) — moving the disk to another machine yields unreadable bytes.
Tests
The loader is exercised indirectly by every integration test: audit-log.service, api-key.service, etc. all import secrets at module load. If dev-secrets.sh was not run, all the integration tests fail with a clear “ENOENT” pointing at the missing file.