From b5cb2cc43f95415f9c9fc997eb5a5874af7d223d Mon Sep 17 00:00:00 2001 From: Kevin Cotrone Date: Mon, 16 Feb 2026 22:31:38 -0800 Subject: [PATCH] clevis: add boot.initrd.clevisLuksAskpass module Enable upstream clevis-luks-askpass systemd units in initrd for automatic LUKS unlocking using clevis bindings stored in LUKS headers. --- .../manual/release-notes/rl-2605.section.md | 2 + nixos/modules/module-list.nix | 1 + .../system/boot/clevis-luks-askpass.nix | 84 +++++++++++++++++++ nixos/tests/installer.nix | 72 +++++++++++++++- pkgs/by-name/cl/clevis/package.nix | 34 ++++++++ 5 files changed, 189 insertions(+), 4 deletions(-) create mode 100644 nixos/modules/system/boot/clevis-luks-askpass.nix diff --git a/nixos/doc/manual/release-notes/rl-2605.section.md b/nixos/doc/manual/release-notes/rl-2605.section.md index f04b0e03cf91..5c6325a51065 100644 --- a/nixos/doc/manual/release-notes/rl-2605.section.md +++ b/nixos/doc/manual/release-notes/rl-2605.section.md @@ -91,6 +91,8 @@ - [nohang](https://github.com/hakavlad/nohang), a daemon for Linux that prevents out of memory (OOM) situations from affecting system responsiveness. Available as [services.nohang](#opt-services.nohang.enable) +- [clevis-luks-askpass](https://github.com/latchset/clevis), automatic LUKS unlocking in initrd using clevis token bindings stored in LUKS headers. Available as [boot.initrd.clevisLuksAskpass](#opt-boot.initrd.clevisLuksAskpass.enable). + - [bentopdf](https://github.com/alam00000/bentopdf), a privacy-first PDF toolkit running completely in-browser. Available as [services.bentopdf](#opt-services.bentopdf.enable). - [hyprwhspr-rs](https://github.com/better-slop/hyprwhspr-rs), a keybind activated speech-to-text voice dictation utility built for use with Hyprland. Available as `services.hyprwhspr-rs` diff --git a/nixos/modules/module-list.nix b/nixos/modules/module-list.nix index 082c26f90560..4c307a32de9e 100644 --- a/nixos/modules/module-list.nix +++ b/nixos/modules/module-list.nix @@ -1892,6 +1892,7 @@ ./system/activation/switchable-system.nix ./system/activation/top-level.nix ./system/boot/binfmt.nix + ./system/boot/clevis-luks-askpass.nix ./system/boot/clevis.nix ./system/boot/emergency-mode.nix ./system/boot/grow-partition.nix diff --git a/nixos/modules/system/boot/clevis-luks-askpass.nix b/nixos/modules/system/boot/clevis-luks-askpass.nix new file mode 100644 index 000000000000..1d9685ff889c --- /dev/null +++ b/nixos/modules/system/boot/clevis-luks-askpass.nix @@ -0,0 +1,84 @@ +{ + config, + lib, + pkgs, + ... +}: +let + cfg = config.boot.initrd.clevisLuksAskpass; +in +{ + options = { + boot.initrd.clevisLuksAskpass.enable = lib.mkEnableOption '' + clevis-luks-askpass in initrd. + + Watches for systemd password requests during boot and answers them + using clevis tokens bound to LUKS headers. Runs in parallel with + the interactive password prompt. If clevis cannot unlock a device + (tang unreachable, no binding, etc.) the user can still type the + passphrase. + + Prerequisites: + - Bind clevis to each LUKS device: + clevis luks bind -d /dev/xxx tang '{"url":"..."}' + - Configure networking in the initrd so tang servers are reachable + ''; + + boot.initrd.clevisLuksAskpass.package = lib.mkPackageOption pkgs "clevis" { }; + + boot.initrd.clevisLuksAskpass.useTang = lib.mkOption { + description = "Whether the Clevis headers used to decrypt the devices uses a Tang server as a pin."; + default = false; + type = lib.types.bool; + }; + }; + + config = lib.mkIf cfg.enable { + assertions = [ + { + assertion = config.boot.initrd.systemd.enable; + message = "clevis-luks-askpass requires boot.initrd.systemd.enable = true"; + } + ]; + + warnings = + if + cfg.useTang && !config.boot.initrd.network.enable && !config.boot.initrd.systemd.network.enable + then + [ "In order to use a Tang pinned secret you must configure networking in initrd" ] + else + [ ]; + + boot.initrd.systemd = { + # Install upstream clevis-luks-askpass.path and clevis-luks-askpass.service into the initrd + packages = [ cfg.package ]; + + storePaths = [ + cfg.package + "${pkgs.systemd}/lib/systemd/systemd-reply-password" + "${pkgs.jose}/bin/jose" + "${pkgs.curl}/bin/curl" + "${pkgs.cryptsetup}/bin/cryptsetup" + "${pkgs.gnused}/bin/sed" + "${pkgs.gnugrep}/bin/grep" + "${pkgs.gawk}/bin/gawk" + "${pkgs.coreutils}/bin/cat" + "${pkgs.luksmeta}/bin/luksmeta" + "${pkgs.tpm2-tools}/bin/tpm2_createprimary" + "${pkgs.tpm2-tools}/bin/tpm2_flushcontext" + "${pkgs.tpm2-tools}/bin/tpm2_load" + "${pkgs.tpm2-tools}/bin/tpm2_unseal" + ]; + + # This is in the [Install] section of clevis-luks-askpass.path but that's not processed in nixos so we add it here + paths.clevis-luks-askpass = { + wantedBy = [ "cryptsetup.target" ]; + }; + + services.clevis-luks-askpass = { + wants = lib.optional cfg.useTang "network-online.target"; + after = lib.optional cfg.useTang "network-online.target"; + }; + }; + }; +} diff --git a/nixos/tests/installer.nix b/nixos/tests/installer.nix index b97f45739f7e..f1e1788127cb 100644 --- a/nixos/tests/installer.nix +++ b/nixos/tests/installer.nix @@ -21,6 +21,7 @@ let forceGrubReinstallCount ? 0, withTestInstrumentation ? true, clevisTest, + clevisAskpassTest ? false, }: pkgs.writeText "configuration.nix" '' { config, lib, pkgs, modulesPath, ... }: @@ -73,7 +74,12 @@ let boot.kernelParams = [ "console=tty0" "ip=192.168.1.1:::255.255.255.0::eth1:none" ]; boot.initrd = { availableKernelModules = [ "tpm_tis" ]; - clevis = { enable = true; useTang = true; }; + ${ + if clevisAskpassTest then + "clevisLuksAskpass = { enable = true; useTang = true; };" + else + "clevis = { enable = true; useTang = true; };" + } network.enable = true; }; ''} @@ -108,6 +114,7 @@ let testFlakeSwitch, testByAttrSwitch, clevisTest, + clevisAskpassTest ? false, clevisFallbackTest, disableFileSystems, }: @@ -190,6 +197,7 @@ let grubUseEfi extraConfig clevisTest + clevisAskpassTest ; } }", @@ -197,11 +205,20 @@ let ) installer.copy_from_host("${pkgs.writeText "secret" "secret"}", "/mnt/etc/nixos/secret") - ${optionalString clevisTest '' - with subtest("Create the Clevis secret with Tang"): + ${optionalString (clevisTest && !clevisAskpassTest) + '' + with subtest("Create the Clevis secret with Tang"): + installer.systemctl("start network-online.target") + installer.wait_for_unit("network-online.target") + installer.succeed('echo -n password | clevis encrypt sss \'{"t": 2, "pins": {"tpm2": {}, "tang": {"url": "http://192.168.1.2"}}}\' -y > /mnt/etc/nixos/clevis-secret.jwe')'' + } + + ${optionalString clevisAskpassTest '' + with subtest("Bind Clevis to LUKS header"): installer.systemctl("start network-online.target") installer.wait_for_unit("network-online.target") - installer.succeed('echo -n password | clevis encrypt sss \'{"t": 2, "pins": {"tpm2": {}, "tang": {"url": "http://192.168.1.2"}}}\' -y > /mnt/etc/nixos/clevis-secret.jwe')''} + installer.succeed("echo -n password | clevis luks bind -y -k - -d /dev/vda3 sss '{\"t\": 2, \"pins\": {\"tpm2\": {}, \"tang\": {\"url\": \"http://192.168.1.2\"}}}'") + ''} ${optionalString clevisFallbackTest '' with subtest("Shutdown Tang to check fallback to interactive prompt"): @@ -283,6 +300,7 @@ let grubUseEfi extraConfig clevisTest + clevisAskpassTest ; forceGrubReinstallCount = 1; } @@ -316,6 +334,7 @@ let grubUseEfi extraConfig clevisTest + clevisAskpassTest ; forceGrubReinstallCount = 2; } @@ -384,6 +403,7 @@ let grubUseEfi extraConfig clevisTest + clevisAskpassTest ; forceGrubReinstallCount = 1; withTestInstrumentation = false; @@ -482,6 +502,7 @@ let grubUseEfi extraConfig clevisTest + clevisAskpassTest ; forceGrubReinstallCount = 1; } @@ -518,6 +539,7 @@ let grubUseEfi extraConfig clevisTest + clevisAskpassTest ; forceGrubReinstallCount = 1; withTestInstrumentation = false; @@ -637,6 +659,7 @@ let testFlakeSwitch ? false, testByAttrSwitch ? false, clevisTest ? false, + clevisAskpassTest ? false, clevisFallbackTest ? false, disableFileSystems ? false, selectNixPackage ? pkgs: pkgs.nixVersions.stable, @@ -820,6 +843,7 @@ let testFlakeSwitch testByAttrSwitch clevisTest + clevisAskpassTest clevisFallbackTest disableFileSystems ; @@ -1044,6 +1068,43 @@ let ''; }; + mkClevisLuksAskpassTest = + { + fallback ? false, + }: + makeInstallerTest "clevis-luks-askpass${optionalString fallback "-fallback"}" { + clevisTest = true; + clevisAskpassTest = true; + clevisFallbackTest = fallback; + enableOCR = fallback; + extraInstallerConfig = { + environment.systemPackages = with pkgs; [ clevis ]; + }; + createPartitions = '' + installer.succeed( + "flock /dev/vda parted --script /dev/vda -- mklabel msdos" + + " mkpart primary ext2 1M 100MB" + + " mkpart primary linux-swap 100M 1024M" + + " mkpart primary 1024M -1s", + "udevadm settle", + "mkswap /dev/vda2 -L swap", + "swapon -L swap", + "modprobe dm_mod dm_crypt", + "echo -n password | cryptsetup luksFormat -q /dev/vda3 -", + "echo -n password | cryptsetup luksOpen --key-file - /dev/vda3 crypt-root", + "mkfs.ext3 -L nixos /dev/mapper/crypt-root", + "mount LABEL=nixos /mnt", + "mkfs.ext3 -L boot /dev/vda1", + "mkdir -p /mnt/boot", + "mount LABEL=boot /mnt/boot", + "udevadm settle") + ''; + postBootCommands = optionalString fallback '' + target.wait_for_text("Please enter") + target.send_chars("password\n") + ''; + }; + mkClevisZfsTest = { fallback ? false, @@ -1741,6 +1802,9 @@ in }; } // optionalAttrs systemdStage1 { + clevisLuksAskpass = mkClevisLuksAskpassTest { }; + clevisLuksAskpassFallback = mkClevisLuksAskpassTest { fallback = true; }; + stratisRoot = makeInstallerTest "stratisRoot" { createPartitions = '' installer.succeed( diff --git a/pkgs/by-name/cl/clevis/package.nix b/pkgs/by-name/cl/clevis/package.nix index d3358cb0df87..944da436ca71 100644 --- a/pkgs/by-name/cl/clevis/package.nix +++ b/pkgs/by-name/cl/clevis/package.nix @@ -5,6 +5,7 @@ cryptsetup, curl, fetchFromGitHub, + gawk, gnugrep, gnused, jansson, @@ -17,6 +18,7 @@ nixosTests, pkg-config, stdenv, + systemd, tpm2-tools, }: @@ -52,6 +54,7 @@ stdenv.mkDerivation (finalAttrs: { jose libpwquality luksmeta + systemd tpm2-tools ]; @@ -74,11 +77,26 @@ stdenv.mkDerivation (finalAttrs: { # The xargs command is a little bit convoluted because a simpler version would # be vulnerable to code injection. This hint is a courtesy of Stack Exchange: # https://unix.stackexchange.com/a/267438 + # + # Meson's find_program only searches paths relative to the build prefix + # and /usr/lib, neither of which contain systemd-reply-password in the + # nix store. Prepend the actual nix store path so meson can find it. + # + # The systemd pkgconfig variable 'systemdsystemunitdir' points into the + # read-only systemd store path. Redirect unit installation to $out. postPatch = '' for f in $(find src/ -type f -print0 |\ xargs -0 -I@ sh -c 'grep -q "/bin/cat" "$1" && echo "$1"' sh @); do substituteInPlace "$f" --replace-fail '/bin/cat' '${lib.getExe' coreutils "cat"}' done + + substituteInPlace src/luks/systemd/meson.build \ + --replace-fail "sd_reply_pass = find_program(" \ + "sd_reply_pass = find_program('${systemd}/lib/systemd/systemd-reply-password'," + + substituteInPlace src/luks/systemd/meson.build \ + --replace-fail "unitdir = systemd.get_pkgconfig_variable('systemdsystemunitdir')" \ + "unitdir = join_paths(get_option('prefix'), 'lib', 'systemd', 'system')" ''; # We wrap the main clevis binary entrypoint but not the sub-binaries. @@ -94,10 +112,23 @@ stdenv.mkDerivation (finalAttrs: { luksmeta tpm2-tools ]; + askpassPath = [ + coreutils + cryptsetup + curl + gawk + gnugrep + gnused + jose + luksmeta + ]; in '' wrapProgram $out/bin/clevis \ --prefix PATH ':' "${lib.makeBinPath includeIntoPath}:${placeholder "out"}/bin" + + wrapProgram $out/libexec/clevis-luks-askpass \ + --prefix PATH ':' "${lib.makeBinPath askpassPath}:${placeholder "out"}/bin" ''; passthru.tests = { @@ -111,6 +142,9 @@ stdenv.mkDerivation (finalAttrs: { ; clevisLuksSystemdStage1 = nixosTests.installer-systemd-stage-1.clevisLuks; clevisLuksFallbackSystemdStage1 = nixosTests.installer-systemd-stage-1.clevisLuksFallback; + clevisLuksAskpassSystemdStage1 = nixosTests.installer-systemd-stage-1.clevisLuksAskpass; + clevisLuksAskpassFallbackSystemdStage1 = + nixosTests.installer-systemd-stage-1.clevisLuksAskpassFallback; clevisZfsSystemdStage1 = nixosTests.installer-systemd-stage-1.clevisZfs; clevisZfsFallbackSystemdStage1 = nixosTests.installer-systemd-stage-1.clevisZfsFallback; };