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.ymlenvironment 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
services/*/docker-compose.yml— look forPASSWORD,SECRET,KEY,TOKEN,API_KEYin environment variablesservices/*/config.*— look for credentials in application configsansible/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
- Run
grep -riE 'PASSWORD|SECRET|TOKEN|API_KEY' services/*/docker-compose.ymlto find all hardcoded secrets - Cross-reference with Vault secrets to identify gaps
- Create Vault entries for any missing credentials
Phase 2: Migrate
For each service with hardcoded credentials:
- Generate a strong random password (if using a default)
- Store in Vault at
secret/<service> - Update the playbook to fetch from Vault and write to
.envor template the config - Update
docker-compose.ymlto useenv_file: .envinstead of inline values - Deploy and verify
Phase 3: Prevent regression
- Add a CI check (Forgejo Actions) that scans for common credential patterns in committed files
- Document the pattern in
CLAUDE.md: "Secrets belong in Vault, injected by playbooks into.envfiles"
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.