openbao: derive the AppRole credential path per environment
All checks were successful
PR build (required check) / build-backend (pull_request) Has been skipped
PR build (required check) / build-frontend (pull_request) Has been skipped
PR build (required check) / validate-observability (pull_request) Has been skipped
PR build (required check) / changes (pull_request) Successful in 7s
PR build (required check) / gate (pull_request) Successful in 1s
secrets-guard / encrypted (pull_request) Successful in 9s
shell-lint / shellcheck (pull_request) Successful in 8s

render-secrets-openbao.sh resolved the AppRole file to a single host-wide default,
/etc/thermograph/openbao-approle — which is prod's. On vps2 that made beta's render
authenticate as tg-prod, and tg-host-prod.hcl denies thermograph/data/env/beta, so
beta could never render from OpenBao. It surfaces as `cannot read
thermograph/env/beta`: the policy working correctly against the wrong identity.

env-topology.sh exists precisely because vps2 runs two environments and one
host-wide marker cannot answer "which environment is this". The AppRole path is the
same question and had been given the same host-wide answer. It is now derived per
environment alongside host, checkout, branch, ports and DB role, and the renderer
resolves THERMOGRAPH_BAO_APPROLE, then TG_BAO_APPROLE, then the conventional path —
the same precedence render-secrets.sh:44 already uses for the backend selector.

Only beta differs. prod and dev keep the conventional path; dev is alone on vps1 so
there is no collision there.

Beta parity passes when the path is supplied by hand (24 keys = 8 + 16 shared);
this makes it automatic. Derivation confirmed by sourcing env-topology.sh for all
three environments.
This commit is contained in:
Emi Griffith 2026-07-30 19:49:28 -07:00
parent 702bcd920c
commit 5001269b37
4 changed files with 28 additions and 10 deletions

View file

@ -76,6 +76,16 @@ thermograph_topology() {
# for the reason that marker was demoted to a fallback in the first place: vps2 runs # for the reason that marker was demoted to a fallback in the first place: vps2 runs
# prod and beta side by side, and one host-wide file cannot name two backends. # prod and beta side by side, and one host-wide file cannot name two backends.
TG_SECRETS_BACKEND=sops TG_SECRETS_BACKEND=sops
# Which AppRole credential file render-secrets-openbao.sh authenticates with. Same
# reasoning as TG_SECRETS_BACKEND above, and the same trap: vps2 renders TWO
# environments, so a single host-wide default cannot serve both. prod and dev each
# get the conventional path (dev is alone on vps1, so there is no collision there);
# beta is the one that must differ, because it shares a filesystem with prod.
#
# Without this, beta's render falls back to prod's credentials and tg-host-prod.hcl
# correctly denies thermograph/data/env/beta — a 403 at deploy time on the box that
# runs prod.
TG_BAO_APPROLE=/etc/thermograph/openbao-approle
case "$env_name" in case "$env_name" in
prod) prod)
@ -116,6 +126,10 @@ thermograph_topology() {
# therefore prod's blast radius too (see .claude/hooks/prod-guard.sh). # therefore prod's blast radius too (see .claude/hooks/prod-guard.sh).
TG_SSH_HOST=169.58.46.181 TG_SSH_HOST=169.58.46.181
TG_SSH_TARGET=agent@169.58.46.181 TG_SSH_TARGET=agent@169.58.46.181
# The one environment that cannot use the default AppRole path: it shares a
# filesystem with prod, so it needs its own credential file to authenticate as
# tg-beta rather than tg-prod. bootstrap-policies.sh installs it here.
TG_BAO_APPROLE=/etc/thermograph/openbao-approle-beta
# A SECOND checkout on the same box. Separate from prod's so the two can # A SECOND checkout on the same box. Separate from prod's so the two can
# sit on different commits of this repo, and so `git reset --hard` in one # sit on different commits of this repo, and so `git reset --hard` in one
# deploy can never yank the tree out from under the other's running # deploy can never yank the tree out from under the other's running
@ -228,7 +242,7 @@ thermograph_topology() {
export TG_LB_HTTP_PORT TG_LB_FE_PORT TG_DB_NAME TG_DB_USER export TG_LB_HTTP_PORT TG_LB_FE_PORT TG_DB_NAME TG_DB_USER
export TG_TAGS_FILE TG_LOCK_FILE TG_BIND_ADDR TG_SKIP_COMMON export TG_TAGS_FILE TG_LOCK_FILE TG_BIND_ADDR TG_SKIP_COMMON
export TG_SVC_PREFIX TG_DATA_NETWORK TG_DB_SERVICE TG_POST_DEPLOY export TG_SVC_PREFIX TG_DATA_NETWORK TG_DB_SERVICE TG_POST_DEPLOY
export TG_SECRETS_BACKEND export TG_SECRETS_BACKEND TG_BAO_APPROLE
export TG_SSH_HOST TG_SSH_TARGET export TG_SSH_HOST TG_SSH_TARGET
} }

