0016. A laptop tool syncs app secrets with Key Vault
- Status: accepted (Terraform's part narrowed by ADR 0017)
- Date: 2026-10-15
Context
Maintainers set Pixel's secrets in Key Vault by pasting values into gitignored terraform.tfvars (with write_secrets = true) or by running az keyvault secret set one name at a time. Neither shows what's in the vault now, neither says what a change would do before it happens, and it's easy to forget the restart afterwards or to set a secret in the wrong environment's vault. The same tokens already live on the maintainer's laptop in .env.dev / .env.prod for just register. ADR 0015 made infra/secrets.manifest.yaml the list of secrets and marked the app group as the one a sync tool would manage.
Decision
just secrets-pull dev|prodandjust secrets-push dev|prod(scripts/secrets-sync.ts) sync the manifest'sappsecrets between an environment's vault (pixel-dev-kv,pixel-prod-kv) and the laptop. Env secrets live under their environment variable in.env.<env>;admins-yamllives inconfig/admins.<env>.yaml. All are gitignored.infrasecrets stay with Terraform and the tool never reads them.- It follows
just ha-devices. Pull reads the vault and writes the local files, asking before it overwrites a local value you haven't pushed. Push reads the vault (the live values), compares, shows what would change and asks. Before it writes it reads the vault again and stops if anything changed meanwhile; after it writes it reads every value back. - It never prints a value. A secret is shown by name and length.
admins.yamlis IDs, not a token, so its diff is shown, and a push checks it against that environment'smembers.yamlon the file share the way Pixel does at startup. - A push never deletes a secret from the vault, and never sets one from a command-line argument (values go through a private temporary file).
- Guards. The env file must say the right
PIXEL_ENV(the same check asjust register). A Discord token that's also in another bot's env file (.env.devor.envfor prod,.env.prodfor dev) is refused. Pushing to prod also asks you to typeprod. - Restart is offered, not assumed. Pixel reads secrets once at startup. After a push the tool offers
just restart <env>, which restarts the running revision (one replica) without deploying anything.
Consequences
- Terraform's
write_secretspath owns only theinfrasecrets. It used to write app values too, which collided with this tool; since ADR 0017 this tool is the only writer of theappgroup. - A maintainer needs Key Vault Secrets Officer on the vault (Secrets User is enough to pull), and the same storage access as
just ha-devicesfor themembers.yamlcheck. - The local files hold real secrets. They are gitignored and written with mode
0600; don't copy them anywhere else. - Coding agents don't run
just secrets-push prodorjust push prod(AGENTS.md: no prod secrets).
Addendum: just pull / just push and the shared tfvars
Each stack's terraform.tfvars lived only on the laptop of whoever last applied, so nobody else could plan or apply. It is now the Key Vault secret terraform-tfvars in that environment's vault, in a new manifest group, tooling (delivery tool): read and written only by the laptop tools. Terraform skips the group, so the Container App never references it.
just pull dev|prodpulls the tfvars, the app secrets, and read-only mirrors of the share'smembers.yaml,devices.yamlandannouncement-routes.yaml.just push dev|prodpushes the tfvars and the app secrets, with the same diff, approve, re-check and read-back as above. The mirrors never go back up: Pixel andjust ha-devicesstay their only writers.- The tfvars hold settings, never secrets. A push refuses one that sets a variable the stack marks
sensitive, sets one it doesn't declare, or names the other environment's vault or storage account. Secret inputs go insecrets.auto.tfvars, which Terraform auto-loads and which never leaves the laptop. The push checks the tfvars before it shows a diff, and the diff hides every sensitive variable's value (commented-out ones too), so a secret that wrongly ended up in the file is never printed. - Pushing the tfvars changes nothing on Azure by itself. Plan and apply stay separate steps (and prod stays maintainer-only).