Skip to Content
Deneva MCPComponentsSecrets Loader

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:

ModePathHow 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:

NamePurpose
CREDENTIAL_KEKKey-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_SECRETHMAC key for hashing API keys.
DB_PASSWORDPostgres password for the mcp_app runtime role.
INNGEST_SIGNING_KEYVerifies inbound Inngest webhook signatures (Phase 4).
GOOGLE_CLIENT_SECRETOAuth 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_TOKENGoogle 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.sh

Idempotent: 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-mcp

Why not .env?

  • .env files 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.env is 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.