Files
project-charlie/docs/secrets.md
T
2026-05-06 18:10:33 +02:00

3.2 KiB

Secrets

SOPS-in-repo with one age key per host. Encrypted files live next to docker-compose.yml in each Charlie/<stack> repo. The reusable deploy-stack.yml workflow SSHes into the target host as sys-gitea-runner and runs sops --decrypt there using /etc/charlie/age.key.

Operational recipe: onboarding.md → Host age key (SOPS). Caller-facing reference: automation.md.

Goal

Every host can:

  1. Obtain only the secrets it's allowed to decrypt.
  2. Materialize them where compose can consume them (typically .env).
  3. Do this non-interactively from a Gitea Actions runner.

Constraints

  • Source of truth lives with the stack. A secret for mediacompose belongs in Charlie/mediacompose, not in a separate vault. Blast radius is bounded by repo access.
  • Per-machine decryption. A compromised host leaks only the secrets in repos whose .sops.yaml lists that host's key.
  • Self-identifying host. hostname -s | tr '[:upper:]' '[:lower:]' everywhere.
  • Versioning is git. Rotation = commit + (when needed) sops updatekeys + push.

Layout in a stack repo

Charlie/<stack>/
├── docker-compose.yml
├── .env.example                # tracked, redacted
├── secrets/
│   └── env.sops.yaml           # encrypted; decrypts to .env
├── .sops.yaml                  # creation rules: which recipients can decrypt
└── .gitea/workflows/deploy.yaml  # calls deploy-stack.yml

For shared stacks, .sops.yaml lists every host that runs the stack.

Keys

  • One age key per host, at /etc/charlie/age.key (0640 root:charlie-deploy). Public keys in hosts.md.
  • One admin key held by alex on the admin laptop, listed in every repo's .sops.yaml so secrets stay decryptable from off-host. Backup as a vaultwarden Secure Note (Charlie SOPS admin age key).
  • Per-host, not per-stack. Container isolation already provides the per-stack property — /etc/charlie/age.key isn't bind-mounted into containers.

Rotation

# In the stack repo on the admin laptop.
tmp=$(mktemp) && chmod 600 "$tmp"
printf 'KEY=new-value\n' > "$tmp"
sops --encrypt --input-type dotenv --output-type yaml \
     --filename-override secrets/env.sops.yaml "$tmp" > secrets/env.sops.yaml
shred -u "$tmp"
git commit -am 'Rotate <KEY>'
git push   # triggers redeploy

For per-host secret files, repeat per host. Adding/removing recipients (e.g. onboarding mohg): edit .sops.yaml, then sops updatekeys secrets/env.sops.yaml.

What this design intentionally doesn't do

  • Manage secrets for stacks that aren't runner-deployed.
  • Replace vaultwarden's human-facing UX. Vaultwarden stays for browser autofill + secure notes (including the admin age key backup).
  • Solve secrets for Charlie/gitea auto-deploy — gitea-on-melina is deployed manually (it bootstraps the runners that would deploy it). SOPS is offline crypto though, so its secrets can live encrypted in the repo and a manual sops -d | docker compose up -d works.