Skip to content

feat(template): optional fnox-encrypted production secrets - #2114

Open
davidpoblador wants to merge 1 commit into
mainfrom
fnox-secrets
Open

feat(template): optional fnox-encrypted production secrets#2114
davidpoblador wants to merge 1 commit into
mainfrom
fnox-secrets

Conversation

@davidpoblador

Copy link
Copy Markdown
Member

Carries back what a downstream project (alltuner/radio) hit while migrating its secrets, so the next project generated from this template does not repeat it.

What

A new age_recipient answer. Provide one and the project scaffolds:

  • fnox.toml — age-encrypted secrets, safe to commit
  • compose.secrets.yml — deploy-only overlay declaring the secret environment
  • deploy-latest running under fnox exec

Leave it blank and the existing plaintext .env flow is generated unchanged.

The trap this encodes

Secrets go in the overlay, not compose.prod.yml, because CI builds with:

docker buildx bake -f compose.prod.yml --push

Bake interpolates the entire file, not only its build: sections. A ${VAR:?} guard there fails every CI build, since a runner has no secrets. Downstream this tagged a release that was then never built, and it is invisible locally because the bake step only runs on release.

The deploy also passes --env-file /dev/null, so Compose cannot quietly interpolate a local .env. Without that the guards are satisfied by whatever file happens to be lying around, and a deploy outside fnox exec silently uses stale values instead of failing.

Mechanics

compose.prod.yml and .justfiles/cicd.justfile become .j2 so they can vary on the answer. The single {{ HOST }} in the recipe is escaped, since that is just syntax rather than copier's.

Verified

Generated a project both ways:

with age_recipient without
fnox.toml, compose.secrets.yml present absent
env_file in compose.prod.yml absent present (×2)
bake with no secrets exit 0 exit 0
deploy with unset secret fails loudly n/a
deploy with secret set exit 0 n/a

No stray Jinja in either output.

Answering `age_recipient` scaffolds a fnox.toml and a compose.secrets.yml
overlay, and points deploy-latest at `fnox exec`, so production secrets are
age-encrypted in a committable file and decrypted only at deploy. Leaving
it blank keeps the existing plaintext .env flow.

Secrets go in the overlay rather than compose.prod.yml because CI builds
with `docker buildx bake -f compose.prod.yml`, which interpolates the whole
file and not only its build sections — a ${VAR:?} guard there fails every
build, since a runner has no secrets. This was found the hard way in a
downstream project whose release was tagged but never built.

The deploy also passes --env-file /dev/null, so Compose cannot quietly
interpolate a local .env and a deploy outside fnox exec fails loudly rather
than starting with blank credentials.

compose.prod.yml and cicd.justfile become .j2 so they can vary on the
answer; the one `{{ HOST }}` in the recipe is escaped as just syntax.

Verified by generating both variants: with a recipient, bake succeeds with
no secrets, the deploy renders, and an unset secret fails loudly; without
one, the .env flow is byte-identical to before.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QnxqibhS4XvDJF3abxcdPT
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant