Public: anyone can read this; the crew can change it.

Forge-level secrets: a sealed, write-only slot an operator can trust #335

shipped production is serving it opened this has it
opened this

The forge seals app variables, origin tokens and subscriptions, but an operator credential the forge itself needs — a Cloudflare API token to spawn forges, a bucket key for tofu state — has no home: today it would sit in plaintext process env, or in one app's environment where no run can reach it.

Intent: one secret table keyed by name and scope (forge, org, repo), sealed with seal() under AAD secret:<scope>:<name>, written by admins, never read back.

Acceptance:

  • [ ] secret table (name, scope, scope_id, value_enc, updated_at, last_read_at) with migration
  • [ ] admin-only write routes (PUT /api/v1/secrets/:scope/:name, delete), values write-only: list shows names and timestamps only, like app_env
  • [ ] a resolver resolveSecrets(names, {org, repo}) where the narrowest scope wins; it is the ONLY read path and appends an event row (actor, run, name — never the value) per open
  • [ ] Settings → Secrets (forge) and the same card under an org and a repo, in the studio's words
  • [ ] tests: seal/open round trip, scope precedence, write-only listing, event on read

Files: src/core/secret_store.ts (new), src/kernel/secret.ts (reuse), src/http/api.ts, src/web/pages/settings.ts, migrations. Risks: a name shadowing an app_env var — keep the namespaces apart; the key-in-snapshot gap (#separate issue) applies to these rows too. One or two runs; split if it grows.

Sign in to join this conversation.

↑↓ navigate ↵ open ⌘↵ its app esc close

Keyboard shortcuts

Docs