Skip to content

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

  1. Decrypts bridge keys to /run/infra-secrets/ (tmpfs)
  2. Runs docker compose up -d with decrypted stack env (no .env file on disk)
  3. Starts bridge with tmpfs secret files
  4. Optionally starts MCP from tmpfs env file
  5. 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

  1. Deploy creds → Woodpecker only (onboard-project.sh sets sftp_*, plane_api_key, etc.)
  2. Optional encrypted referencesops 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)

  1. age-keygen -o secrets/.age/key-new.txt
  2. sops updatekeys -y secrets/*.enc.env (with both keys in .sops.yaml temporarily)
  3. Update Woodpecker sops_age_key
  4. 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.