thermograph/infra/deploy/secrets/README.md
Emi Griffith ae1d9bb534 Subtree-merge thermograph-infra (origin/main) into infra/
git-subtree-dir: infra
git-subtree-mainline: d6df04eab2
git-subtree-split: 99b4b3f78d
2026-07-22 22:01:11 -07:00

4.9 KiB

Secrets — the git-native vault (SOPS + age)

The single source of truth for Thermograph's secrets is the set of SOPS-encrypted YAML files in this directory. They're committed to the repo encrypted (values only — keys stay readable so diffs are meaningful) and rendered into /etc/thermograph.env at deploy time by deploy/render-secrets.sh, which the prod/beta deploy.sh invokes.

Cycling a key is: edit → commit → deploy. No SSHing in to hand-edit a root file, no per-host duplication.

Files

File Holds Encrypted?
common.yaml Secrets shared by every host — Discord tokens/secret, VAPID keypair, THERMOGRAPH_AUTH_SECRET, metrics token, indexnow key, SMTP creds
prod.yaml Prod-only overrides — POSTGRES_PASSWORD, REGISTRY_TOKEN
beta.yaml Beta-only overrides
example.yaml Format reference / CI fixture (fake values)
../../.sops.yaml Which age recipient files are encrypted to (plaintext config)

At deploy the renderer concatenates common.yaml then <env>.yaml, so a host value wins (env_file / source take the last occurrence of a duplicate key). <env> comes from /etc/thermograph/secrets-env on the box (prod/beta/dev). dev has no real secrets and is intentionally not wired in — see deploy-dev.sh.

The age key (the one thing that matters)

  • The private key is the single recovery root. Lose it and every secret here is unrecoverable; leak it and every secret is compromised.
  • It is never in the repo. It lives at:
    • ~/.config/sops/age/keys.txt on the operator's machine (for editing), and
    • /etc/thermograph/age.key (0400) on each host that renders at deploy time.
  • Back it up in the password manager. The public recipient is in .sops.yaml.

Prerequisites (once per machine)

Install sops + age (single static binaries). On a host, use deploy/provision-secrets.sh; on your laptop:

# see the pinned URLs in provision-secrets.sh, or:
go install github.com/getsops/sops/v3/cmd/sops@latest   # if you have Go

Everyday: rotate a key

sops deploy/secrets/common.yaml        # opens DECRYPTED in $EDITOR; re-encrypts on save
git commit -am "rotate <thing>" && git push forgejo
# beta auto-deploys on push to main. prod (no CI) — one command:
ssh agent@169.58.46.181 'cd /opt/thermograph && git pull && deploy/deploy.sh'

A host-specific value (e.g. POSTGRES_PASSWORD) is the same, editing prod.yaml / beta.yaml instead. POSTGRES_PASSWORD is special: the database only reads it on a fresh volume, so also ALTER ROLE thermograph PASSWORD '…' inside the running Postgres to match — the env change alone won't re-key an initialized DB.

Add a new secret

  1. sops deploy/secrets/common.yaml, add THERMOGRAPH_NEW_THING: value.
  2. Add the app reader (os.environ.get("THERMOGRAPH_NEW_THING")).
  3. Commit + deploy. It flows into /etc/thermograph.env automatically.

Rotate the age identity itself (re-key)

age-keygen -o new.key                              # new identity
# add the new PUBLIC recipient alongside the old in .sops.yaml, then:
sops updatekeys deploy/secrets/*.yaml              # re-encrypt to BOTH keys
# distribute new.key to /etc/thermograph/age.key on every host + ~/.config/sops/age,
# deploy once so every host can decrypt with the new key, then remove the OLD
# recipient from .sops.yaml and:
sops updatekeys deploy/secrets/*.yaml              # drop the old key
git commit -am "rotate age identity" && push + deploy

First-time bootstrap (seed from the live boxes)

Order matters — values must match the live env exactly so AUTH_SECRET / VAPID / POSTGRES_PASSWORD don't rotate unintentionally:

  1. Install sops+age and drop /etc/thermograph/age.key (0400) + /etc/thermograph/secrets-env on prod & beta (provision-secrets.sh).
  2. Seed each host's file from its live env with the helper (pulls over SSH, encrypts, and verifies the render round-trips — run it on your own machine, it reads live secrets):
    deploy/secrets/seed-from-live.sh prod agent@169.58.46.181
    deploy/secrets/seed-from-live.sh beta agent@75.119.132.91
    
    That writes exact per-host copies (prod.yaml/beta.yaml); factor shared values into common.yaml later. Commit.
  3. Dry-run: on the box, render to a temp file and diff against the live /etc/thermograph.env — must be byte-identical before cutover.
  4. Merge the deploy.sh wiring; run one deploy; confirm the app is healthy.

Safety

  • CI (.forgejo/workflows/secrets-guard.yml) fails if any *.yaml here is committed unencrypted.
  • Rendering is presence-detected: a host without the age key + marker keeps its existing /etc/thermograph.env, so nothing breaks before a host is migrated.
  • Rendering happens on the target box, so CI/runners never see the age key or the plaintext.