Skip to content

Secrets Management

  • Never commit secrets — no keys, passwords, or tokens in source, config files, commit messages, logs, or docs.
  • Never log secrets — not even partially.
  • Local development uses .env files (gitignored).
  • Production uses Docker secrets mounted at /run/secrets/<name>.

SecretService (common/services/secret.service.ts) is the single access point:

// Docker secret file first, env var as fallback (dev mode)
const key = this.secrets.get('backup_encryption_key', 'BACKUP_ENCRYPTION_KEY');
// Or fail closed:
const jwt = this.secrets.getOrThrow('jwt_access_secret');
  • get(name, fallbackEnv?) → cached value or undefined
  • getOrThrow(name, fallbackEnv?) → throws when missing
  • In containers, entrypoint.sh reads /run/secrets/* and composes derived values (e.g. DATABASE_URL from host/user/password parts).
Secret Purpose
db_password PostgreSQL authentication
redis_password Redis authentication
jwt_access_secret / jwt_refresh_secret JWT signing
mistral_api_key Mistral Vision API
smb_credentials_key AES-256-GCM for stored SMB credentials
webhook_encryption_key AES-256-GCM for webhook secrets
vapid_public_key / vapid_private_key Web Push
tool_provisioning_secret M2M tenant provisioning
backup_encryption_key AES-256-CBC backup archive encryption

Credentials have a single purpose each. Example: the database password authenticates pg_dump; backup archives are encrypted with the separate backup_encryption_key — so rotating or compromising one credential does not cascade into the other.

  1. Add a mapping row in docker/secrets-mapping.env (file name → source key + generation flags).
  2. Add the secret to the compose service(s) that need it.
  3. Read it in code via SecretService — never via process.env directly when a secret file may exist.
  4. Document it in .env.example (name + purpose, never a real value).