0015. A secrets manifest is the one list of Key Vault secrets
- Status: accepted (the sync tool is ADR 0016; ADR 0017 makes it the only writer of the
appgroup) - Date: 2026-10-15
Context
Which secrets Pixel keeps in Azure Key Vault was written down in several places at once: the Terraform locals (kv_secret_names, pixel_secret_env), the write-only entries in secrets.tf, the module and stack variables, src/config.ts, .env.example, the architecture config table and the runbook. Adding a secret (/spacestate needed two) meant editing all of them, and nothing failed when one was missed. A Key Vault sync tool, so that maintainers stop pasting values through Terraform variables, needs the same list and would be a seventh copy.
Decision
infra/secrets.manifest.yamlis the single source of truth. Per secret: its Key Vault name (vault), agroup, how it isdeliveryed, the environment variable (env), thefeaturethat turns it on, and a description. It holds names and metadata only, never a value.- Two groups.
appsecrets belong to the Pixel app and are what the sync tool will manage.infrasecrets (tailscale-auth-key,ghcr-pull-token) belong to the platform around it, and Terraform keeps writing them. - Terraform reads the manifest.
infra/modules/pixel/locals.tfderiveskv_secret_namesandpixel_secret_envfrom it withyamldecode. The only thing still written in HCL is the condition behind each feature name (secret_features). scripts/secrets-manifest.tsloads and validates it (just validate-secrets). It collects every problem, and never repeats a value from the file.- A parity test fails on drift.
scripts/secrets-manifest.test.tschecks the manifest againstsecrets.tf, the Terraform variables (ephemeral and sensitive, in the module and both stacks),config.ts,.env.example, the architecture config table, the runbook and the infra doc, and names whatever is missing. - Terraform still writes the values for now. Moving the app group's values to a sync tool is a later change.
Consequences
- Adding a secret is one manifest entry plus the places the test names; forgetting one fails
just check. - The Terraform output is unchanged: the manifest-driven logic gives the same secrets and environment variables as the old locals for every combination of features.
- The parity test reads Terraform, docs and
.env.example, so those paths are in thecheckallowlist of.github/path-filters.yml. - A new feature condition needs a line in
secret_featuresas well as in the manifest. The test catches a mismatch.