Skip to main content

max / alloy

Let the passphrase be typed when the TPM will not open the disk `--block-setup tpm2-luks` writes `luks.options=tpm2-device=auto,headless=true`, and `headless=true` means never prompt. When the TPM does not satisfy — a cleared TPM, a replaced board, the disk read in another machine, which is the entire case enroll_plan exists for — systemd-cryptsetup fails the unit instead of asking, and the boot ends at "Failed to start systemd-cryptsetup@luks-...". So the installer enrolled a passphrase and a recovery phrase, asserted in acceptance_checks that the header has a slot that is not the TPM's, told the user in the wizard that the passphrase is "asked for at boot only when the TPM cannot open the disk", and then shipped a machine where none of that could happen. The first encrypted install that ever completed produced exactly that machine. Both halves measured on the same disk: the wizard's passphrase opens the volume from another machine, and deleting this one option from the boot entry, with nothing else changed, makes the same disk prompt, unlock, and reach the greeter. --karg-delete does reach a karg bootc adds during its own install, which was the open question. Verified by reading the installed BLS entry: it carries `luks.options=tpm2-device=auto` and nothing else, no leftover and no duplicate. The TPM is still tried first and still opens the disk unattended where it answers. Both strings are named, because the delete matches an argument exactly and so depends on bootc's wording. A test derives the replacement from the deleted string, since that is the pair whose drift would be silent.
Author: Max Johnson <me@maxj.phd> · 2026-08-10 01:51 UTC
Signed with PGP, not checked
Commit: 54c03459f2805e64426f7ebfc0ea1b0845583bb1
Parent: 8bc957b
1 file changed, +94 insertions, -2 deletions
@@ -626,6 +626,27 @@
626 626 .arg(partition)
627 627 }
628 628
629 + /// The kernel command line `--block-setup tpm2-luks` writes, verbatim.
630 + ///
631 + /// Read off an installed disk's boot entry in a VM 2026-08-09, not from bootc's
632 + /// source, so it is a fact about the bootc being shipped rather than about the
633 + /// one being read. It is deleted whole and replaced by [`LUKS_PROMPTING_KARG`];
634 + /// see the deploy stage in [`install_plan`] for why.
635 + ///
636 + /// **If bootc changes this string the delete stops matching and stops saying
637 + /// so.** `--karg-delete` is content with removing nothing. The install would
638 + /// then carry both this argument and the replacement, and go back to producing
639 + /// machines that cannot be unlocked by hand. That is the cost of the approach
640 + /// and the reason the string is stated once, here, rather than inline.
641 + const LUKS_HEADLESS_KARG: &str = "luks.options=tpm2-device=auto,headless=true";
642 +
643 + /// The same argument with `headless=true` gone, which is the whole change.
644 + ///
645 + /// The TPM is still tried first and still opens the disk unattended on a
646 + /// machine whose TPM answers. What changes is only what happens when it does
647 + /// not: a prompt, rather than a failed unit.
648 + const LUKS_PROMPTING_KARG: &str = "luks.options=tpm2-device=auto";
649 +
629 650 /// The filesystem label bootc gives the separate `/boot` partition.
630 651 ///
631 652 /// bootc creates that partition only when the root is encrypted: the bootloader
@@ -2620,6 +2641,34 @@
2620 2641 // is a permanently unreadable disk.
2621 2642 if encrypt {
2622 2643 install = install.args(["--block-setup", "tpm2-luks"]);
2644 + // And then undo the half of that flag which makes the two
2645 + // enrollments below unreachable.
2646 + //
2647 + // `--block-setup tpm2-luks` writes the kernel command line
2648 + // [`LUKS_HEADLESS_KARG`], and `headless=true` means never
2649 + // prompt. When the TPM does not satisfy — a cleared TPM, a
2650 + // replaced board, the disk read in another machine, which is
2651 + // the entire case [`enroll_plan`] exists for —
2652 + // systemd-cryptsetup fails the unit instead of asking, and the
2653 + // boot is over with no way to type either secret.
2654 + //
2655 + // That is the invariant [`acceptance_checks`] already asserts,
2656 + // undone one argument later: the install refuses to finish when
2657 + // the header has no slot but the TPM's, and then shipped a
2658 + // machine where that slot could never be used. Measured in a VM
2659 + // 2026-08-09: the install completes, the passphrase opens the
2660 + // volume from another machine, and the machine itself stops at
2661 + // "Failed to start systemd-cryptsetup@luks-...". Deleting this
2662 + // one option from the boot entry, and nothing else, makes the
2663 + // same disk prompt, unlock and reach the greeter.
2664 + //
2665 + // Deleted whole and rewritten rather than edited, because
2666 + // --karg-delete matches a kernel argument exactly and
2667 + // `luks.options` is one argument with a comma-separated value.
2668 + // That makes it bootc's exact string this depends on, which is
2669 + // why [`LUKS_HEADLESS_KARG`] is named and stated once.
2670 + install = install.args(["--karg-delete", LUKS_HEADLESS_KARG]);
2671 + install = install.args(["--karg", LUKS_PROMPTING_KARG]);
2623 2672 }
2624 2673 install.arg(disk)
2625 2674 }),
@@ -5940,6 +5989,45 @@
5940 5989 assert!(Stage::Run(unmount(TARGET_MOUNT)).display().contains("-R"));
5941 5990 }
5942 5991
5992 + // The replacement has to be the deleted argument minus `headless=true` and
5993 + // nothing else. Written as a derivation rather than as two literals: two
5994 + // hand-written strings can drift apart, and the drift that matters here is
5995 + // silent, since a delete that no longer matches removes nothing and says
5996 + // nothing.
5997 + #[test]
5998 + fn the_prompting_karg_is_the_headless_one_with_the_flag_removed() {
5999 + assert_eq!(
6000 + LUKS_HEADLESS_KARG.replace(",headless=true", ""),
6001 + LUKS_PROMPTING_KARG,
6002 + );
6003 + }
6004 +
6005 + // An encrypted deploy has to both delete and replace. Deleting alone leaves
6006 + // the volume with no tpm2-device option and no unattended unlock; replacing
6007 + // alone leaves both arguments on the command line.
6008 + #[test]
6009 + fn the_encrypted_deploy_replaces_the_headless_luks_karg() {
6010 + let shown = deploy_line(true);
6011 + assert!(
6012 + shown.contains(&format!("--karg-delete {LUKS_HEADLESS_KARG}")),
6013 + "{shown}",
6014 + );
6015 + assert!(
6016 + shown.contains(&format!("--karg {LUKS_PROMPTING_KARG}")),
6017 + "{shown}"
6018 + );
6019 + }
6020 +
6021 + // And touches neither when there is no LUKS volume to unlock. The karg
6022 + // belongs to --block-setup, so an unencrypted install never carries it and
6023 + // a delete against it would be noise on the summary the user reads.
6024 + #[test]
6025 + fn a_plain_deploy_says_nothing_about_luks_kargs() {
6026 + let shown = deploy_line(false);
6027 + assert!(!shown.contains("--karg"), "{shown}");
6028 + assert!(!shown.contains("luks.options"), "{shown}");
6029 + }
6030 +
5943 6031 // Both slots bootc wipes, enrolled against the container rather than the
5944 6032 // mapper device: the header is on the partition.
5945 6033 #[test]
@@ -6033,12 +6121,16 @@
6033 6121 // --block-setup because the fixture takes the offered default and
6034 6122 // encrypts. It is on the same line and there for the same reason: it
6035 6123 // decides how the machine behaves for its whole life, and it is not
6036 - // reversible after this command.
6124 + // reversible after this command. The two karg arguments after it are
6125 + // that flag's correction, and belong on the same line for the same
6126 + // reason: they decide whether a machine whose TPM stops answering can
6127 + // be unlocked by hand at all.
6037 6128 assert_eq!(
6038 6129 shown[0],
6039 6130 format!(
6040 6131 "bootc install to-disk --wipe --target-imgref {UPDATE_IMAGE} \
6041 - --block-setup tpm2-luks /dev/sda"
6132 + --block-setup tpm2-luks --karg-delete {LUKS_HEADLESS_KARG} \
6133 + --karg {LUKS_PROMPTING_KARG} /dev/sda"
6042 6134 ),
6043 6135 );
6044 6136 assert_eq!(shown[1], "udevadm settle");