Skip to content

ADR-009 — Move All Credentials to Vault

Date: 2026-04-10 Status: Proposed

Decision

All database passwords, API keys, user credentials, and other secrets must be stored in Vault and injected at deploy time. No credentials should be hardcoded in docker-compose files, config files, or playbooks.

Context

Several services have credentials hardcoded in docker-compose files or config files checked into the repo:

  • Database passwords in docker-compose.yml environment variables
  • API keys in application config files
  • Default passwords that were never rotated

This violates the principle that secrets belong in Vault. If the repo is compromised, all hardcoded credentials are exposed. It also makes rotation impossible without redeploying config files.

Audit scope

Files to scan

  1. services/*/docker-compose.yml — look for PASSWORD, SECRET, KEY, TOKEN, API_KEY in environment variables
  2. services/*/config.* — look for credentials in application configs
  3. ansible/playbooks/*.yml — ensure credentials come from Vault, not hardcoded vars

Known violations (to fix)

Service File Credential Current state Vault path
MediaManager /opt/mm/config/config.toml db_password, SMTP creds Hardcoded in config secret/mediamanager (db_password added)
MariaDB (all-might) docker-compose.yml MYSQL_ROOT_PASSWORD Needs check secret/mariadb
Paperless docker-compose.yml PAPERLESS_DBPASS Needs check secret/paperless
Synapse config Registration shared secret Needs check secret/synapse
N8N docker-compose.yml Encryption key Needs check secret/n8n
Open-WebUI docker-compose.yml WEBUI_SECRET_KEY Needs check secret/open-webui

Already correct

Service How credentials are managed
Karakeep Playbook fetches from secret/karakeep, writes .env
Databasement Playbook fetches from secret/databasement + secret/external-oauth2-proxies, writes .env
Forgejo Playbook fetches from secret/forgejo
Caddy No secrets in config
NetBird Playbook fetches from secret/netbird
VPN (Gluetun) Playbook fetches from secret/gluetun/*

Implementation plan

Phase 1: Audit

  1. Run grep -riE 'PASSWORD|SECRET|TOKEN|API_KEY' services/*/docker-compose.yml to find all hardcoded secrets
  2. Cross-reference with Vault secrets to identify gaps
  3. Create Vault entries for any missing credentials

Phase 2: Migrate

For each service with hardcoded credentials:

  1. Generate a strong random password (if using a default)
  2. Store in Vault at secret/<service>
  3. Update the playbook to fetch from Vault and write to .env or template the config
  4. Update docker-compose.yml to use env_file: .env instead of inline values
  5. Deploy and verify

Phase 3: Prevent regression

  1. Add a CI check (Forgejo Actions) that scans for common credential patterns in committed files
  2. Document the pattern in CLAUDE.md: "Secrets belong in Vault, injected by playbooks into .env files"

Trade-offs

Accepted

  • Vault dependency for all deploys: If Vault is down, no service can be (re)deployed. Mitigated by PBS snapshots of LXC 106 (Vault) and Vault raft snapshots via Backrest.
  • More complex playbooks: Each playbook needs Vault fetch tasks. This is already the pattern for Karakeep/Databasement — standardize it.

Rejected

  • SOPS/age encrypted files in repo: Simpler than Vault but doesn't support rotation, access control, or audit logging. Vault is already deployed and working.
  • Docker secrets: Only works with Docker Swarm, not standalone Docker Compose.