diff --git a/modules/boot/kernel.nix b/modules/boot/kernel.nix index a864d09a..40f9b534 100644 --- a/modules/boot/kernel.nix +++ b/modules/boot/kernel.nix @@ -128,6 +128,13 @@ ''; }; + + boot.resumeDevice = lib.mkOption { + type = lib.types.str; + default = ""; + description = "Device from which to resume after hibernation. Empty = disabled. When set, adds resume= to boot.kernelParams."; + }; + boot.kernelParams = lib.mkOption { type = lib.types.listOf ( lib.types.strMatching ''([^"[:space:]]|"[^"]*")+'' @@ -278,6 +285,10 @@ "rtc_cmos" ]; + boot.kernelParams = lib.mkIf (config.boot.resumeDevice != "") [ + "resume=${config.boot.resumeDevice}" + ]; + boot.initrd.kernelModules = [ # For LVM. "dm_mod" diff --git a/modules/filesystems/default.nix b/modules/filesystems/default.nix index a3aa50ab..85be0e5c 100644 --- a/modules/filesystems/default.nix +++ b/modules/filesystems/default.nix @@ -27,7 +27,11 @@ let sw: let device = if sw.label != null then "/dev/disk/by-label/${sw.label}" else sw.device; - options = sw.options ++ lib.optional (sw.priority != null) "pri=${toString sw.priority}"; + options = sw.options + ++ lib.optional (sw.priority != null) "pri=${toString sw.priority}" + ++ lib.optional (sw.discardPolicy != null) ( + if sw.discardPolicy == "both" then "discard" else "discard=${sw.discardPolicy}" + ); in "${escape device} none swap ${escape (lib.concatStringsSep "," options)} 0 0\n"; @@ -98,6 +102,44 @@ let ) + "\n" ) fstabFileSystems; + + # Swap entries with randomEncryption.enable can't be stable fstab lines: the backing device is a fresh /dev/mapper/ created with a brand new random key on every boot, so they're set up imperatively instead + isEncryptedSwap = sw: sw.randomEncryption.enable; + plainSwapDevices = lib.filter (sw: !isEncryptedSwap sw) config.swapDevices; + encryptedSwapDevices = lib.filter isEncryptedSwap config.swapDevices; + + sanitizeName = s: lib.replaceStrings [ "/" " " ] [ "-" "-" ] (lib.removePrefix "/" s); + + makeEncryptedSwapTask = sw: rec { + name = "cryptswap-${sanitizeName sw.device}"; + value = + let + re = sw.randomEncryption; + options = sw.options + ++ lib.optional (sw.priority != null) "pri=${toString sw.priority}" + ++ lib.optional (sw.discardPolicy != null) ( + if sw.discardPolicy == "both" then "discard" else "discard=${sw.discardPolicy}" + ); + in + { + description = "Encrypted swap device on ${sw.device}"; + runlevels = "S"; + command = toString ( + pkgs.writeShellScript name '' + set -eu + ${pkgs.cryptsetup}/bin/cryptsetup plainOpen \ + -c ${lib.escapeShellArg re.cipher} \ + -s ${toString re.keySize} \ + ${lib.optionalString (re.sectorSize != 0) "--sector-size ${toString re.sectorSize}"} \ + ${lib.optionalString re.allowDiscards "--allow-discards"} \ + -d ${lib.escapeShellArg re.source} \ + ${lib.escapeShellArg sw.device} ${lib.escapeShellArg name} + ${pkgs.util-linux}/bin/mkswap /dev/mapper/${name} + ${pkgs.util-linux}/bin/swapon -o ${lib.escapeShellArg (lib.concatStringsSep "," options)} /dev/mapper/${name} + '' + ); + }; + }; in { imports = [ @@ -126,13 +168,27 @@ in # system.fsPackages = [ pkgs.dosfstools ]; # environment.systemPackages = with pkgs; [ fuse3 fuse ] ++ config.system.fsPackages; + assertions = lib.map (sw: { + assertion = + sw.label == null + && (builtins.match "/dev/disk/by-(uuid|label)/.*" sw.device == null); + message = '' + Random-encrypted swap device ${sw.device} must not use swapDevices.*.label, + and should not be referenced by UUID or label, since those are erased and regenerated on every + boot once the partition is encrypted. Use a stable path such as + /dev/disk/by-partuuid/... instead. + ''; + }) encryptedSwapDevices; + environment.systemPackages = lib.unique ( lib.flatten ( lib.concatMap (v: lib.optional v.enable v.packages or [ ]) ( lib.attrValues config.boot.supportedFilesystems ) ) - ); + ) ++ lib.optional (encryptedSwapDevices != [ ]) pkgs.cryptsetup; + + finit.tasks = lib.listToAttrs (lib.map makeEncryptedSwapTask encryptedSwapDevices); environment.etc.fstab.text = '' # This is a generated file. Do not edit! @@ -151,8 +207,8 @@ in ] ) fileSystems) { }} - # swap devices - ${lib.concatMapStrings makeSwapEntry config.swapDevices} + # swap devices (random-encrypted swap is handled by finit.tasks instead) + ${lib.concatMapStrings makeSwapEntry plainSwapDevices} ''; boot.supportedFilesystems = lib.mapAttrs' ( diff --git a/modules/filesystems/options.nix b/modules/filesystems/options.nix index 9be5c69c..80b53161 100644 --- a/modules/filesystems/options.nix +++ b/modules/filesystems/options.nix @@ -143,6 +143,84 @@ let type = with lib.types; nonEmptyListOf nonEmptyStr; description = "Options used to set up the swap device."; }; + + discardPolicy = lib.mkOption { + default = null; + example = "both"; + type = with lib.types; nullOr (enum [ "once" "pages" "both" ]); + description = '' + Specify the discard policy for the swap device. If "once", + then the discard operation will be performed once, at swapon + invocation. If "pages", then the discard operation will be + performed on every freed page (this is "discard" semantics + in swap parlance, not TRIM). If "both", then both will be + enabled. If unset, then no discard policy is applied. + ''; + }; + + randomEncryption = lib.mkOption { + default = { }; + description = '' + Encrypt swap device with a random key. This way you won't have a + persistent swap device, the entire contents become unreadable + after every reboot. Mutually exclusive with {option}`label`, + since the underlying mapped device is regenerated on every boot + and is therefore not activated through `/etc/fstab` like a + regular swap device, but through a dedicated `finit` task. + ''; + type = lib.types.submodule { + options = { + enable = lib.mkOption { + default = false; + type = lib.types.bool; + description = "Encrypt swap device with a random key."; + }; + + cipher = lib.mkOption { + default = "aes-xts-plain64"; + type = lib.types.nonEmptyStr; + description = '' + Specify the cipher to use, see {command}`cryptsetup + benchmark` for a list of options. + ''; + }; + + keySize = lib.mkOption { + default = 256; + type = lib.types.ints.positive; + description = "Specify the size of the randomly generated key, in bits."; + }; + + sectorSize = lib.mkOption { + default = 0; + type = lib.types.ints.unsigned; + description = '' + Specify the sector size in bytes, or `0` to let + {command}`cryptsetup` pick the default. + ''; + }; + + source = lib.mkOption { + default = "/dev/urandom"; + type = lib.types.nonEmptyStr; + description = '' + Source of randomness used to generate the swap + encryption key on every boot. + ''; + }; + + allowDiscards = lib.mkOption { + default = false; + type = lib.types.bool; + description = '' + Whether to allow TRIM requests to the underlying device. + This option has security implications, see the + upstream NixOS manual section on swap encryption. + ''; + }; + }; + }; + }; }; }; in