Problem
Today's LUKS header backup workflow is a 3-step dance:
braid add, braid replace, braid enroll, and braid recover write a local copy of the LUKS header to /var/lib/braid/luks-headers/<mapper>.luksheader.
- The user copies the file off-system (USB, second machine, cloud key storage).
- The user deletes the local copy.
braid status and the TUI emit an advisory (LUKS header backups exist in ... -- copy offsite and delete local copies) for as long as a local copy persists, because keeping it on the same machine defeats the off-system backup model.
The local file is documented in docs/luks-unlock.md:104-122 as a "transient byproduct," not the intended backup target. It exists only because no command currently accepts a user-supplied off-system destination.
The result is a UX smell: the product warns the user about an artifact it just created. A workflow with two manual steps after the command (copy, delete) is more error-prone than a workflow with zero. The advisory loop in cli/src/luks.rs:934-951 (header_backup_advisories_in), wired into cli/src/status.rs:345 and the TUI, exists only to nag users through that dance.
Current callsites
All call luks::backup_luks_header / backup_luks_header_to against the default state-paths target:
cli/src/enroll_key_file.rs:306 -- in apply_enrollment
cli/src/add.rs:905 -- after fresh-disk LUKS format + keyfile enroll
cli/src/replace.rs:488 -- after the new disk's LUKS format + keyfile enroll
cli/src/recover.rs:2179 and cli/src/recover.rs:2467 -- post-recovery rebuild paths
Proposed direction
Sketch only -- pick one or combine:
- Option A:
--backup-to <path> flag on add, replace, and enroll to direct the header backup straight to a user-supplied off-system path (mounted USB, secondary disk, etc.). Default to today's local path if the flag is absent so callers can opt in incrementally.
- Option B: dedicated
braid backup-headers <dir> primitive that produces backups for all current pool members on demand. Pairs cleanly with future workflows like keyfile rotation or "audit my headers."
With either, the long-term goal is to retire the local default at /var/lib/braid/luks-headers/ -- which then retires the advisory in cli/src/luks.rs:934-951, the status surface at cli/src/status.rs:345, and the TUI advisory band.
Open questions
- Atomicity.
add, replace, and enroll mutate LUKS slots before the backup runs. If a user-supplied off-system target is unavailable mid-command (USB unplugged, mount failed), do we fail, fall back to a local default, or skip the backup with a warning? Today's behavior is ?-propagate, which leaves the keyfile enrolled but the operation reported as failed.
- Plan/dry-run preview.
compile_enroll_steps in cli/src/enroll_key_file.rs:354-395 lists the backup target in the dry-run plan. A user-supplied path has to be resolvable at planning time so the preview can show it.
- Mount-point invariant. Off-system targets on USB devices should probably reuse the mount-point validation pattern already in
validate_generated_keyfile_target (cli/src/enroll_key_file.rs:546-582) -- the same "is this a real mount, not the host root falling through?" guard.
- Messaging invariant interaction.
docs/luks-unlock.md:104-122 forbids user-visible recovery guidance from referencing local .luksheader files. Removing the local default makes that invariant trivially satisfied; the messaging code in doctor, status, and unlock errors should be re-audited once the local path is gone.
Non-goals
- Don't add backwards-compatibility shims for the local default once it's removed. braid is unreleased.
- Don't change the failure semantics of
luks::backup_luks_header_to itself in this work -- a separate, smaller change can improve the per-failure error message while this larger refactor is still pending.
Why not now
Feature work, not a fix. Only worth doing if the advisory churn is real friction in practice. Filed for tracking; surfaced during a verify-issue review of an enroll-then-backup-failure code-review finding that misread the documented "transient byproduct" model. The narrow fix the finding proposed (re-create the local backup on AlreadyEnrolled re-runs) was rejected because it would re-create the file the user is documented to delete -- this larger refactor is the right shape if it's worth doing at all.
Problem
Today's LUKS header backup workflow is a 3-step dance:
braid add,braid replace,braid enroll, andbraid recoverwrite a local copy of the LUKS header to/var/lib/braid/luks-headers/<mapper>.luksheader.braid statusand the TUI emit an advisory (LUKS header backups exist in ... -- copy offsite and delete local copies) for as long as a local copy persists, because keeping it on the same machine defeats the off-system backup model.The local file is documented in
docs/luks-unlock.md:104-122as a "transient byproduct," not the intended backup target. It exists only because no command currently accepts a user-supplied off-system destination.The result is a UX smell: the product warns the user about an artifact it just created. A workflow with two manual steps after the command (copy, delete) is more error-prone than a workflow with zero. The advisory loop in
cli/src/luks.rs:934-951(header_backup_advisories_in), wired intocli/src/status.rs:345and the TUI, exists only to nag users through that dance.Current callsites
All call
luks::backup_luks_header/backup_luks_header_toagainst the default state-paths target:cli/src/enroll_key_file.rs:306-- inapply_enrollmentcli/src/add.rs:905-- after fresh-disk LUKS format + keyfile enrollcli/src/replace.rs:488-- after the new disk's LUKS format + keyfile enrollcli/src/recover.rs:2179andcli/src/recover.rs:2467-- post-recovery rebuild pathsProposed direction
Sketch only -- pick one or combine:
--backup-to <path>flag onadd,replace, andenrollto direct the header backup straight to a user-supplied off-system path (mounted USB, secondary disk, etc.). Default to today's local path if the flag is absent so callers can opt in incrementally.braid backup-headers <dir>primitive that produces backups for all current pool members on demand. Pairs cleanly with future workflows like keyfile rotation or "audit my headers."With either, the long-term goal is to retire the local default at
/var/lib/braid/luks-headers/-- which then retires the advisory incli/src/luks.rs:934-951, the status surface atcli/src/status.rs:345, and the TUI advisory band.Open questions
add,replace, andenrollmutate LUKS slots before the backup runs. If a user-supplied off-system target is unavailable mid-command (USB unplugged, mount failed), do we fail, fall back to a local default, or skip the backup with a warning? Today's behavior is?-propagate, which leaves the keyfile enrolled but the operation reported as failed.compile_enroll_stepsincli/src/enroll_key_file.rs:354-395lists the backup target in the dry-run plan. A user-supplied path has to be resolvable at planning time so the preview can show it.validate_generated_keyfile_target(cli/src/enroll_key_file.rs:546-582) -- the same "is this a real mount, not the host root falling through?" guard.docs/luks-unlock.md:104-122forbids user-visible recovery guidance from referencing local.luksheaderfiles. Removing the local default makes that invariant trivially satisfied; the messaging code indoctor,status, andunlockerrors should be re-audited once the local path is gone.Non-goals
luks::backup_luks_header_toitself in this work -- a separate, smaller change can improve the per-failure error message while this larger refactor is still pending.Why not now
Feature work, not a fix. Only worth doing if the advisory churn is real friction in practice. Filed for tracking; surfaced during a
verify-issuereview of an enroll-then-backup-failure code-review finding that misread the documented "transient byproduct" model. The narrow fix the finding proposed (re-create the local backup onAlreadyEnrolledre-runs) was rejected because it would re-create the file the user is documented to delete -- this larger refactor is the right shape if it's worth doing at all.