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

The Backups page takes the snapshot destination: bucket and keys sealed in the forge, not plaintext in its environment #367

Merged opened this in answer to

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

Merged

The Backups page now takes the snapshot destination: endpoint, bucket, region, access key and secret key. The keys are sealed in the forge's secrets table, and the `S3_*` environment variables remain a fallback.

shipped production is serving it deploy d63cf43

opened this

The Backups page now takes the snapshot destination: endpoint, bucket, region, access key and secret key. The keys are sealed in the forge's secrets table, and the S3_* environment variables remain a fallback.

I could not run tsc, because bun x tsc --noEmit fails here with "Cannot find type definition file for 'bun'". The new destination tests, test/snapshot.test.ts, test/secret-store.test.ts and test/web.test.ts pass. I did not run the full check from forge.json.

What changed

  • Resolver: src/snapshot/destination.ts is the one place that decides where snapshots go. Precedence is the sealed destination, then FORGE_SNAPSHOT_DIR, then S3_*. The nightly snapshot, "Snapshot now", the test button, the page and the restore command all use it.
  • Sealing: the five fields are sealed as forge-level secrets named SNAPSHOT_S3_*. A new destination takes effect on the next snapshot, without a restart. Nothing is ever shown back: the page says which fields are set, and a blank field keeps its sealed value.
  • Page: the Backups page has a form and a line saying which destination is in effect (sealed, directory, or environment). Its buttons are:

- Seal destination: saves the fields. - Test connection: lists the bucket and reports one sentence. It tries the typed values, or the destination in effect if all four required fields are blank. The failure message has the keys stripped out. - Clear sealed destination: removes it, so the environment's destination takes over again.

  • Validation: the endpoint must be an http(s) URL without credentials, the bucket name is checked, and the keys must be printable with no spaces. Only admins can write, and the existing same-origin check covers the new POST routes.
  • Always-on service: the snapshot service now starts on every boot and resolves its destination on each run. A forge with no destination shows the old "No snapshots are taken" notice, now with the form below it.
  • Tests and docs: test/snapshot-destination.test.ts covers precedence, a failed test-connection message with no key in it, partial seals, validation, non-admins, and the page not showing the keys. I also added a section to docs/SNAPSHOT.md.

Left out

  • Restore: it reads the environment only. A restore starts from an empty data directory, so nothing is sealed yet, and the operator keeps S3_* or FORGE_SNAPSHOT_DIR for it. The docs say so.
  • Prefill and audit: the form never prefills, not even the endpoint or bucket, because there is no read-back path for a sealed value. Each time the destination is resolved (on a change, or after a restart) it adds secret.read events to the audit log.
  • Endpoint safety: the endpoint is not restricted to public hosts. Only an admin can set it.

To try: sign in as an admin, open Settings → Backups, fill in the form, press "Test connection", then "Seal destination" and "Snapshot now".

🤖 Generated with Claude Code

Closes #338

commented

I merged main into the branch and pushed it. The four conflicts were all in the snapshot and Backups code, and I kept both sides' changes.

I could not typecheck. bun x tsc --noEmit fails here with "Cannot find type definition file for 'bun'", the same as before. The tests for the destination, snapshot, secret store and web pass (63 in total). I did not run the full check from forge.json.

How I resolved each file:

  • src/main.ts: the snapshot service still resolves its destination on each run, so a sealed bucket takes effect without a restart. It now also passes FORGE_SNAPSHOT_PASSPHRASE, so the wrapped-key behaviour from #366 is unchanged.
  • src/snapshot/service.ts: the destination can still change at runtime, and the handle keeps wraps. The startup log line reports both the destination and whether the key is wrapped.
  • src/snapshot/restore.ts: the CLI still reads its destination through the one resolver. It keeps the passphrase handling and the report of sealed values that could not be restored.
  • src/web/pages/backups.ts: the page has the key-wrapping notice and the destination form together.

What to look at: the restore CLI still reads only the environment, because a restore into an empty directory has nothing sealed yet. This is the same as before the merge.

🤖 Generated with Claude Code

Sign in to join this conversation.

↑↓ navigate ↵ open ⌘↵ its app esc close

Keyboard shortcuts

Docs