Secrets management (SOPS + Woodpecker)¶
Platform secrets are encrypted in git and decrypted only at deploy time into tmpfs (/run/infra-secrets) or process memory. CI/deploy credentials for apps stay in Woodpecker only — never on the VPS disk.
Threat model¶
| Risk | Mitigation |
|---|---|
| Leaked git clone | secrets/*.enc.env encrypted with age |
Casual cat /opt/devops/.env |
No persistent plain .env on VPS |
| Backups copying passwords | tmpfs excluded; plain .env removed after migration |
| Stolen Woodpecker DB | Per-repo secrets; rotate SOPS_AGE_KEY separately |
| Root on VPS | Cannot fully hide — docker inspect / /proc/*/environ still show runtime env |
Root access always wins at runtime. The goal is no long-lived plain files and encrypted at rest in git.
Where secrets live¶
| Secret type | Storage | Edit with |
|---|---|---|
| Woodpecker DB, OAuth, Traefik auth | secrets/vps-stack.enc.env |
sops edit |
| Plane ↔ Forgejo bridge tokens | secrets/plane-bridge.enc.env |
sops edit |
| Plane MCP OAuth | secrets/plane-mcp.enc.env |
sops edit |
| Per-app SFTP (reference only) | secrets/projects/<repo>.enc.env |
sops edit |
| App deploy (SFTP, Plane notify) | Woodpecker repo secrets | Woodpecker UI / onboard-project.sh |
| Decryption key (private) | secrets/.age/key.txt (local, gitignored) |
Never commit |
| Auto-deploy key | Woodpecker sops_age_key on infra/devops |
Copy private age line once |
First-time setup¶
# 1. Generate age key + encrypt templates (already done if secrets/*.enc.env exist)
bash scripts/secrets-init.sh
# 2. Install private key for editing (your PC only)
mkdir -p ~/.config/sops/age
cp secrets/.age/key.txt ~/.config/sops/age/keys.txt
chmod 600 ~/.config/sops/age/keys.txt
# 3. Woodpecker secrets on infra-devops repo
# sops_age_key = AGE-SECRET-KEY-1... line from secrets/.age/key.txt
# deploy_ssh_host = 85.215.32.166
# deploy_ssh_user = root
# deploy_ssh_key = private SSH key for deploy
Edit a secret¶
export SOPS_AGE_KEY_FILE=secrets/.age/key.txt # or use ~/.config/sops/age/keys.txt
sops edit secrets/plane-bridge.enc.env
git add secrets/plane-bridge.enc.env
git commit -m "chore: update bridge secrets"
git push # Woodpecker deploys if pipeline enabled
Deploy manually¶
bash scripts/sync-to-server.sh # from PC with SSH config (see load-server-env.sh)
ssh bioscan-vps "cd /opt/devops && SOPS_AGE_KEY='...' bash scripts/deploy-platform.sh"
deploy-platform.sh:
- Decrypts bridge keys to
/run/infra-secrets/(tmpfs) - Runs
docker compose up -dwith decrypted stack env (no.envfile on disk) - Starts bridge with tmpfs secret files
- Optionally starts MCP from tmpfs env file
- Starts docs nginx container
Remove plain files on VPS (after first successful deploy)¶
bash scripts/sync-to-server.sh
ssh bioscan-vps 'cd /opt/devops && SOPS_AGE_KEY=... bash scripts/deploy-platform.sh'
bash scripts/purge-plain-env-on-vps.sh
See also scripts/rotate-exposed-secrets.md if tokens were exposed in chat.
Onboard a new app¶
- Deploy creds → Woodpecker only (
onboard-project.shsetssftp_*,plane_api_key, etc.) - Optional encrypted reference →
sops encrypt local/projects/myapp.env > secrets/projects/myapp.enc.env
Backup rules¶
scripts/backup.sh archives Docker volumes only. It does not include:
/run/infra-secrets/opt/devops/.env(should not exist after migration)secrets/.age/key.txt(never on VPS)
Rotate age key (advanced)¶
age-keygen -o secrets/.age/key-new.txtsops updatekeys -y secrets/*.enc.env(with both keys in.sops.yamltemporarily)- Update Woodpecker
sops_age_key - Remove old key from
.sops.yaml
MCP note¶
Secret rotation and SOPS edit operations are not exposed via the infra-ops MCP in v1. Use this runbook manually.