infra-sync: render /etc/centralis.env from the vault #147

Merged
admin_emi merged 1 commit from feat/sync-centralis-env into dev 2026-08-01 16:13:57 +00:00
Owner

render_centralis_secrets has existed since the OpenBao work and has no caller on any deploy path — only verify-centralis-render.sh, which is a check. A renderer nothing calls is not a migration, it is a file. /etc/centralis.env has stayed hand-maintained the whole time, with the vault holding a copy nothing consumed.

That gap is why turning on Google sign-in for Centralis had no safe home: the only way to set CENTRALIS_GOOGLE_CLIENT_ID was to hand-edit the live file, which is exactly what render-secrets.sh's own header warns against.

The guard is the point

On 2026-07-24 this estate lost Centralis' token registry by writing a file that was missing a key the live one had. The renderer proves its own output round-trips through bash, but it cannot know about a key that exists only in the live file — someone hand-editing /etc/centralis.env is invisible to it.

So this job renders to a scratch path first, compares key sets (names only; both files sourced under env -i so the comparison is over what bash actually ends up with, not over text), and refuses to write, failing loudly, if the live file has anything the vault does not:

!! REFUSING to write /etc/centralis.env.
!! The live file has keys the vault does not, so rendering would DELETE them:
!!   CENTRALIS_GOOGLE_CLIENT_ID
!! Someone hand-edited the live file. Put those keys in the vault instead:
!!   sops edit infra/deploy/secrets/centralis.prod.yaml

That inverts the hazard: rendering stops being a way to lose a hand-edit and becomes the thing that catches one.

Verified against a live file carrying a hand-added CENTRALIS_GOOGLE_CLIENT_ID — correctly flagged, write refused — and against the empty/unreadable-live-file path, which needs || true to survive pipefail.

Why this job recreates a container when the other three deliberately do not

This workflow's header says it never rolls a service, because image tags are the app domains' axis. Centralis differs in kind: its compose file interpolates /etc/centralis.env at parse time (set -a && . /etc/centralis.env && docker compose up -d), so rendering without recreating changes nothing at all — the silent no-op this estate keeps getting bitten by. docker restart would not do it either, for the same reason. It is one container with no replicas: a two-second recreate, not a deploy.

It also skips the recreate entirely when the rendered file is byte-identical to the live one, so a no-op push does not bounce a healthy container.

Sequencing note

Once this is live, the OAuth keys go in the vault, not in /etc/centralis.env:

sops edit infra/deploy/secrets/centralis.prod.yaml

Adding them to the live file instead will now fail this job rather than being silently overwritten on the next infra push.

`render_centralis_secrets` has existed since the OpenBao work and has **no caller on any deploy path** — only `verify-centralis-render.sh`, which is a check. A renderer nothing calls is not a migration, it is a file. `/etc/centralis.env` has stayed hand-maintained the whole time, with the vault holding a copy nothing consumed. That gap is why turning on Google sign-in for Centralis had no safe home: the only way to set `CENTRALIS_GOOGLE_CLIENT_ID` was to hand-edit the live file, which is exactly what `render-secrets.sh`'s own header warns against. ## The guard is the point On 2026-07-24 this estate lost Centralis' token registry by writing a file that was missing a key the live one had. The renderer proves its **own** output round-trips through bash, but it cannot know about a key that exists only in the live file — someone hand-editing `/etc/centralis.env` is invisible to it. So this job renders to a scratch path first, compares **key sets** (names only; both files sourced under `env -i` so the comparison is over what bash actually ends up with, not over text), and **refuses to write, failing loudly**, if the live file has anything the vault does not: ``` !! REFUSING to write /etc/centralis.env. !! The live file has keys the vault does not, so rendering would DELETE them: !! CENTRALIS_GOOGLE_CLIENT_ID !! Someone hand-edited the live file. Put those keys in the vault instead: !! sops edit infra/deploy/secrets/centralis.prod.yaml ``` That inverts the hazard: rendering stops being a way to lose a hand-edit and becomes the thing that catches one. **Verified** against a live file carrying a hand-added `CENTRALIS_GOOGLE_CLIENT_ID` — correctly flagged, write refused — and against the empty/unreadable-live-file path, which needs `|| true` to survive `pipefail`. ## Why this job recreates a container when the other three deliberately do not This workflow's header says it never rolls a service, because image tags are the app domains' axis. Centralis differs in kind: its compose file interpolates `/etc/centralis.env` at **parse** time (`set -a && . /etc/centralis.env && docker compose up -d`), so rendering without recreating changes nothing at all — the silent no-op this estate keeps getting bitten by. `docker restart` would not do it either, for the same reason. It is one container with no replicas: a two-second recreate, not a deploy. It also skips the recreate entirely when the rendered file is byte-identical to the live one, so a no-op push does not bounce a healthy container. ## Sequencing note Once this is live, the OAuth keys go in the **vault**, not in `/etc/centralis.env`: ``` sops edit infra/deploy/secrets/centralis.prod.yaml ``` Adding them to the live file instead will now fail this job rather than being silently overwritten on the next infra push.
admin_emi added 1 commit 2026-08-01 16:10:59 +00:00
infra-sync: render /etc/centralis.env from the vault
All checks were successful
PR build (required check) / changes (pull_request) Successful in 7s
secrets-guard / encrypted (pull_request) Successful in 6s
PR build (required check) / build-backend (pull_request) Has been skipped
shell-lint / shellcheck (pull_request) Successful in 7s
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) / gate (pull_request) Successful in 2s
shell-lint / shellcheck (push) Successful in 8s
secrets-guard / encrypted (push) Successful in 7s
b62eb60121
render_centralis_secrets has existed since the OpenBao work and had no caller
on any deploy path -- only verify-centralis-render.sh, a check. A renderer
nothing calls is not a migration, it is a file. /etc/centralis.env has stayed
hand-maintained the whole time, with the vault holding a copy nothing consumed.

That gap is why turning on Google sign-in for Centralis had no safe home: the
only way to set CENTRALIS_GOOGLE_CLIENT_ID was to hand-edit the live file,
which is precisely what render-secrets.sh's header warns against.

THE GUARD IS THE POINT. On 2026-07-24 this estate lost Centralis' token
registry by writing a file missing a key the live one had. The renderer proves
its own output round-trips through bash, but it cannot know about a key that
exists only in the live file. So this job renders to a scratch path first,
compares KEY SETS (names only, both sides sourced in `env -i` so the
comparison is over what bash actually ends up with), and refuses to write --
failing loudly -- if the live file has anything the vault does not. Rendering
turns from a way to lose a hand-edit into the thing that catches one.

Verified the guard against a live file carrying a hand-added
CENTRALIS_GOOGLE_CLIENT_ID: correctly identified as a key that would be
deleted, write refused. Also verified the empty/unreadable-live-file path,
which needs `|| true` to survive pipefail.

It recreates the container where the other three jobs deliberately roll
nothing, and the difference is real rather than an exception: Centralis'
compose file interpolates /etc/centralis.env at PARSE time, so rendering
without recreating changes nothing -- the silent no-op this estate keeps
getting bitten by. `docker restart` would not do it either. One container, no
replicas, a two-second recreate. It also skips the recreate entirely when the
rendered file is byte-identical to the live one, so a no-op push does not
bounce a healthy container.
admin_emi merged commit b62eb60121 into dev 2026-08-01 16:13:56 +00:00
Sign in to join this conversation.
No reviewers
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference: Jinemi/thermograph#147
No description provided.