Skip to content

Restore at setup: the wizard accepts an encrypted backup in place of the config form (#786 sub-issue B) - #933

Merged
VijitSingh97 merged 2 commits into
develop-v2from
feat/909-restore-at-setup
Aug 14, 2026
Merged

Restore at setup: the wizard accepts an encrypted backup in place of the config form (#786 sub-issue B)#933
VijitSingh97 merged 2 commits into
develop-v2from
feat/909-restore-at-setup

Conversation

@VijitSingh97

Copy link
Copy Markdown
Collaborator

Implements issue #909 — completes the disaster-recovery loop that backup export (#908) opens, the Home Assistant restore-during-onboarding pattern.

Mechanism: the setup wizard gains a "restore from backup" path — upload an encrypted pithead-backup-*.tar.gz.enc + its passphrase as an alternative to the config form. The host side (firstboot_consume_restore) reuses stack_restore's machinery: magic-byte format check, full-stream integrity verify BEFORE anything is touched, then decrypt/validate the embedded config through a COPY (never mutate live state until accepted) and land it exactly like a typed submission. A rejected archive — wrong passphrase, malformed, oversize (64 MiB cap, enforced client + server), or now unsafe members — falls back to the form with the reason surfaced, never blocking setup. Restore is wizard-time only. The passphrase is read once and deleted immediately, accepted or not; it never reaches argv or logs.

Verifier caught one critical gap, fixed (commit 5b4ee02): extraction went to a temp dir then cp -a to /, checking only that config.json landed — an absolute path, a .. component, or a symlink/hardlink member could escape. Now the member list is audited and refused fail-closed before staging, regardless of tar version; tier-1 cases cover a symlink and an absolute-path member (both refused, live files untouched).

Coverage: tier-1 firstboot_consume_restore cases (accept, wrong/missing passphrase, oversize, malformed, traversal, empty) — suite 2209/0; a KVM restore-boot leg in tests/os/run.sh (capture backup → upload → boot fresh disk → verify wallet/onion/db restored), proven in the next battery. Documented in docs/appliance.md.

Part of the #786 pre-GA set (A #908 backup, B this, D #910 media). Ponytail: reuses stack_restore, no new crypto or abstraction.

🤖 Generated with Claude Code

VijitSingh97 and others added 2 commits August 13, 2026 10:30
…d of the config form

Implements #909 (#786 sub-issue B): the setup wizard accepts an uploaded encrypted
pithead-backup archive + its passphrase as an alternative to the config form, on both a
plain first boot and the installation medium's combined install+configure page.

- wizard.py: POST /submit-restore writes the uploaded archive + passphrase to the spool
  (same "container asks, host decides" split as the rest of the wizard); a size cap
  (64 MiB — config/keys/db, never chains) is enforced both by aiohttp's client_max_size
  and an explicit check with a clearer message. On the installation medium the disk/wipe
  fields ride beside the archive through the same _gate_install_request every other
  submission takes.
- pithead: firstboot_consume_restore decrypts and integrity-verifies the archive (the same
  magic-byte + full-stream check stack_restore uses), stages the extraction through a
  mktemp copy, validates the embedded config.json via the same fresh-process
  parse_and_validate_config call every other config path uses, and only on success commits
  the whole tree onto "/" and touches $spool/applied — landing in the identical accept path
  a typed submission takes. A rejected archive (bad passphrase, wrong format, failed
  integrity, unusable config) writes error.txt and touches nothing, falling back to the
  form exactly like a rejected config. The passphrase is read once and deleted immediately
  either way.
- wizard.mjs: a "Restoring an existing Pithead? Upload its backup instead." toggle above
  the setup form, and its own small card (archive upload + passphrase) that respects the
  installer's disk picker.
- docs: docs/appliance.md gets a "Recovering from a backup" section (fresh flash -> restore
  -> done) and a troubleshooting entry; docs/dev/appliance-wizard.md documents the new spool
  channel and validate-through-a-copy design; docs/dev/testing-strategy.md records the new
  tier-1 and tier-4 coverage.
- tests: tier-1 stage/spool cases in test_wizard.py (accept, missing archive, oversize,
  installer disk gates) and wizard.test.mjs (toggle, size cap, multipart body, installer
  gates); tier-1 host-side firstboot_consume_restore cases in tests/stack/run.sh against a
  genuine backup archive (accept, bad passphrase, missing passphrase, oversize, malformed
  archive); a new KVM install-phase restore leg in tests/os/run.sh (real backup off a live
  machine, uploaded instead of the form, wallet + Tor identity proven restored) — unverified
  until the next battery run.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The verifier caught it: firstboot_consume_restore extracted to a temp
dir then cp -a'd the tree to /, but only checked config.json landed —
an absolute path, a .. component, or a symlink/hardlink member could
write outside the restore set. Modern tar refuses these, but the
destination is the filesystem root, so audit the member list and fail
closed regardless of tar version. Tier-1 cases cover a symlink and an
absolute-path member (both refused, live files untouched).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
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