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

secret.key leaves the snapshot bucket: whoever reads the bucket must not hold the key and the data together #366

Merged opened this in answer to

Check · Review · Land · Live done — Landed and live in production.

Merged

Snapshots no longer put the raw `secret.key` in the bucket. `test/snapshot.test.ts` and `test/web.test.ts` pass. I did not get `bun x tsc --noEmit` to run here (it fails with "Cannot find type definition file for 'bun'"), so the full `forge.json` check has not been run.

shipped production is serving it deploy 9f16946

opened this

Snapshots no longer put the raw secret.key in the bucket. test/snapshot.test.ts and test/web.test.ts pass. I did not get bun x tsc --noEmit to run here (it fails with "Cannot find type definition file for 'bun'"), so the full forge.json check has not been run.

What changed

  • Wrapped key: with FORGE_SNAPSHOT_PASSPHRASE set, a snapshot writes secret.key.wrapped instead (new src/snapshot/wrap.ts). It is AES-256-GCM under a scrypt-derived key and uses only node:crypto, so there is no new dependency. Each snapshot checks the wrapped key opens before it is written.
  • No passphrase: no key travels at all. The manifest has a new key field (wrapped, none, or raw for older snapshots).
  • Restore:

- With the right passphrase, secret.key is rebuilt at mode 0600 and sealed values open. - A wrong passphrase refuses the restore before anything is written. - With no passphrase, repos, issues and app data restore. The command lists every sealed value by name (NAME (app slug) or NAME (scope secret)) that must be set again. The restored forge makes a new key on first boot.

  • Backups page: it says whether the last snapshot carried a wrapped key. It always warns that losing the passphrase loses every sealed value. When no passphrase is set, it says no key travels and how to turn wrapping on.
  • Docs: docs/SNAPSHOT.md has a new "The key" section covering where the passphrase lives and what is lost without it. docs/EXTRACT.md now has the restore command with the passphrase.
  • Tests: these cover the raw key being in no store file, restore with the passphrase, restore without it (sealed value named, repo intact), a wrong passphrase, a snapshot with no passphrase, wrap round-trip and tamper, and the page in both states.

What I left out

  • Snapshots taken before this change still hold the raw key in the bucket. Restore still accepts them and the page tells you to take a new one. I did not delete them, so an operator who distrusts the bucket has to remove them by hand.
  • No passphrase rotation or KMS support.
  • The "lost without the passphrase" list covers forge, org and repo secrets and app variables. Origin, prompt and subscription tokens are not enumerated by name.

To try it

FORGE_SNAPSHOT_DIR=/tmp/s FORGE_SNAPSHOT_PASSPHRASE=… bun run src/main.ts

Take a snapshot from Settings → Backups, then run FORGE_SNAPSHOT_PASSPHRASE=… bun run src/snapshot/restore.ts <name> <empty-dir> with the same FORGE_SNAPSHOT_DIR.

🤖 Generated with Claude Code

Closes #337

Sign in to join this conversation.

↑↓ navigate ↵ open ⌘↵ its app esc close

Keyboard shortcuts

Docs