From d0ba074777950f2d6f8566aa20774ddf181a8c28 Mon Sep 17 00:00:00 2001 From: Niklas Gollenstede Date: Tue, 27 Feb 2024 16:36:01 +0100 Subject: [PATCH] add support for config.boot.initrd.systemd.enable --- .vscode/settings.json | 13 ++ flake.lock | Bin 1669 -> 1669 bytes hosts/example.nix.md | 32 ++++- lib/setup-scripts/maintenance.sh | 11 +- modules/filesystems/pre-mount-commands.nix.md | 26 ++-- modules/installer.nix.md | 4 +- modules/setup/disks.nix.md | 2 +- modules/setup/keystore.nix.md | 119 +++++++++++++----- modules/setup/temproot.nix.md | 34 +++-- modules/setup/zfs.nix.md | 74 ++++++++--- modules/vm-exec.nix.md | 1 + 11 files changed, 238 insertions(+), 78 deletions(-) diff --git a/.vscode/settings.json b/.vscode/settings.json index 083dace..f972b89 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -1,4 +1,9 @@ { + "[nix]": { + "editor.detectIndentation": true, + "editor.insertSpaces": true, + "editor.tabSize": 4, + }, "markdown.validate.ignoredLinks": [ "./modules/", "./patches/", @@ -11,6 +16,7 @@ "acltype", // zfs "acpi", // abbr "ahci", // abbr + "asDropin", // systemd "ashift", // zfs "askpass", // program "attrset", "attrsets", // nix/abbr (attribute set) @@ -40,6 +46,7 @@ "createrawvmdk", // virtual box "createvm", // virtual box "cryptsetup", // program + "crypttab", // linux "dedup", // zfs "devs", // abbr (devices) "diffutils", // package @@ -61,12 +68,14 @@ "fetchurl", // nix function "fido2", // protocol "filesystems", // plural + "findutils", // package "fmask", // mount "foldl", // nix (fold left) "foldr", // nix (fold right) "gcroots", // Nix "gdisk", // program "getsize64", // cli arg + "getty", // program "gnugrep", // package "gnused", // package "gollenstede", // name @@ -169,10 +178,14 @@ "stdenv", // nix "storageattach", // virtual box "swsuspend", // parameter + "sysinit", // systemd "syslinux", // package + "sysroot", // systemd "sysrq", // linux + "sysusr", // systemd "temproot", // abbr (temporary root (FS)) "timesync", // systemd + "tmpdir", // abbr "TMPDIR", // env var "tmpfiles", // nixos option "tmpfs", // linux diff --git a/flake.lock b/flake.lock index 8a235d2afcacce5af57a65e966aa6a83ce1cf225..6b0a9150be1243d39a250e9c617cad306e96f5e0 100644 GIT binary patch delta 361 zcmXZXOHKkY9DwnFAS>chHWI=@NidmCJJWWI8wY$4P>e7j?%MyfFuat96XTwZE0f+s z7F@$yco89bg5US8va9UpGUFkKQo_tSc~thR@nr1w5!eHYgMOb-=krL|ZFdfxF-w?1!H`geX@35HYr;Es5iM2mmB zQbM*ZHBbPIwWk4q5sDn;In3pub_B)Jm7KCIc)N`~H>c@C^PSF`la!a?oY(~Yjiabx zRbQ`%QJ`RCzh8<*S=1En#uvnb&fvvrjVEnrK0V&-#%m>4Y8yl-85Yt}jDbXwAruP9 RsDw~x&NV|+ZVG9Y_yx8UYq|gc delta 360 zcmXZWxlRI60EOXz3|7Qa8VR9Lk}+ZKe#h9bNrojLK?CL7P0SEvkpb;#tW55ENWp9P z7QToRV|R)_`OZGK&wajTA%mPF`;{)Zl@44E+ATjGt_O`qE9eHMGaU~%aIgxa>Wl|r zRZ!E^wF$R}Ox~WIT-u*hzOvGB>84o10s&32?*Xlp3KbO+K#oErJWiqFi0WieX^T$n z_>n54vXl@;`JeqTew+2fM{45sk}RfPXJmXdkNg<1jehU7M|^HNeR1C%zWm$uvILAu zDG/dev/disk/by-partlabel/"$bootkeyPartlabel" || exit + local blockDev=$1 ; local bootkeyPartlabel=bootkey-@{config.networking.hostName!hashString.sha256:0:8} + @{native.gptfdisk}/bin/sgdisk --new=0:0:+1 --change-name=0:"$bootkeyPartlabel" --typecode=0:8301 "$blockDev" || return # create new 1 sector (512b) partition + @{native.parted}/bin/partprobe "$blockDev" && @{native.systemd}/bin/udevadm settle -t 15 || return # wait for partitions to update + { /dev/disk/by-partlabel/"$bootkeyPartlabel" || return } declare-command mount-keystore-luks cryptsetupOptions... << 'EOD' diff --git a/modules/filesystems/pre-mount-commands.nix.md b/modules/filesystems/pre-mount-commands.nix.md index a841e61..39dc7c2 100644 --- a/modules/filesystems/pre-mount-commands.nix.md +++ b/modules/filesystems/pre-mount-commands.nix.md @@ -1,6 +1,6 @@ /* -# `fileSystems.*.preMountCommands` +# `fileSystems.*.preMountCommands`/`.postUnmountCommands` ## Implementation @@ -14,7 +14,6 @@ in { preMountCommands = lib.mkOption { description = '' Commands to be run as root every time before mounting this filesystem **via systemd**, but after all its dependents were mounted. This does not order itself before or after `systemd-fsck@''${utils.escapeSystemdPath device}.service`. - This is not implemented for mounts in the initrd (those that are `neededForBoot`) yet. Note that if a symlink exists at a mount point when systemd's fstab-generator runs, it will read/resolve the symlink and use the link's target as the mount point, resulting in mismatching unit names for that mount, effectively disabling its `.preMountCommands`. This does not (apparently and unfortunately) run when mounting via the `mount` command (and probably not with the `mount` system call either). ''; type = lib.types.lines; default = ""; }; @@ -26,29 +25,34 @@ in { }; }; config = let - in ({ - assertions = lib.mapAttrsToList (name: fs: { + assertions = lib.mkIf (!config.boot.initrd.systemd.enable) (lib.mapAttrsToList (name: fs: { assertion = (fs.preMountCommands == "") || (!utils.fsNeededForBoot fs); - message = ''The filesystem "${name}" has `.preMountCommands` but is also (possibly implicitly) `.neededForBoot`. This is not currently supported.''; - }) config.fileSystems; + message = ''The filesystem "${name}" has `.preMountCommands` but is also (possibly implicitly) `.neededForBoot`. This is not supported without `boot.initrd.systemd.enable`.''; + }) config.fileSystems); # The implementation is derived from the "mkfs-${device'}" service in nixpkgs. - systemd.services = lib.fun.mapMergeUnique (_: args@{ mountPoint, device, depends, ... }: if (args.preMountCommands != "") || (args.postUnmountCommands != "") then let + services = initrd: lib.fun.mapMergeUnique (_: fs@{ mountPoint, device, depends, ... }: if + (fs.preMountCommands != "" || fs.postUnmountCommands != "") && initrd == utils.fsNeededForBoot fs + then let isDevice = lib.fun.startsWith "/dev/" device; mountPoint' = utils.escapeSystemdPath mountPoint; device' = utils.escapeSystemdPath device; - in { "pre-mount-${mountPoint'}" = rec { + in { "pre-mount-${mountPoint'}" = rec { # TODO: in initrd (or during installation), how to deal with the fact that the system is not mounted at "/"? description = "Prepare mounting ${device} at ${mountPoint}"; wantedBy = [ "${mountPoint'}.mount" ]; before = wantedBy; partOf = wantedBy; requires = lib.optional isDevice "${device'}.device"; after = lib.optional isDevice "${device'}.device"; unitConfig.RequiresMountsFor = map utils.escapeSystemdExecArg (depends ++ (lib.optional (lib.hasPrefix "/" device) device) ++ [ (builtins.dirOf mountPoint) ]); unitConfig.DefaultDependencies = false; restartIfChanged = false; serviceConfig.Type = "oneshot"; serviceConfig.RemainAfterExit = true; - script = lib.mkIf (args.preMountCommands != "") args.preMountCommands; - preStop = lib.mkIf (args.postUnmountCommands != "") args.postUnmountCommands; # ("preStop" still runs post unmount) + script = lib.mkIf (fs.preMountCommands != "") fs.preMountCommands; + preStop = lib.mkIf (fs.postUnmountCommands != "") fs.postUnmountCommands; # ("preStop" still runs post unmount) }; } else { }) config.fileSystems; - }); + in { + inherit assertions; + systemd.services = services false; + boot.initrd.systemd.services = lib.mkIf (config.boot.initrd.systemd.enable) (services true); + }; } diff --git a/modules/installer.nix.md b/modules/installer.nix.md index 91a4a9d..8d24647 100644 --- a/modules/installer.nix.md +++ b/modules/installer.nix.md @@ -48,11 +48,11 @@ in { }; build.scripts = lib.mkOption { type = lib.types.functionTo lib.types.str; internal = true; readOnly = true; - default = context: lib.fun.substituteImplicit { # This replaces the `@{}` references in the scripts with normal bash variables that hold serializations of the Nix values they refer to. + default = context: "${lib.fun.substituteImplicit { # This replaces the `@{}` references in the scripts with normal bash variables that hold serializations of the Nix values they refer to. inherit pkgs; scripts = lib.sort (a: b: a.order < b.order) (lib.attrValues cfg.scripts); context = { inherit config options pkgs; inherit (moduleArgs) inputs; } // context; # inherit (builtins) trace; - }; + }}"; }; }; }; diff --git a/modules/setup/disks.nix.md b/modules/setup/disks.nix.md index c3acaa3..bf51700 100644 --- a/modules/setup/disks.nix.md +++ b/modules/setup/disks.nix.md @@ -48,7 +48,7 @@ in { position = lib.mkOption { description = "Position at which to create the partition. The default »+0« means the beginning of the largest free block."; type = lib.types.str; default = "+0"; }; alignment = lib.mkOption { description = "Adjusted alignment quantifier for this partition only."; type = lib.types.nullOr lib.types.int; default = null; example = 1; }; index = lib.mkOption { description = "Optionally explicit partition table index to place this partition in. Use ».order« to make sure that this index hasn't been used yet.."; type = lib.types.nullOr lib.types.int; default = null; }; - order = lib.mkOption { description = "Creation order ranking of this partition. Higher orders will be created first, and will thus be placed earlier in the partition table (if ».index« isn't explicitly set) and also further to the front of the disk space."; type = lib.types.int; default = if config.size == null then 500 else (1000 - (if config.index == null then 0 else 256 - config.index)); }; + order = lib.mkOption { description = "Creation order ranking of this partition. Higher orders will be created first, and will thus be placed earlier in the partition table (if ».index« isn't explicitly set) and also further to the front of the disk space. No partitions may follow one that has ».size == null«. The computed default puts the partition(s) with ».size == null« last (»500«), partitions with an ».index« before that (»744 + .index«, sorted), and all other partitions at the front (»1000«, unordered)."; type = lib.types.int; default = if config.size == null then 500 else (1000 - (if config.index == null then 0 else 256 - config.index)); }; }; }))); default = { }; apply = lib.filterAttrs (k: v: v != null); diff --git a/modules/setup/keystore.nix.md b/modules/setup/keystore.nix.md index 86e2e84..dc6037f 100644 --- a/modules/setup/keystore.nix.md +++ b/modules/setup/keystore.nix.md @@ -27,7 +27,7 @@ Any number of other devices may thus specify paths in the keystore as keylocatio ```nix #*/# end of MarkDown, beginning of NixOS module: -dirname: inputs: { config, pkgs, lib, ... }: let lib = inputs.self.lib.__internal__; in let +dirname: inputs: { config, pkgs, lib, utils, ... }: let lib = inputs.self.lib.__internal__; in let inherit (inputs.config.rename) setup installer; cfg = config.${setup}.keystore; hash = builtins.substring 0 8 (builtins.hashString "sha256" config.networking.hostName); @@ -50,14 +50,19 @@ in let module = { config = let - in lib.mkIf cfg.enable (lib.mkMerge [ ({ + in lib.mkIf (cfg.enable) (lib.mkMerge [ ({ + ${setup}.keystore.keys."luks/keystore-${hash}/0" = lib.mkOptionDefault "hostname"; # (This is the only key that the setup scripts unconditionally require to be declared.) assertions = [ { assertion = cfg.keys?"luks/keystore-${hash}/0"; message = ''At least one key (»0«) for »luks/keystore-${hash}« must be specified!''; } ]; - boot.initrd.luks.devices = lib.mkIf cfg.enableLuksGeneration (lib.fun.mapMerge (key: let + }) ({ ## Declare LUKS devices for all LUKS keys: + ${setup}.keystore.enableLuksGeneration = lib.mkIf (config.virtualisation.useDefaultFilesystems or false) (lib.mkVMOverride false); + }) (lib.mkIf (cfg.enableLuksGeneration) { + + boot.initrd.luks.devices = (lib.fun.mapMerge (key: let label = builtins.substring 5 ((builtins.stringLength key) - 7) key; in { ${label} = { device = lib.mkDefault "/dev/disk/by-partlabel/${label}"; @@ -65,27 +70,16 @@ in let module = { allowDiscards = lib.mkDefault true; # If attackers can observe trimmed blocks, then they can probably do much worse ... }; }) (lib.fun.filterMatching ''^luks/.*/0$'' (lib.attrNames cfg.keys))); - ${setup}.keystore.keys."luks/keystore-${hash}/0" = lib.mkOptionDefault "hostname"; # (This is the only key that the setup scripts unconditionally require to be declared.) + boot.initrd.systemd.services = lib.mkIf (config.boot.initrd.systemd.enable) (lib.fun.mapMerge (key: let + label = builtins.substring 5 ((builtins.stringLength key) - 7) key; + in if label == "keystore-${hash}" || (!config.boot.initrd.luks.devices?${label}) then { } else { "systemd-cryptsetup@${utils.escapeSystemdPath label}" = rec { + overrideStrategy = "asDropin"; # (could this be set via a x-systemd.after= crypttab option?) + after = [ "systemd-cryptsetup@keystore\\x2d${hash}.service" ]; wants = after; # (this may be implicit if systemd knew about the /run/keystore-... mount point) + }; }) (lib.fun.filterMatching ''^luks/.*/0$'' (lib.attrNames cfg.keys))); - }) ({ - boot.initrd.luks.devices."keystore-${hash}" = { - device = "/dev/disk/by-partlabel/keystore-${hash}"; - postOpenCommands = '' - echo "Mounting ${keystore}" - mkdir -p ${keystore} - mount -o nodev,umask=0277,ro /dev/mapper/keystore-${hash} ${keystore} - ''; - preLVM = true; # ensure the keystore is opened early (»preLVM« also seems to be pre zpool import, and it is the only option that affects the opening order) - keyFile = lib.mkMerge [ - (lib.mkIf cfg.unlockMethods.trivialHostname "${pkgs.writeText "hostname" config.networking.hostName}") - (lib.mkIf cfg.unlockMethods.usbPartition "/dev/disk/by-partlabel/bootkey-${hash}") - ]; - fallbackToPassword = true; # (might as well) - preOpenCommands = lib.mkIf cfg.unlockMethods.pinThroughYubikey verbose.doOpenWithYubikey; - }; + }) ({ ## Create and populate keystore during installation: - # Create and populate keystore during installation: fileSystems.${keystore} = { fsType = "vfat"; device = "/dev/mapper/keystore-${hash}"; options = [ "ro" "nosuid" "nodev" "noexec" "noatime" "umask=0277" "noauto" ]; formatArgs = [ ]; }; ${setup}.disks.partitions."keystore-${hash}" = { type = lib.mkDefault "8309"; order = lib.mkDefault 1375; disk = lib.mkDefault "primary"; size = lib.mkDefault "32M"; }; @@ -94,10 +88,32 @@ in let module = { ${pkgs.rsync}/bin/rsync -a ${keystore}/ $tmp/ )''; - }) (lib.mkIf (config.boot.initrd.luks.devices?"keystore-${hash}") { # (this will be false when overwritten by »nixos/modules/virtualisation/qemu-vm.nix«) + + ## Unlocking and closing during early boot + }) (lib.mkIf (!(config.virtualisation.useDefaultFilesystems or false)) (let # (don't bother with any of this if »boot.initrd.luks.devices« is forced to »{ }« in »nixos/modules/virtualisation/qemu-vm.nix«) + in lib.mkMerge [ ({ + + boot.initrd.luks.devices."keystore-${hash}".keyFile = lib.mkMerge [ + (lib.mkIf (cfg.unlockMethods.trivialHostname) "${pkgs.writeText "hostname" config.networking.hostName}") + (lib.mkIf (cfg.unlockMethods.usbPartition) "/dev/disk/by-partlabel/bootkey-${hash}") + ]; + boot.initrd.systemd.storePaths = lib.mkIf (cfg.unlockMethods.trivialHostname && config.boot.initrd.systemd.enable) [ "${pkgs.writeText "hostname" config.networking.hostName}" ]; boot.initrd.supportedFilesystems = [ "vfat" ]; + }) (lib.mkIf (!config.boot.initrd.systemd.enable) { # Legacy initrd + + boot.initrd.luks.devices."keystore-${hash}" = { + preLVM = true; # ensure the keystore is opened early (»preLVM« also seems to be pre zpool import, and it is the only option that affects the opening order) + preOpenCommands = lib.mkIf (cfg.unlockMethods.pinThroughYubikey) verbose.doOpenWithYubikey; # TODO: required? + fallbackToPassword = true; # (might as well) + postOpenCommands = '' + echo "Mounting ${keystore}" + mkdir -p ${keystore} + mount -o nodev,umask=0277,ro /dev/mapper/keystore-${hash} ${keystore} + ''; + }; + boot.initrd.postMountCommands = '' ${if (lib.any (lib.fun.matches "^home/.*$") (lib.attrNames cfg.keys)) then '' echo "Transferring home key composites" @@ -111,23 +127,68 @@ in let module = { cryptsetup close /dev/mapper/keystore-${hash} ''; - boot.initrd.luks.yubikeySupport = lib.mkIf cfg.unlockMethods.pinThroughYubikey true; - boot.initrd.extraUtilsCommands = lib.mkIf cfg.unlockMethods.pinThroughYubikey (lib.mkAfter '' + boot.initrd.luks.yubikeySupport = lib.mkIf (cfg.unlockMethods.pinThroughYubikey) true; + boot.initrd.extraUtilsCommands = lib.mkIf (cfg.unlockMethods.pinThroughYubikey) (lib.mkAfter '' copy_bin_and_libs ${verbose.askPassWithYubikey}/bin/cryptsetup-askpass sed -i "s|/bin/sh|$out/bin/sh|" "$out/bin/cryptsetup-askpass" ''); - }) ]); + }) (lib.mkIf (config.boot.initrd.systemd.enable) (let # Systemd initrd + + unlockWithYubikey = pkgs.writeShellScript "unlock-keystore" (let + dev = config.boot.initrd.luks.devices."keystore-${hash}"; + in '' + ${verbose.tryYubikey} + ${lib.optionalString (dev.keyFile != null) '' + if systemd-cryptsetup attach 'keystore-${hash}' '/dev/disk/by-partlabel/keystore-${hash}' ${lib.escapeShellArg dev.keyFile} '${lib.optionalString dev.allowDiscards "discard,"}headless' ; then exit ; fi + printf '%s\n\n' 'Unlocking keystore-${hash} with '${lib.escapeShellArg dev.keyFile}' failed.' >/dev/console + ''} + for attempt in "" 2 3 ; do ( + passphrase=$( systemd-ask-password 'Please enter passphrase for disk keystore-${hash}'"''${attempt:+ (attempt $attempt/3)}" ) || exit + passphrase="$( tryYubikey "$passphrase" 2>/dev/console )" || exit + systemd-cryptsetup attach 'keystore-${hash}' '/dev/disk/by-partlabel/keystore-${hash}' <( printf %s "$passphrase" ) '${lib.optionalString dev.allowDiscards "discard,"}headless' || exit + ) && break ; done || exit + ''); + in { + boot.initrd.systemd.services = { + "systemd-cryptsetup@keystore\\x2d${hash}" = { + overrideStrategy = "asDropin"; + serviceConfig.ExecStart = lib.mkIf (cfg.unlockMethods.pinThroughYubikey) [ "" "${unlockWithYubikey}" ]; + postStart = '' + echo "Mounting ${keystore}" + mkdir -p ${keystore} + mount -o nodev,umask=0277,ro /dev/mapper/keystore-${hash} ${keystore} + ''; + }; + # lib.mkIf (lib.any (lib.fun.matches "^home/.*$") (lib.attrNames cfg.keys)) + initrd-nixos-activation.postStart = '' + mkdir -pm 551 /sysroot/run/keys/home-composite/ + if [[ -e ${keystore}/home/ ]] ; then + cp -a ${keystore}/home/*.key /sysroot/run/keys/home-composite/ + fi + ''; + initrd-cleanup.preStart = '' + umount ${keystore} || true + rmdir ${keystore} || true + ${config.systemd.package}/lib/systemd/systemd-cryptsetup detach keystore-${hash} + ''; + }; + boot.initrd.luks.devices."keystore-${hash}".keyFileTimeout = 10; + boot.initrd.systemd.storePaths = lib.mkIf (cfg.unlockMethods.pinThroughYubikey) [ unlockWithYubikey ]; + boot.initrd.systemd.initrdBin = lib.mkIf (cfg.unlockMethods.pinThroughYubikey) [ pkgs.yubikey-personalization ]; + + })) ])) ]); + }; verbose = rec { tryYubikey = ''tryYubikey () { # 1: key local key="$1" ; local slot - if [ "$(ykinfo -q -2 2>/dev/null)" = '1' ] ; then slot=2 ; - elif [ "$(ykinfo -q -1 2>/dev/null)" = '1' ] ; then slot=1 ; fi + if [ "$( ykinfo -q -2 2>/dev/null )" = '1' ] ; then slot=2 ; + elif [ "$( ykinfo -q -1 2>/dev/null )" = '1' ] ; then slot=1 ; fi if [ "$slot" ] ; then - echo >&2 ; echo "Using slot $slot of detected Yubikey ..." >&2 - key="$(ykchalresp -$slot "$key" 2>/dev/null || true)" + echo "Using slot $slot of detected Yubikey ..." >&2 + key="$( ykchalresp -$slot "$key" 2>/dev/null )" || true if [ "$key" ] ; then echo "Got response from Yubikey" >&2 ; fi fi printf '%s' "$key" diff --git a/modules/setup/temproot.nix.md b/modules/setup/temproot.nix.md index b1a06ab..aa50546 100644 --- a/modules/setup/temproot.nix.md +++ b/modules/setup/temproot.nix.md @@ -57,14 +57,16 @@ This completely configures the disks, partitions, pool, datasets, and mounts for setup.temproot.temp.type = "zfs"; setup.temproot.local.type = "zfs"; setup.temproot.remote.type = "zfs"; + setup.temproot.swap = { size = "2G"; asPartition = true; encrypted = true; }; # Change/set the pools storage layout (see above), then adjust the partitions disks/sizes. Declaring disks requires them to be passed to the system installer. setup.zfs.pools."rpool-${hash}".vdevArgs = [ "raidz1" "rpool-rz1-${hash}" "rpool-rz2-${hash}" "rpool-rz3-${hash}" "log" "rpool-zil-${hash}" "cache" "rpool-arc-${hash}" ]; setup.disks.partitions."rpool-rz1-${hash}" = { disk = "raidz1"; }; setup.disks.partitions."rpool-rz2-${hash}" = { disk = "raidz2"; }; setup.disks.partitions."rpool-rz3-${hash}" = { disk = "raidz3"; }; + setup.disks.partitions."swap-${hash}" = { }; # ... (created & configured implicitly) setup.disks.partitions."rpool-zil-${hash}" = { size = "2G"; }; - setup.disks.partitions."rpool-arc-${hash}" = { }; } # (this is actually already implicitly declared) + setup.disks.partitions."rpool-arc-${hash}" = { }; } # (this is also already implicitly declared) ``` On a less beefy system, but also with less data to manage, `tmpfs` works fine for `tmp`, and `f2fs` promises to get more performance out of the flash/ram/cpu: @@ -265,15 +267,31 @@ in { }) ]))) ({ "/" = { options = { }; uid = 0; gid = 0; mode = "755"; }; } // cfg.${type}.mounts); - })) (lib.mkIf (cfg.temp.type == "zfs") { + })) (lib.mkIf (cfg.temp.type == "zfs") (let + description = "ZFS rollback to ${cfg.temp.zfs.dataset}/**@empty"; + command = ''zfs list -H -o name -t snapshot -r ${lib.escapeShellArg cfg.temp.zfs.dataset} | grep '@empty$' | xargs -n1 -- zfs rollback -r''; + pool = builtins.head (builtins.split "/" cfg.temp.zfs.dataset); + in { - boot.initrd.postDeviceCommands = lib.mkAfter '' - echo 'Clearing root ZFS' - ( zfs list -H -o name -t snapshot -r ${cfg.temp.zfs.dataset} | grep '@empty$' | xargs -n1 --no-run-if-empty zfs rollback -r ) - ''; + boot.initrd.postDeviceCommands = lib.mkIf (!config.boot.initrd.systemd.enable) (lib.mkAfter '' + echo '${description}' + ( ${command} ) + ''); + + boot.initrd.systemd.services.zfs-rollback-temp = lib.mkIf (config.boot.initrd.systemd.enable) (rec { + after = [ "zfs-import-${pool}.service" ]; before = [ "zfs-import.target" ]; wantedBy = before; + serviceConfig.Type = "oneshot"; + #inherit description; script = "PATH=${lib.makeBinPath [ pkgs.findutils pkgs.gnugrep ]}:$PATH ; ${command}"; + inherit description; script = command; + }); + #boot.initrd.systemd.initrdBin = lib.mkIf (config.boot.initrd.systemd.enable) [ pkgs.findutils pkgs.gnugrep ]; + boot.initrd.systemd.extraBin = lib.mkIf (config.boot.initrd.systemd.enable) { + grep = lib.getExe pkgs.gnugrep; + xargs = "${pkgs.findutils}/bin/xargs"; + }; - }) (lib.mkIf (cfg.temp.type == "bind") { # (TODO: this should completely clear or even recreate the »cfg.temp.bind.source«) + })) (lib.mkIf (cfg.temp.type == "bind") { # (TODO: this should completely clear or even recreate the »cfg.temp.bind.source«) boot.cleanTmpDir = true; # Clear »/tmp« on reboot. @@ -357,7 +375,7 @@ in { ${setup} = { zfs.enable = true; - zfs.pools.${lib.head (lib.splitString "/" dataset)} = { }; # ensure the pool exists (all properties can be adjusted) + zfs.pools.${lib.head (builtins.split "/" dataset)} = { }; # ensure the pool exists (all properties can be adjusted) keystore.keys."zfs/${dataset}" = lib.mkIf (type == "remote" && config.${setup}.keystore.enable) (lib.mkOptionDefault "random"); # the entire point of ZFS remote are backups, and those should be encrypted zfs.datasets = { diff --git a/modules/setup/zfs.nix.md b/modules/setup/zfs.nix.md index 6324cd0..ede1c4c 100644 --- a/modules/setup/zfs.nix.md +++ b/modules/setup/zfs.nix.md @@ -71,7 +71,7 @@ in let module = { ## Implement »cfg.datasets.*.mount«: fileSystems = lib.fun.mapMerge (path: { props, mount, ... }: if mount != false then { - "${props.mountpoint}" = { fsType = "zfs"; device = path; options = [ "zfsutil" ] ++ (lib.optionals (mount == "noauto") [ "noauto" ]); }; + "${props.mountpoint}" = { fsType = "zfs"; device = path; options = [ "zfsutil" "x-systemd.after=zfs-import.target" ] ++ (lib.optionals (mount == "noauto") [ "noauto" ]); }; } else { }) cfg.datasets; ## Load keys (only) for (all) datasets that are declared as encryption roots and aren't disabled: @@ -86,7 +86,7 @@ in let module = { compression = lib.mkOptionDefault "lz4"; # Seems to be the best compromise between compression and CPU load. atime = lib.mkOptionDefault "off"; relatime = lib.mkOptionDefault "on"; # Very much don't need access times at all. - acltype = lib.mkOptionDefault "posix"; # Enable ACLs (access control lists) on linux; might be useful at some point. (»posix« is the same as »posixacl«, but this is the normalized form) + acltype = lib.mkOptionDefault "posix"; # Enable ACLs (access control lists) on Linux; might be useful at some point. (»posix« is the same as »posixacl«, but this is the normalized form) xattr = lib.mkOptionDefault "sa"; # Allow extended attributes and store them as system attributes, recommended with »acltype=posix«. dnodesize = lib.mkOptionDefault "auto"; # Recommenced with »xattr=sa«. (A dnode is roughly equal to inodes, storing file directory or meta data.) #normalization = lib.mkOptionDefault "formD"; # Don't enforce utf-8, and thus don't normalize file names; instead accept any byte stream as file name. @@ -94,9 +94,6 @@ in let module = { canmount = lib.mkOptionDefault "off"; mountpoint = lib.mkOptionDefault "none"; # Assume the pool root is a "container", unless overwritten. }; }) cfg.pools; - # All pools that have at least one dataset that (explicitly or implicitly) has a key to be loaded from »/run/keystore-.../zfs/« have to be imported in the initramfs while the keystore is open (but only if the keystore is not disabled): - zfs.extraInitrdPools = lib.mkIf (config.boot.initrd.luks.devices?"keystore-${hash}") (lib.mapAttrsToList (name: _: lib.head (lib.splitString "/" name)) (lib.filterAttrs (name: { props, ... }: if props?keylocation then lib.fun.startsWith "file:///run/keystore-${hash}/" props.keylocation else config.${setup}.keystore.keys?"zfs/${name}") cfg.datasets)); - # Might as well set some defaults for all partitions required (though for all but one at least some of the values will need to be changed): disks.partitions = lib.fun.mapMergeUnique (name: { ${name} = { # (This also implicitly ensures that no partition is used twice for zpools.) type = lib.mkDefault "bf00"; size = lib.mkOptionDefault null; order = lib.mkDefault 500; @@ -104,13 +101,28 @@ in let module = { }; - }) ({ ## Implement »cfg.extraInitrdPools«: + }) (let ## Implement »cfg.extraInitrdPools«: - boot.initrd.postDeviceCommands = (lib.mkAfter '' + # All pools that have at least one dataset that (explicitly or implicitly) has a key to be loaded from »/run/keystore-.../zfs/« have to be imported in the initramfs while the keystore is open (but only if the keystore is not disabled): + keystorePools = lib.optionals (config.boot.initrd.luks.devices?"keystore-${hash}") (lib.mapAttrsToList (name: _: lib.head (lib.splitString "/" name)) (lib.filterAttrs (name: { props, ... }: if props?keylocation then lib.fun.startsWith "file:///run/keystore-${hash}/" props.keylocation else config.${setup}.keystore.keys?"zfs/${name}") cfg.datasets)); + in { + ${setup}.zfs.extraInitrdPools = keystorePools; + + boot.initrd.postDeviceCommands = lib.mkIf (!config.boot.initrd.systemd.enable) (lib.mkAfter '' ${lib.concatStringsSep "\n" (map verbose.initrd-import-zpool cfg.extraInitrdPools)} ${verbose.initrd-load-keys} ''); + boot.initrd.systemd.services = lib.mkIf (config.boot.initrd.systemd.enable) (lib.fun.mapMerge (pool: { "zfs-import-${pool}" = let + service = config.systemd.services."zfs-import-${pool}" or null; + addPrefix = deps: map (dep: if dep == "zfs-import.target" || dep == "sysusr-usr.mount" then dep else "sysroot-${dep}") deps; + in lib.mkMerge [ (lib.mkIf (service != null) ( + (builtins.removeAttrs service [ "restartTriggers" "reloadTriggers" ]) + // { requiredBy = addPrefix service.requiredBy; before = addPrefix service.before; } + )) (lib.mkIf (builtins.elem pool keystorePools) ( + rec { after = [ "systemd-cryptsetup@keystore\\x2d${hash}.service" ]; wants = after; } # without this, the keystore's password prompt fails + )) ]; }) cfg.extraInitrdPools); + }) (lib.mkIf (config.boot.resumeDevice == "") { ## Disallow hibernation without fixed »resumeDevice«: @@ -156,35 +168,59 @@ in let module = { anyPool = filterBy: lib.any (pool: pool.${filterBy}) (lib.attrValues cfg.pools); poolNames = filterBy: lib.attrNames (lib.filterAttrs (name: pool: pool.${filterBy}) cfg.pools); filter = pool: "^${pool}($|[/])"; - ensure-datasets = zfsPackage: extraUtils: pkgs.writeScript "ensure-datasets" '' - #!${pkgs.pkgsStatic.bash}/bin/bash - set -o pipefail -o nounset ; declare-command () { : ; } ; declare-flag () { : ; } ; - ${lib.fun.substituteImplicit { inherit pkgs; scripts = lib.attrValues { inherit (lib.self.setup-scripts) zfs utils; }; context = { inherit config; native = pkgs // { zfs = zfsPackage; } // (lib.optionalAttrs (extraUtils != null) (lib.genAttrs [ + ensure-datasets = zfsPackage: extraUtils: (let + inherit (lib.fun.substituteImplicit { inherit pkgs; scripts = lib.attrValues { inherit (lib.self.setup-scripts) zfs utils; }; context = { inherit config; native = pkgs // { zfs = zfsPackage; } // (lib.optionalAttrs (extraUtils != null) (lib.genAttrs [ "kmod" # modprobe "util-linux" # mount umount "nix" "openssh" "jq" # (unused) - ] (_: extraUtils))); }; }} + ] (_: extraUtils))); }; }) script scripts vars; + in { script = pkgs.writeScript "ensure-datasets" '' + #!${pkgs.pkgsStatic.bash}/bin/bash + set -o pipefail -o nounset ; declare-command () { : ; } ; declare-flag () { : ; } ; + ${script} ensure-datasets "$@" - ''; + ''; inherit scripts vars; }); ensure-datasets-for = filterBy: zfsPackage: extraUtils: ''( if [ ! "''${IN_NIXOS_ENTER:-}" ] && [ -e ${zfsPackage}/bin/zfs ] ; then ${lib.concatStrings (map (pool: '' expected=${lib.escapeShellArg (builtins.toJSON (lib.mapAttrs (n: v: v.props // (if v.permissions != { } then { ":permissions" = v.permissions; } else { })) (lib.filterAttrs (path: _: path == pool || lib.fun.startsWith "${pool}/" path) cfg.datasets)))} if [ "$(${zfsPackage}/bin/zfs get -H -o value nixos-${setup}:applied-datasets ${pool})" != "$expected" ] ; then - ${ensure-datasets zfsPackage extraUtils} / ${lib.escapeShellArg (filter pool)} && ${zfsPackage}/bin/zfs set nixos-${setup}:applied-datasets="$expected" ${pool} + ${(ensure-datasets zfsPackage extraUtils).script} / ${lib.escapeShellArg (filter pool)} && ${zfsPackage}/bin/zfs set nixos-${setup}:applied-datasets="$expected" ${pool} fi '') (poolNames filterBy))} fi )''; + ensure-datasets-service = pool: initrd: if + (if initrd then !pool.autoApplyDuringBoot else !pool.autoApplyOnActivation) + then { } else { "zfs-ensure-${pool.name}" = let + expected = builtins.toJSON (lib.mapAttrs (n: v: v.props // (if v.permissions != { } then { ":permissions" = v.permissions; } else { })) (lib.filterAttrs (path: _: path == pool.name || lib.fun.startsWith "${pool.name}/" path) cfg.datasets)); + #zfsPackage = if initrd then pkgs.runCommandLocal "root-link" { } ''ln -sT / $out'' else pkgs.runCommandLocal "booted-system-link" { } ''ln -sT /run/booted-system/sw $out''; + zfsPackage = if initrd then "/" else "/run/booted-system/sw"; + #extraUtils = if initrd then pkgs.runCommandLocal "root-link" { } ''ln -sT / $out'' else null; + extraUtils = if initrd then "/" else null; + in rec { + after = [ "zfs-import-${pool.name}.service" ]; + before = [ "zfs-import.target" "shutdown.target" ]; wantedBy = [ "zfs-import.target" ]; conflicts = [ "shutdown.target" ]; + unitConfig.DefaultDependencies = false; # (not after basic.target or sysinit.target) + serviceConfig.Type = "oneshot"; + script = '' + expected=${lib.escapeShellArg expected} + if [ "$(${zfsPackage}/bin/zfs get -H -o value nixos-${setup}:applied-datasets ${pool.name})" != "$expected" ] ; then + ${(ensure-datasets zfsPackage extraUtils).script} ${if initrd then "/sysroot" else "/"} ${lib.escapeShellArg (filter pool.name)} && ${zfsPackage}/bin/zfs set nixos-${setup}:applied-datasets="$expected" ${pool.name} + fi + ''; + } // (lib.optionalAttrs (!initrd) { + restartTriggers = [ expected ]; + }); }; in { - boot.initrd.postDeviceCommands = lib.mkIf (anyPool "autoApplyDuringBoot") (lib.mkOrder 2000 '' + boot.initrd.postDeviceCommands = lib.mkIf (!config.boot.initrd.systemd.enable) (lib.mkIf (anyPool "autoApplyDuringBoot") (lib.mkOrder 2000 '' ${ensure-datasets-for "autoApplyDuringBoot" extraUtils extraUtils} - ''); + '')); + boot.initrd.systemd.services = lib.mkIf (config.boot.initrd.systemd.enable) (lib.fun.mapMerge (pool: ensure-datasets-service pool true) (lib.attrValues cfg.pools)); + boot.initrd.systemd.storePaths = lib.mkIf (anyPool "autoApplyDuringBoot") (let deps = ensure-datasets "/" "/"; in [ "${pkgs.pkgsStatic.bash}/bin/bash" deps.script ] ++ deps.scripts ++ (lib.filter (v: lib.isStringLike v && lib.hasPrefix builtins.storeDir v) (lib.attrValues deps.vars))); # TODO: using pkgsStatic.bash may not be necessary here boot.initrd.supportedFilesystems = lib.mkIf (anyPool "autoApplyDuringBoot") [ "zfs" ]; ${setup}.zfs.extraInitrdPools = (poolNames "autoApplyDuringBoot"); - system.activationScripts.A_ensure-datasets = lib.mkIf (anyPool "autoApplyOnActivation") { - text = ensure-datasets-for "autoApplyOnActivation" (pkgs.runCommandLocal "booted-system-link" { } ''ln -sT /run/booted-system/sw $out'') null; # (want to use the version of ZFS that the kernel module uses, also it's convenient that this does not yet exist during activation at boot) - }; # these are sorted alphabetically, unless one gets "lifted up" by some other ending on it via its ».deps« field + systemd.services = lib.fun.mapMerge (pool: ensure-datasets-service pool false) (lib.attrValues cfg.pools); }) ])) ( diff --git a/modules/vm-exec.nix.md b/modules/vm-exec.nix.md index ca87da8..015d76c 100644 --- a/modules/vm-exec.nix.md +++ b/modules/vm-exec.nix.md @@ -88,6 +88,7 @@ in let hostModule = { virtualisation.graphics = false; # Instead of tearing down the initrd environment, adjust some mounts and run the »command« in the initrd: + boot.initrd.systemd.enable = lib.mkVMOverride false; boot.initrd.postMountCommands = '' for fs in tmp/shared tmp/xchg nix/store.lower nix/var/nix/db.lower ; do