View file

@ -27,7 +27,13 @@ thermograph_openbao_source() {
local env_name="${1:?env name required}" local env_name="${1:?env name required}"
local addr="${THERMOGRAPH_BAO_ADDR:-https://10.10.0.1:8200}" local addr="${THERMOGRAPH_BAO_ADDR:-https://10.10.0.1:8200}"
local mount="${THERMOGRAPH_BAO_MOUNT:-thermograph}" local mount="${THERMOGRAPH_BAO_MOUNT:-thermograph}"
local approle="${THERMOGRAPH_BAO_APPROLE:-/etc/thermograph/openbao-approle}" # Explicit override first, then the per-environment value env-topology.sh derives,
# then the conventional path. Same precedence shape as render-secrets.sh:44's
# THERMOGRAPH_SECRETS_BACKEND / TG_SECRETS_BACKEND pair, for the same reason: the
# THERMOGRAPH_-prefixed name is the by-hand escape hatch, the TG_ one is what the
# deploy path sets. Falling straight through to the bare default is what made beta
# authenticate as tg-prod and get denied its own path.
local approle="${THERMOGRAPH_BAO_APPROLE:-${TG_BAO_APPROLE:-/etc/thermograph/openbao-approle}}"
local ca="${THERMOGRAPH_BAO_CACERT:-/etc/thermograph/openbao-ca.crt}" local ca="${THERMOGRAPH_BAO_CACERT:-/etc/thermograph/openbao-ca.crt}"
command -v bao >/dev/null 2>&1 || { command -v bao >/dev/null 2>&1 || {

View file

@ -185,15 +185,14 @@ its own host:
```sh ```sh
# on vps2 # on vps2
infra/openbao/verify-parity.sh --env prod # expect 32 keys infra/openbao/verify-parity.sh --env prod # expect 32 keys
THERMOGRAPH_BAO_APPROLE=/etc/thermograph/openbao-approle-beta \ infra/openbao/verify-parity.sh --env beta # expect 24 keys
infra/openbao/verify-parity.sh --env beta # expect 24 keys
# on vps1 # on vps1
cd /opt/thermograph-dev && infra/openbao/verify-parity.sh --env dev # expect 12 keys cd /opt/thermograph-dev && infra/openbao/verify-parity.sh --env dev # expect 12 keys
``` ```
The explicit `THERMOGRAPH_BAO_APPROLE` for beta is a stopgap: `env-topology.sh` does Beta needs no special handling: `env-topology.sh` derives `TG_BAO_APPROLE` per
not yet derive the AppRole path per environment, so the renderer falls back to prod's environment, so beta authenticates as `tg-beta` rather than falling back to prod's
credentials and `tg-host-prod.hcl` correctly denies `env/beta`. credentials and being denied its own path by `tg-host-prod.hcl`.
`seed-from-sops.sh` reads every production secret in plaintext. Per `infra/CLAUDE.md` `seed-from-sops.sh` reads every production secret in plaintext. Per `infra/CLAUDE.md`
the equivalent `seed-from-live.sh` is explicitly *not for an agent to run*; this the equivalent `seed-from-live.sh` is explicitly *not for an agent to run*; this

View file

@ -213,9 +213,8 @@ secrets is not for an agent to run:
Then prove parity. NOT \`--all\` from one box: each AppRole is CIDR-bound to the host Then prove parity. NOT \`--all\` from one box: each AppRole is CIDR-bound to the host
that legitimately renders it, so an environment can only be verified from its own host. that legitimately renders it, so an environment can only be verified from its own host.
# here (vps2) -- beta needs its own AppRole, prod uses the default # here (vps2)
infra/openbao/verify-parity.sh --env prod # expect 32 keys infra/openbao/verify-parity.sh --env prod # expect 32 keys
THERMOGRAPH_BAO_APPROLE=/etc/thermograph/openbao-approle-beta \\
infra/openbao/verify-parity.sh --env beta # expect 24 keys infra/openbao/verify-parity.sh --env beta # expect 24 keys
# on vps1 # on vps1
cd /opt/thermograph-dev && infra/openbao/verify-parity.sh --env dev # expect 12 keys cd /opt/thermograph-dev && infra/openbao/verify-parity.sh --env dev # expect 12 keys