feat(secrets): sync the app secrets from Infisical #505

Merged
Claude-Run merged 1 commit from feat/BUNYIP-504-sync-secrets-infisical into main 2026-08-08 00:36:21 +02:00
Member

compose.yml mounts every application secret from a file under ./secrets/ and the api reads it through the {NAME}_FILE convention (BUNYIP-38), but nothing populated those files from a secrets manager: deployed hosts kept them in sops and dev boxes generated throwaways. Infisical was already in use for the E2E account password only.

scripts/sync-secrets.nu renders the ./secrets/* files from the Infisical folder /bunyip/app. It syncs rather than fetches at runtime, so the containers keep reading /run/secrets/*, no secret re-enters the process environment, and a bunyip-api restart never depends on Infisical being reachable. No Rust changes.

Behaviour: authenticates with a machine identity via Universal Auth (INFISICAL_CLIENT_ID / INFISICAL_CLIENT_SECRET, or a pre-set INFISICAL_TOKEN that short-circuits the login) and fails naming the missing variable; reads one key per mapped secret with the infisical secrets get ... --plain form already documented in docs/e2e.md; aborts before any write when a mapped key is absent, or empty where the table forbids empty, so a silent empty file can never turn RLS or a feature off; writes atomically at mode 0400 via a temp file in the same directory, skipping values that already match so a re-run leaves mtimes alone; prints no secret value in any mode; leaves ./secrets/oidc/*.pem alone.

The mapping rule is that the Infisical key is the {NAME} of the compose {NAME}_FILE entry, and it is not hand-maintained on both sides: sync-secrets.nu --self-test re-derives the table from the compose.yml secrets block and the service environment, exercises the write rules (0400, atomic, no-op re-run, no value printed, missing/empty key rejected), and runs as a check.yml step, so a new compose secret or a loosened write rule fails CI.

Nushell reserves env as a variable name, so the script's flag is --environment; just sync-secrets accepts --env and --dry-run and translates.

init-secrets.nu is unchanged as the dev-box path and gains a header pointer to the sibling script. Docs: new docs/secrets-infisical.md (machine identity setup, folder layout, mapping table, rotation flow and the reencrypt-secrets interaction for APP_ENCRYPTION_KEY), plus the compose.yml quick start, README self-host, docs/getting-started.md prerequisites, CLAUDE.md conventions, and the docs/e2e.md paragraph that described the wiring as a future item.

#BUNYIP-504

compose.yml mounts every application secret from a file under ./secrets/ and the api reads it through the {NAME}_FILE convention (BUNYIP-38), but nothing populated those files from a secrets manager: deployed hosts kept them in sops and dev boxes generated throwaways. Infisical was already in use for the E2E account password only. scripts/sync-secrets.nu renders the ./secrets/* files from the Infisical folder /bunyip/app. It syncs rather than fetches at runtime, so the containers keep reading /run/secrets/*, no secret re-enters the process environment, and a bunyip-api restart never depends on Infisical being reachable. No Rust changes. Behaviour: authenticates with a machine identity via Universal Auth (INFISICAL_CLIENT_ID / INFISICAL_CLIENT_SECRET, or a pre-set INFISICAL_TOKEN that short-circuits the login) and fails naming the missing variable; reads one key per mapped secret with the `infisical secrets get ... --plain` form already documented in docs/e2e.md; aborts before any write when a mapped key is absent, or empty where the table forbids empty, so a silent empty file can never turn RLS or a feature off; writes atomically at mode 0400 via a temp file in the same directory, skipping values that already match so a re-run leaves mtimes alone; prints no secret value in any mode; leaves ./secrets/oidc/*.pem alone. The mapping rule is that the Infisical key is the {NAME} of the compose {NAME}_FILE entry, and it is not hand-maintained on both sides: `sync-secrets.nu --self-test` re-derives the table from the compose.yml secrets block and the service environment, exercises the write rules (0400, atomic, no-op re-run, no value printed, missing/empty key rejected), and runs as a check.yml step, so a new compose secret or a loosened write rule fails CI. Nushell reserves `env` as a variable name, so the script's flag is --environment; `just sync-secrets` accepts --env and --dry-run and translates. init-secrets.nu is unchanged as the dev-box path and gains a header pointer to the sibling script. Docs: new docs/secrets-infisical.md (machine identity setup, folder layout, mapping table, rotation flow and the reencrypt-secrets interaction for APP_ENCRYPTION_KEY), plus the compose.yml quick start, README self-host, docs/getting-started.md prerequisites, CLAUDE.md conventions, and the docs/e2e.md paragraph that described the wiring as a future item. #BUNYIP-504
feat(secrets): sync the app secrets from Infisical
All checks were successful
E2E / PR gate (pull_request) Successful in 31s
Check / fmt + clippy + build + tests (pull_request) Successful in 8m41s
Create release / Create release from merged PR (pull_request) Has been skipped
f4eb570fec
compose.yml mounts every application secret from a file under ./secrets/ and the api reads it through the {NAME}_FILE convention (BUNYIP-38), but nothing populated those files from a secrets manager: deployed hosts kept them in sops and dev boxes generated throwaways. Infisical was already in use for the E2E account password only.

scripts/sync-secrets.nu renders the ./secrets/* files from the Infisical folder /bunyip/app. It syncs rather than fetches at runtime, so the containers keep reading /run/secrets/*, no secret re-enters the process environment, and a bunyip-api restart never depends on Infisical being reachable. No Rust changes.

Behaviour: authenticates with a machine identity via Universal Auth (INFISICAL_CLIENT_ID / INFISICAL_CLIENT_SECRET, or a pre-set INFISICAL_TOKEN that short-circuits the login) and fails naming the missing variable; reads one key per mapped secret with the `infisical secrets get ... --plain` form already documented in docs/e2e.md; aborts before any write when a mapped key is absent, or empty where the table forbids empty, so a silent empty file can never turn RLS or a feature off; writes atomically at mode 0400 via a temp file in the same directory, skipping values that already match so a re-run leaves mtimes alone; prints no secret value in any mode; leaves ./secrets/oidc/*.pem alone.

The mapping rule is that the Infisical key is the {NAME} of the compose {NAME}_FILE entry, and it is not hand-maintained on both sides: `sync-secrets.nu --self-test` re-derives the table from the compose.yml secrets block and the service environment, exercises the write rules (0400, atomic, no-op re-run, no value printed, missing/empty key rejected), and runs as a check.yml step, so a new compose secret or a loosened write rule fails CI.

Nushell reserves `env` as a variable name, so the script's flag is --environment; `just sync-secrets` accepts --env and --dry-run and translates.

init-secrets.nu is unchanged as the dev-box path and gains a header pointer to the sibling script. Docs: new docs/secrets-infisical.md (machine identity setup, folder layout, mapping table, rotation flow and the reencrypt-secrets interaction for APP_ENCRYPTION_KEY), plus the compose.yml quick start, README self-host, docs/getting-started.md prerequisites, CLAUDE.md conventions, and the docs/e2e.md paragraph that described the wiring as a future item.

#BUNYIP-504
Claude-Run deleted branch feat/BUNYIP-504-sync-secrets-infisical 2026-08-08 00:36:21 +02:00
Sign in to join this conversation.
No reviewers
No labels
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
psa-systems/bunyip!505
No description provided.