diff --git a/nixos/doc/manual/release-notes/rl-2305.section.md b/nixos/doc/manual/release-notes/rl-2305.section.md index 801cd0c6dc8d..08aba92ffe27 100644 --- a/nixos/doc/manual/release-notes/rl-2305.section.md +++ b/nixos/doc/manual/release-notes/rl-2305.section.md @@ -322,3 +322,5 @@ In addition to numerous new and upgraded packages, this release has the followin - The option `services.prometheus.exporters.pihole.interval` does not exist anymore and has been removed. - `k3s` can now be configured with an EnvironmentFile for its systemd service, allowing secrets to be provided without ending up in the Nix Store. + +- `boot.initrd.luks.device.` has a new `tryEmptyPassphrase` option, this is useful for OEM's who need to install an encrypted disk with a future settable passphrase diff --git a/nixos/modules/system/boot/luksroot.nix b/nixos/modules/system/boot/luksroot.nix index cdb5d8bf3c26..8954c90812f9 100644 --- a/nixos/modules/system/boot/luksroot.nix +++ b/nixos/modules/system/boot/luksroot.nix @@ -158,6 +158,20 @@ let wait_target "header" ${dev.header} || die "${dev.header} is unavailable" ''} + try_empty_passphrase() { + ${if dev.tryEmptyPassphrase then '' + echo "Trying empty passphrase!" + echo "" | ${csopen} + cs_status=$? + if [ $cs_status -eq 0 ]; then + return 0 + else + return 1 + fi + '' else "return 1"} + } + + do_open_passphrase() { local passphrase @@ -212,13 +226,27 @@ let ${csopen} --key-file=${dev.keyFile} \ ${optionalString (dev.keyFileSize != null) "--keyfile-size=${toString dev.keyFileSize}"} \ ${optionalString (dev.keyFileOffset != null) "--keyfile-offset=${toString dev.keyFileOffset}"} + cs_status=$? + if [ $cs_status -ne 0 ]; then + echo "Key File ${dev.keyFile} failed!" + if ! try_empty_passphrase; then + ${if dev.fallbackToPassword then "echo" else "die"} "${dev.keyFile} is unavailable" + echo " - failing back to interactive password prompt" + do_open_passphrase + fi + fi else - ${if dev.fallbackToPassword then "echo" else "die"} "${dev.keyFile} is unavailable" - echo " - failing back to interactive password prompt" - do_open_passphrase + # If the key file never shows up we should also try the empty passphrase + if ! try_empty_passphrase; then + ${if dev.fallbackToPassword then "echo" else "die"} "${dev.keyFile} is unavailable" + echo " - failing back to interactive password prompt" + do_open_passphrase + fi fi '' else '' - do_open_passphrase + if ! try_empty_passphrase; then + do_open_passphrase + fi ''} } @@ -476,6 +504,7 @@ let preLVM = filterAttrs (n: v: v.preLVM) luks.devices; postLVM = filterAttrs (n: v: !v.preLVM) luks.devices; + stage1Crypttab = pkgs.writeText "initrd-crypttab" (lib.concatStringsSep "\n" (lib.mapAttrsToList (n: v: let opts = v.crypttabExtraOpts ++ optional v.allowDiscards "discard" @@ -483,6 +512,8 @@ let ++ optional (v.header != null) "header=${v.header}" ++ optional (v.keyFileOffset != null) "keyfile-offset=${toString v.keyFileOffset}" ++ optional (v.keyFileSize != null) "keyfile-size=${toString v.keyFileSize}" + ++ optional (v.keyFileTimeout != null) "keyfile-timeout=${builtins.toString v.keyFileTimeout}s" + ++ optional (v.tryEmptyPassphrase) "try-empty-password=true" ; in "${n} ${v.device} ${if v.keyFile == null then "-" else v.keyFile} ${lib.concatStringsSep "," opts}") luks.devices)); @@ -594,6 +625,25 @@ in ''; }; + tryEmptyPassphrase = mkOption { + default = false; + type = types.bool; + description = lib.mdDoc '' + If keyFile fails then try an empty passphrase first before + prompting for password. + ''; + }; + + keyFileTimeout = mkOption { + default = null; + example = 5; + type = types.nullOr types.int; + description = lib.mdDoc '' + The amount of time in seconds for a keyFile to appear before + timing out and trying passwords. + ''; + }; + keyFileSize = mkOption { default = null; example = 4096; @@ -889,6 +939,10 @@ in message = "boot.initrd.luks.devices..bypassWorkqueues is not supported for kernels older than 5.9"; } + { assertion = !config.boot.initrd.systemd.enable -> all (x: x.keyFileTimeout == null) (attrValues luks.devices); + message = "boot.initrd.luks.devices..keyFileTimeout is only supported for systemd initrd"; + } + { assertion = config.boot.initrd.systemd.enable -> all (dev: !dev.fallbackToPassword) (attrValues luks.devices); message = "boot.initrd.luks.devices..fallbackToPassword is implied by systemd stage 1."; } diff --git a/nixos/tests/all-tests.nix b/nixos/tests/all-tests.nix index ff2549395a0b..b7618aa00109 100644 --- a/nixos/tests/all-tests.nix +++ b/nixos/tests/all-tests.nix @@ -311,6 +311,7 @@ in { influxdb = handleTest ./influxdb.nix {}; initrd-network-openvpn = handleTest ./initrd-network-openvpn {}; initrd-network-ssh = handleTest ./initrd-network-ssh {}; + initrd-luks-empty-passphrase = handleTest ./initrd-luks-empty-passphrase.nix {}; initrdNetwork = handleTest ./initrd-network.nix {}; initrd-secrets = handleTest ./initrd-secrets.nix {}; initrd-secrets-changing = handleTest ./initrd-secrets-changing.nix {}; @@ -660,6 +661,7 @@ in { systemd-initrd-btrfs-raid = handleTest ./systemd-initrd-btrfs-raid.nix {}; systemd-initrd-luks-fido2 = handleTest ./systemd-initrd-luks-fido2.nix {}; systemd-initrd-luks-keyfile = handleTest ./systemd-initrd-luks-keyfile.nix {}; + systemd-initrd-luks-empty-passphrase = handleTest ./initrd-luks-empty-passphrase.nix { systemdStage1 = true; }; systemd-initrd-luks-password = handleTest ./systemd-initrd-luks-password.nix {}; systemd-initrd-luks-tpm2 = handleTest ./systemd-initrd-luks-tpm2.nix {}; systemd-initrd-modprobe = handleTest ./systemd-initrd-modprobe.nix {}; diff --git a/nixos/tests/initrd-luks-empty-passphrase.nix b/nixos/tests/initrd-luks-empty-passphrase.nix new file mode 100644 index 000000000000..41765a395ec6 --- /dev/null +++ b/nixos/tests/initrd-luks-empty-passphrase.nix @@ -0,0 +1,97 @@ +{ system ? builtins.currentSystem +, config ? {} +, pkgs ? import ../.. {inherit system config; } +, systemdStage1 ? false }: +import ./make-test-python.nix ({ lib, pkgs, ... }: let + + keyfile = pkgs.writeText "luks-keyfile" '' + MIGHAoGBAJ4rGTSo/ldyjQypd0kuS7k2OSsmQYzMH6TNj3nQ/vIUjDn7fqa3slt2 + gV6EK3TmTbGc4tzC1v4SWx2m+2Bjdtn4Fs4wiBwn1lbRdC6i5ZYCqasTWIntWn+6 + FllUkMD5oqjOR/YcboxG8Z3B5sJuvTP9llsF+gnuveWih9dpbBr7AgEC + ''; + +in { + name = "initrd-luks-empty-passphrase"; + + nodes.machine = { pkgs, ... }: { + virtualisation = { + emptyDiskImages = [ 512 ]; + useBootLoader = true; + useEFIBoot = true; + }; + + boot.loader.systemd-boot.enable = true; + boot.initrd.systemd = lib.mkIf systemdStage1 { + enable = true; + emergencyAccess = true; + }; + environment.systemPackages = with pkgs; [ cryptsetup ]; + + specialisation.boot-luks-wrong-keyfile.configuration = { + boot.initrd.luks.devices = lib.mkVMOverride { + cryptroot = { + device = "/dev/vdc"; + keyFile = "/etc/cryptroot.key"; + tryEmptyPassphrase = true; + fallbackToPassword = !systemdStage1; + }; + }; + virtualisation.bootDevice = "/dev/mapper/cryptroot"; + boot.initrd.secrets."/etc/cryptroot.key" = keyfile; + }; + + specialisation.boot-luks-missing-keyfile.configuration = { + boot.initrd.luks.devices = lib.mkVMOverride { + cryptroot = { + device = "/dev/vdc"; + keyFile = "/etc/cryptroot.key"; + tryEmptyPassphrase = true; + fallbackToPassword = !systemdStage1; + }; + }; + virtualisation.bootDevice = "/dev/mapper/cryptroot"; + }; + }; + + testScript = '' + # Encrypt key with empty key so boot should try keyfile and then fallback to empty passphrase + + + def grub_select_boot_luks_wrong_key_file(): + """ + Selects "boot-luks" from the GRUB menu + to trigger a login request. + """ + machine.send_monitor_command("sendkey down") + machine.send_monitor_command("sendkey down") + machine.send_monitor_command("sendkey ret") + + def grub_select_boot_luks_missing_key_file(): + """ + Selects "boot-luks" from the GRUB menu + to trigger a login request. + """ + machine.send_monitor_command("sendkey down") + machine.send_monitor_command("sendkey ret") + + # Create encrypted volume + machine.wait_for_unit("multi-user.target") + machine.succeed("echo "" | cryptsetup luksFormat /dev/vdc --batch-mode") + machine.succeed("bootctl set-default nixos-generation-1-specialisation-boot-luks-wrong-keyfile.conf") + machine.succeed("sync") + machine.crash() + + # Check if rootfs is on /dev/mapper/cryptroot + machine.wait_for_unit("multi-user.target") + assert "/dev/mapper/cryptroot on / type ext4" in machine.succeed("mount") + + # Choose boot-luks-missing-keyfile specialisation + machine.succeed("bootctl set-default nixos-generation-1-specialisation-boot-luks-missing-keyfile.conf") + machine.succeed("sync") + machine.crash() + + # Check if rootfs is on /dev/mapper/cryptroot + machine.wait_for_unit("multi-user.target") + assert "/dev/mapper/cryptroot on / type ext4" in machine.succeed("mount") + ''; +})