diff --git a/nixos/doc/manual/release-notes/rl-2411.section.md b/nixos/doc/manual/release-notes/rl-2411.section.md index 1761715c8e5b..0d668459f09b 100644 --- a/nixos/doc/manual/release-notes/rl-2411.section.md +++ b/nixos/doc/manual/release-notes/rl-2411.section.md @@ -35,6 +35,9 @@ Users that want to keep PulseAudio will want to set `services.pipewire.enable = false;` and `hardware.pulseaudio.enable = true;`. There is currently no plan to fully deprecate and remove PulseAudio, however, PipeWire should generally be preferred for new installs. +- Support for mounting filesystems from block devices protected with [dm-verity](https://docs.kernel.org/admin-guide/device-mapper/verity.html) + was added through the `boot.initrd.systemd.dmVerity` option. + ## New Modules {#sec-release-24.11-new-modules} - [TaskChampion Sync-Server](https://github.com/GothenburgBitFactory/taskchampion-sync-server), a [Taskwariror 3](https://taskwarrior.org/docs/upgrade-3/) sync server, replacing Taskwarrior 2's sync server named [`taskserver`](https://github.com/GothenburgBitFactory/taskserver). diff --git a/nixos/modules/module-list.nix b/nixos/modules/module-list.nix index 5e7d7b203e95..59ff758bde82 100644 --- a/nixos/modules/module-list.nix +++ b/nixos/modules/module-list.nix @@ -1621,6 +1621,7 @@ ./system/boot/stage-2.nix ./system/boot/systemd.nix ./system/boot/systemd/coredump.nix + ./system/boot/systemd/dm-verity.nix ./system/boot/systemd/initrd-secrets.nix ./system/boot/systemd/initrd.nix ./system/boot/systemd/journald.nix diff --git a/nixos/modules/system/boot/systemd/dm-verity.nix b/nixos/modules/system/boot/systemd/dm-verity.nix new file mode 100644 index 000000000000..eea048126be0 --- /dev/null +++ b/nixos/modules/system/boot/systemd/dm-verity.nix @@ -0,0 +1,65 @@ +{ config, lib, ... }: + +let + cfg = config.boot.initrd.systemd.dmVerity; +in +{ + options = { + boot.initrd.systemd.dmVerity = { + enable = lib.mkEnableOption "dm-verity" // { + description = '' + Mount verity-protected block devices in the initrd. + + Enabling this option allows to use `systemd-veritysetup` and + `systemd-veritysetup-generator` in the initrd. + ''; + }; + }; + }; + + config = lib.mkIf cfg.enable { + assertions = [ + { + assertion = cfg.enable -> config.boot.initrd.systemd.enable; + message = '' + 'boot.initrd.systemd.dmVerity.enable' requires 'boot.initrd.systemd.enable' to be enabled. + ''; + } + ]; + + boot.initrd = { + availableKernelModules = [ + # For documentation, see https://docs.kernel.org/admin-guide/device-mapper/dm-init.html + "dm_mod" + # For documentation, see: + # - https://docs.kernel.org/admin-guide/device-mapper/verity.html + # - https://gitlab.com/cryptsetup/cryptsetup/-/wikis/DMVerity + "dm_verity" + ]; + + # dm-verity needs additional udev rules from LVM to work. + services.lvm.enable = true; + + # The additional targets and store paths allow users to integrate verity-protected devices + # through the systemd tooling. + systemd = { + additionalUpstreamUnits = [ + # https://github.com/systemd/systemd/blob/main/units/veritysetup-pre.target + "veritysetup-pre.target" + # https://github.com/systemd/systemd/blob/main/units/veritysetup.target + "veritysetup.target" + # https://github.com/systemd/systemd/blob/main/units/remote-veritysetup.target + "remote-veritysetup.target" + ]; + + storePaths = [ + # These are the two binaries mentioned in https://github.com/systemd/systemd/blob/main/src/veritysetup/meson.build; there are no others. + "${config.boot.initrd.systemd.package}/lib/systemd/systemd-veritysetup" + "${config.boot.initrd.systemd.package}/lib/systemd/system-generators/systemd-veritysetup-generator" + ]; + }; + }; + }; + + meta.maintainers = [ lib.maintainers.msanft ]; +} diff --git a/nixos/modules/system/boot/systemd/initrd.nix b/nixos/modules/system/boot/systemd/initrd.nix index 2ccc964820fe..2ad114d6a1d3 100644 --- a/nixos/modules/system/boot/systemd/initrd.nix +++ b/nixos/modules/system/boot/systemd/initrd.nix @@ -218,7 +218,7 @@ in { }; root = lib.mkOption { - type = lib.types.enum [ "fstab" "gpt-auto" ]; + type = lib.types.nullOr (lib.types.enum [ "fstab" "gpt-auto" ]); default = "fstab"; example = "gpt-auto"; description = '' @@ -227,6 +227,9 @@ in { allow specifying the root file system itself this way. Instead, the `fstab` value is used in order to interpret the root file system specified with the `fileSystems` option. + + If the root FS is mounted by other means, such as systemd generators other than + `fstab`, `gpt-auto` or a custom generator, set this to `null`. ''; }; @@ -398,9 +401,9 @@ in { ++ lib.optional (cfg.enableTpm2 && !(pkgs.stdenv.hostPlatform.isRiscV64 || pkgs.stdenv.hostPlatform.isArmv7)) "tpm-crb" ++ lib.optional cfg.package.withEfi "efivarfs"; - boot.kernelParams = [ - "root=${config.boot.initrd.systemd.root}" - ] ++ lib.optional (config.boot.resumeDevice != "") "resume=${config.boot.resumeDevice}" + boot.kernelParams = + lib.optional (config.boot.initrd.systemd.root != null) "root=${config.boot.initrd.systemd.root}" + ++ lib.optional (config.boot.resumeDevice != "") "resume=${config.boot.resumeDevice}" # `systemd` mounts root in initrd as read-only unless "rw" is on the kernel command line. # For NixOS activation to succeed, we need to have root writable in initrd. ++ lib.optional (config.boot.initrd.systemd.root == "gpt-auto") "rw"; diff --git a/nixos/tests/all-tests.nix b/nixos/tests/all-tests.nix index 76c70e2c3fc8..a00291309b84 100644 --- a/nixos/tests/all-tests.nix +++ b/nixos/tests/all-tests.nix @@ -259,6 +259,7 @@ in { dhparams = handleTest ./dhparams.nix {}; disable-installer-tools = handleTest ./disable-installer-tools.nix {}; discourse = handleTest ./discourse.nix {}; + dm-verity = runTest ./dm-verity.nix; dnscrypt-proxy2 = handleTestOn ["x86_64-linux"] ./dnscrypt-proxy2.nix {}; dnscrypt-wrapper = runTestOn ["x86_64-linux"] ./dnscrypt-wrapper; dnsdist = import ./dnsdist.nix { inherit pkgs runTest; }; diff --git a/nixos/tests/dm-verity.nix b/nixos/tests/dm-verity.nix new file mode 100644 index 000000000000..cf20dbe66f59 --- /dev/null +++ b/nixos/tests/dm-verity.nix @@ -0,0 +1,245 @@ +# Tests a NixOS system with a read-only root filesystem that's integrity-protected +# through DM-verity. The root filesystem is mounted read-only, and for NixOS activation +# to succeed, an overlay `tmpfs` is mounted on top of it. +# This test uses systemd-repart to create a bootable disk image, as it supplies handy +# utilities for creating verity partitions, but it can also be setup manually through +# `systemd-veritysetup`. + +{ lib, pkgs, ... }: + +let + imageId = "verity-root-image"; + imageVersion = "1-rc1"; + + # Use a random, but fixed root hash placeholder to allow us specifying the "real" root hash + # after the image is first built. + roothashPlaceholder = "61fe0f0c98eff2a595dd2f63a5e481a0a25387261fa9e34c37e3a4910edf32b8"; +in +{ + name = "verity-root"; + + meta.maintainers = with lib.maintainers; [ msanft ]; + + nodes.machine = + { + lib, + pkgs, + config, + modulesPath, + ... + }: + { + + imports = [ "${modulesPath}/image/repart.nix" ]; + + virtualisation.directBoot.enable = false; + virtualisation.mountHostNixStore = false; + virtualisation.useEFIBoot = true; + + # Disable boot loaders, as a UKI is used, which contains systemd-stub. + # TODO(raitobezarius): revisit this when #244907 lands + boot.loader.grub.enable = false; + + system.image.id = imageId; + system.image.version = imageVersion; + + # systemd-veritysetup-generator takes care of setting up the root filesystem. + fileSystems = lib.mkForce { }; + virtualisation.fileSystems = lib.mkForce { }; + + # Provides 'veritysetup' to check if the verity-protected device + # has been mapped correctly. + environment.systemPackages = with pkgs; [ cryptsetup ]; + + boot.initrd = { + kernelModules = [ "overlay" ]; + supportedFilesystems = [ "erofs" ]; + + systemd = { + enable = true; + dmVerity.enable = true; + root = null; # systemd-veritysetup-generator takes care of mounting / + + tmpfiles.settings = { + "10-mountpoints" = + let + conf = { + mode = "0755"; + user = "root"; + group = "root"; + }; + in + { + "/run/etc/upper".d = conf; + "/run/etc/work".d = conf; + "/run/var/upper".d = conf; + "/run/var/work".d = conf; + "/run/tmp/upper".d = conf; + "/run/tmp/work".d = conf; + }; + }; + + # We directly define the mount units here, as we need to specify dependencies very + # granularly, and systemd-fstab-generator doesn't give us that flexibility. + mounts = [ + { + where = "/sysroot/etc"; + what = "overlay"; + type = "overlay"; + options = "lowerdir=/sysroot${config.system.build.etc}/etc,upperdir=/run/etc/upper,workdir=/run/etc/work"; + wantedBy = [ + "initrd-fs.target" + "initrd-switch-root.target" + "default.target" + ]; + before = [ "initrd-fs.target" ]; + after = [ "systemd-tmpfiles-setup.service" ]; + unitConfig.RequiresMountsFor = "/sysroot/nix/store"; + unitConfig.DefaultDependencies = false; + } + { + where = "/sysroot/var"; + what = "overlay"; + type = "overlay"; + options = "lowerdir=/sysroot/var,upperdir=/run/var/upper,workdir=/run/var/work"; + wantedBy = [ + "initrd-fs.target" + "initrd-switch-root.target" + "default.target" + ]; + before = [ "initrd-fs.target" ]; + after = [ "systemd-tmpfiles-setup.service" ]; + unitConfig.RequiresMountsFor = "/sysroot/nix/store"; + unitConfig.DefaultDependencies = false; + } + { + where = "/sysroot/tmp"; + what = "overlay"; + type = "overlay"; + options = "lowerdir=/sysroot/tmp,upperdir=/run/tmp/upper,workdir=/run/tmp/work"; + wantedBy = [ + "initrd-fs.target" + "initrd-switch-root.target" + "default.target" + ]; + before = [ "initrd-fs.target" ]; + after = [ "systemd-tmpfiles-setup.service" ]; + unitConfig.RequiresMountsFor = "/sysroot/nix/store"; + unitConfig.DefaultDependencies = false; + } + ]; + }; + }; + + boot.kernelParams = [ + "systemd.verity=yes" + "roothash=${roothashPlaceholder}" + ]; + + image.repart = { + name = imageId; + # OVMF does not work with the default repart sector size of 4096 + sectorSize = 512; + partitions = { + # ESP + "00-esp" = { + contents = + let + efiArch = config.nixpkgs.hostPlatform.efiArch; + in + { + "/EFI/BOOT/BOOT${lib.toUpper efiArch}.EFI".source = "${pkgs.systemd}/lib/systemd/boot/efi/systemd-boot${efiArch}.efi"; + + "/EFI/Linux/${config.system.boot.loader.ukiFile}".source = "${config.system.build.uki}/${config.system.boot.loader.ukiFile}"; + }; + repartConfig = { + Type = "esp"; + Format = "vfat"; + # Minimize = "guess" seems to not work very well for vfat + # partitions. It's better to set a sensible default instead. The + # aarch64 kernel seems to generally be a little bigger than the + # x86_64 kernel. To stay on the safe side, leave some more slack + # for every platform other than x86_64. + SizeMinBytes = if config.nixpkgs.hostPlatform.isx86_64 then "64M" else "96M"; + }; + }; + + # Root Partition + "10-root" = { + storePaths = [ config.system.build.toplevel ]; + repartConfig = { + Type = "root"; + Format = "erofs"; + Label = "root"; + Verity = "data"; + VerityMatchKey = "root"; + Minimize = "best"; + # We need to ensure that mountpoints are available. + MakeDirectories = "/bin /boot /dev /etc /home /lib /lib64 /mnt /nix /opt /proc /root /run /srv /sys /tmp /usr /var"; + }; + }; + + # Verity hashtree for the root partition + "20-root-verity" = { + repartConfig = { + Type = "root-verity"; + Label = "root-verity"; + Verity = "hash"; + VerityMatchKey = "root"; + Minimize = "best"; + }; + }; + }; + }; + }; + + testScript = + let + # We override the build of the image by extending it with code to replace the placeholder with the real root hash. + # This way, we can build the image first and then set the root hash afterwards in a single derivation. + buildOverride = oldAttrs: { + nativeBuildInputs = oldAttrs.nativeBuildInputs ++ [ pkgs.jq ]; + postInstall = '' + # Replace the placeholder with the real root hash. + realRoothash=$(${pkgs.jq}/bin/jq -r "[.[] | select(.roothash != null)] | .[0].roothash" $out/repart-output.json) + sed -i "0,/${roothashPlaceholder}/ s/${roothashPlaceholder}/$realRoothash/" $out/${oldAttrs.pname}_${oldAttrs.version}.raw + ''; + }; + in + { nodes, ... }: + '' + import os, subprocess, tempfile + + tmp_disk_image = tempfile.NamedTemporaryFile() + + subprocess.run([ + "${nodes.machine.virtualisation.qemu.package}/bin/qemu-img", + "create", + "-f", + "qcow2", + "-b", + "${nodes.machine.system.build.image.overrideAttrs buildOverride}/${nodes.machine.image.repart.imageFile}", + "-F", + "raw", + tmp_disk_image.name, + ]) + + # Set NIX_DISK_IMAGE so that the qemu script finds the right disk image. + os.environ['NIX_DISK_IMAGE'] = tmp_disk_image.name + + os_release = machine.succeed("cat /etc/os-release") + assert 'IMAGE_ID="${imageId}"' in os_release + assert 'IMAGE_VERSION="${imageVersion}"' in os_release + + bootctl_status = machine.succeed("bootctl status") + assert "Boot Loader Specification Type #2 (.efi)" in bootctl_status + + verity_status = machine.succeed("veritysetup status root") + assert "type: VERITY" in verity_status + assert "status: verified" in verity_status + + commandline = machine.succeed("cat /proc/cmdline") + roothash = commandline.split("roothash=")[1].split(" ")[0] + assert roothash in verity_status + ''; +}