wip: feat(luks): passphraseless reboots via kexec #1009

Draft
lytedev wants to merge 2 commits from propose/kexec-luks-reboot into main
Owner

Reboot the seven LUKS hosts (babyflip, fennec, flab, flipflop, flipflop2, foxtrot, thinker) without walking to the console, following https://www.bevuta.com/en/blog/passphraseless-reboots-using-kexec/

Rationale, deviations from the article, and the security trade-off are in the commit message and lib/doc/passphraseless-kexec-reboots.md; not repeated here.

Operator steps after deploy, per host: sudo kexec-luks-enroll once, then sudo kexec-reboot. The module is inert until enrolled.

Verification: nix build .#checks.x86_64-linux.kexec-luks-reboot — a LUKS-root VM must still prompt on a cold boot, then kexec-reboot must unlock with no console input and leave no key slot 31.

Reboot the seven LUKS hosts (babyflip, fennec, flab, flipflop, flipflop2, foxtrot, thinker) without walking to the console, following https://www.bevuta.com/en/blog/passphraseless-reboots-using-kexec/ Rationale, deviations from the article, and the security trade-off are in the commit message and `lib/doc/passphraseless-kexec-reboots.md`; not repeated here. Operator steps after deploy, per host: `sudo kexec-luks-enroll` once, then `sudo kexec-reboot`. The module is inert until enrolled. Verification: `nix build .#checks.x86_64-linux.kexec-luks-reboot` — a LUKS-root VM must still prompt on a cold boot, then kexec-reboot must unlock with no console input and leave no key slot 31.
feat(luks): passphraseless reboots via kexec
All checks were successful
/ check-format (push) Successful in 11s
/ build (push) Successful in 10m4s
997347a481
Every encrypted host stops at a console passphrase prompt on reboot,
which makes a remote reboot a trip to the machine. kexec hands over to
the next kernel without going through firmware, so an initramfs we build
in RAM survives the transition: prepare-kexec enrolls a single-use key in
LUKS slot 31, prepends a cpio carrying it to the system initramfs, and
loads that. An initrd unit kills slot 31 after sysroot.mount, so a
captured kexec image unlocks nothing after the next boot.

Adapted from https://www.bevuta.com/en/blog/passphraseless-reboots-using-kexec/
with two deviations:

- The article authorises luksAddKey with the LUKS passphrase read from a
  file. Here a dedicated 256-byte enrollment key (slot 30, mode 0400 on
  the encrypted volume) does that job, minted by kexec-luks-enroll, so no
  human-typed passphrase is ever stored. Deriving the key instead from
  the running mapping is not an option: LUKS2 keeps the volume key as an
  unreadable logon key in the kernel keyring, so dmsetup --showkeys
  yields no usable material.
- The cpio is prepended rather than appended. The kernel unpacks
  concatenated initramfs segments in order and only a leading
  uncompressed archive ahead of a compressed one is well-trodden (it is
  how microcode is loaded); appending to a zstd segment relies on
  alignment we do not control.

The behaviour worth being nervous about is the cold-boot path, since the
crypttab key file is absent then and every encrypted host depends on
falling back to the prompt. packages/checks/kexec-luks-reboot.nix
asserts it rather than arguing it: a LUKS-root VM must still prompt on a
cold boot, then kexec-reboot must unlock silently and leave no slot 31.

The enrollment key is a lasting unlock capability stored on the disk, so
the module is inert until an operator runs kexec-luks-enroll on a host;
the trade-off is spelled out in lib/doc/passphraseless-kexec-reboots.md.

Enabled from the encrypted layouts in lib/disko rather than from the
module: the module writes boot.initrd.luks.devices, so defaulting
enable from that attrset is infinite recursion.
fix(luks): make the kexec key actually reach cryptsetup
All checks were successful
/ check-format (push) Successful in 10s
/ build (push) Successful in 7m18s
0fe35cdf9a
The first end-to-end VM run booted the kexec initrd straight into a
passphrase prompt. Three distinct faults, each found by a run:

- The appended archive never landed. Writing an uncompressed cpio
  directly to /etc does not survive; nixpkgs already solves this in
  append-initrd-secrets (nixos/modules/system/boot/stage-1.nix), which
  appends a cpio compressed with the initrd own compressor, staged under
  /.initrd-secrets, and copies it into place from an initrd unit. Follow
  that shape exactly, reusing the config compressorExecutableFunction so
  the compression cannot drift from the initrd it is appended to.

- The copy-into-place unit ran too late. Ordering before
  cryptsetup-pre.target is vacuous: that target is not pulled into the
  initrd transaction, so the unit ran after the volume had already been
  unlocked interactively. Order before systemd-cryptsetup@<name>.service
  itself. It also logs which branch it took, so a future failure is
  diagnosable from the console without another round trip.

- The VM test was proving nothing about the crypttab. qemu-vm.nix
  defines boot.initrd.luks.devices with mkVMOverride, so the test cannot
  both set the device and keep the module contribution to that option --
  the generated crypttab was "cryptroot /dev/vdb -", with no key file at
  all, while the real hosts were correct throughout. The test now
  restates keyFile at the same priority, and a second check asserts the
  line the module generates for thinker, so the restatement cannot
  silently drift from production.

The VM test now passes end to end: cold boot still prompts, the kexec
reboot unlocks with no console input, and slot 31 is gone afterwards.
lytedev changed title from feat(luks): passphraseless reboots via kexec to wip: feat(luks): passphraseless reboots via kexec 2026-08-20 13:55:46 -05:00
All checks were successful
/ check-format (push) Successful in 10s
Required
Details
/ build (push) Successful in 7m18s
Required
Details
This pull request is marked as a work in progress.
This branch is out-of-date with the base branch
View command line instructions

Checkout

From your project repository, check out a new branch and test the changes.
git fetch -u origin propose/kexec-luks-reboot:propose/kexec-luks-reboot
git switch propose/kexec-luks-reboot
Sign in to join this conversation.
No reviewers
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
lytedev/nix!1009
No description provided.