thermograph/infra/deploy
Emi Griffith 6bd07d33cb openbao: add a dormant OpenBao backend alongside SOPS
Phase 1 of moving the secret store to OpenBao. Nothing is cut over:
TG_SECRETS_BACKEND defaults to sops for all three environments, so SOPS
remains authoritative until an environment is flipped in a reviewed PR.

The reason for the migration is one specific failure class. infra/CLAUDE.md
requires that vps1 never hold prod credentials, but that rule is currently
enforced by a caller-side shell flag (THERMOGRAPH_SECRETS_SKIP_COMMON) at
three call sites. A fourth call site, or one by-hand render, writes both S3
keypairs, the VAPID private key, REGISTRY_TOKEN and the IndexNow/metrics
tokens onto the Forgejo CI-runner box and exits 0. policies/tg-host-dev.hcl
makes that a 403 instead: dev's identity cannot read the common path at all.

Shape:
- vps2 only, native systemd (not a container: a Swarm service is circular,
  and a plain docker run is one `prune -a` from gone), raft storage (2.6.0
  deprecated the file backend), static auto-unseal so a reboot cannot block
  deploys, mesh-only self-signed TLS (not ACME — that would put DNS in the
  boot chain of the secret store).
- AppRole per environment, CIDR-bound, 5-minute tokens. Each environment's
  secret-id is owned by its own deploy user, which is a boundary that does
  not exist today since prod and beta both reach the same root-owned age key.
- Render still produces the same raw unquoted dotenv: Centralis compares
  /etc/thermograph.env textually and the Swarm entrypoint parses it by line.

The backend dispatch in render-secrets.sh returns early rather than
refactoring the shared write path, so the SOPS path stays byte-identical.
Verified by rendering dev (12 keys) and prod (32 keys) through it, matching
the live hosts. The duplicated write path is noted for consolidation once
prod has been on OpenBao for a release cycle.

The OpenBao renderer rejects values containing a newline or a shell
metacharacter. /etc/thermograph.env is sourced by bash in six places, so
such a value is a command-execution path on the deploy host; the SOPS path
has always had this hazard and avoids it only because every current value
happens to be alphanumeric.

Also records that the age keypair is NOT retired by this migration: it is
the backup encryption key for every off-box dump (ops-cron.yml:142,197,
30-day retention), so deleting it with the vault files would silently
destroy a month of database recoverability.

verify-parity.sh is the cutover gate — it renders both backends and diffs
them, reporting key names and counts only, never values.
2026-07-29 21:35:20 -07:00
..
backup infra: split the estate into vps1/vps2 — beta joins prod, dev gets a home (#103) 2026-07-26 06:56:38 +00:00
db db: raise max_connections to 200, cap work_mem at 64MB, trim async pool overflow (#124) 2026-07-26 19:48:50 +00:00
forgejo infra: split the estate into vps1/vps2 — beta joins prod, dev gets a home (#103) 2026-07-26 06:56:38 +00:00
migrations Subtree-merge thermograph-infra (origin/main) into infra/ 2026-07-22 22:01:11 -07:00
openmeteo infra: split the estate into vps1/vps2 — beta joins prod, dev gets a home (#103) 2026-07-26 06:56:38 +00:00
secrets infra: split the estate into vps1/vps2 — beta joins prod, dev gets a home (#103) 2026-07-26 06:56:38 +00:00
stack infra: split the estate into vps1/vps2 — beta joins prod, dev gets a home (#103) 2026-07-26 06:56:38 +00:00
swarm infra: split the estate into vps1/vps2 — beta joins prod, dev gets a home (#103) 2026-07-26 06:56:38 +00:00
twa Subtree-merge thermograph-infra (origin/main) into infra/ 2026-07-22 22:01:11 -07:00
Caddyfile.vps1 dev: leave /healthz outside the basic-auth boundary (#113) 2026-07-26 08:28:18 +00:00
Caddyfile.vps2 infra: split the estate into vps1/vps2 — beta joins prod, dev gets a home (#103) 2026-07-26 06:56:38 +00:00
deploy-dev.sh infra: split the estate into vps1/vps2 — beta joins prod, dev gets a home (#103) 2026-07-26 06:56:38 +00:00
deploy.sh deploy.sh: honour TG_POST_DEPLOY on the compose path too (#109) 2026-07-26 07:28:43 +00:00
env-topology.sh openbao: add a dormant OpenBao backend alongside SOPS 2026-07-29 21:35:20 -07:00
migrate-db.py Subtree-merge thermograph-infra (origin/main) into infra/ 2026-07-22 22:01:11 -07:00
POSTGRES-MIGRATION.md Subtree-merge thermograph-infra (origin/main) into infra/ 2026-07-22 22:01:11 -07:00
provision-agent-access.sh Subtree-merge thermograph-infra (origin/main) into infra/ 2026-07-22 22:01:11 -07:00
provision-dev.sh dev: serve at dev.thermograph.org behind basic auth, bound to loopback (#111) 2026-07-26 07:49:43 +00:00
provision-mail-supervision.sh observability: add the estate's first alerting; supervise Postfix 2026-07-24 13:19:19 -07:00
provision-mail.sh infra: split the estate into vps1/vps2 — beta joins prod, dev gets a home (#103) 2026-07-26 06:56:38 +00:00
provision-secrets.sh infra: split the estate into vps1/vps2 — beta joins prod, dev gets a home (#103) 2026-07-26 06:56:38 +00:00
render-secrets-openbao.sh openbao: add a dormant OpenBao backend alongside SOPS 2026-07-29 21:35:20 -07:00
render-secrets.sh openbao: add a dormant OpenBao backend alongside SOPS 2026-07-29 21:35:20 -07:00
RUNBOOK-vps1-vps2-cutover.md dev: serve at dev.thermograph.org behind basic auth, bound to loopback (#111) 2026-07-26 07:49:43 +00:00
thermograph-dev.service Subtree-merge thermograph-infra (origin/main) into infra/ 2026-07-22 22:01:11 -07:00
thermograph.env.example accounts: sign in with Google, on a shared provider-agnostic OAuth engine (#122) 2026-07-27 00:56:43 +00:00
thermograph.service Subtree-merge thermograph-infra (origin/main) into infra/ 2026-07-22 22:01:11 -07:00