From a4ee00fe6e8ba2d06dd56930da2130b7b364b88c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pawe=C5=82?= <134298056+FixeQD@users.noreply.github.com> Date: Tue, 30 Jun 2026 07:32:44 +0000 Subject: [PATCH 1/4] feat(nixos/swap): add discardPolicy option --- modules/filesystems/default.nix | 6 +++++- modules/filesystems/options.nix | 14 ++++++++++++++ 2 files changed, 19 insertions(+), 1 deletion(-) diff --git a/modules/filesystems/default.nix b/modules/filesystems/default.nix index a3aa50ab..f72574cd 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"; diff --git a/modules/filesystems/options.nix b/modules/filesystems/options.nix index 9be5c69c..d667dd23 100644 --- a/modules/filesystems/options.nix +++ b/modules/filesystems/options.nix @@ -143,6 +143,20 @@ 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. + ''; + }; }; }; in From 1c96c6d383a7b4bec5f1a890a12285deabb8d84f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pawe=C5=82?= <134298056+FixeQD@users.noreply.github.com> Date: Tue, 30 Jun 2026 07:59:23 +0000 Subject: [PATCH 2/4] feat(nixos/swap): add randomEncryption option handled via finit tasks --- modules/filesystems/default.nix | 57 +++++++++++++++++++++++++++-- modules/filesystems/options.nix | 64 +++++++++++++++++++++++++++++++++ 2 files changed, 118 insertions(+), 3 deletions(-) diff --git a/modules/filesystems/default.nix b/modules/filesystems/default.nix index f72574cd..161584a8 100644 --- a/modules/filesystems/default.nix +++ b/modules/filesystems/default.nix @@ -102,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, mirroring how modules/zram.nix builds its swap device + 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 ${re.cipher} \ + -s ${toString re.keySize} \ + ${lib.optionalString (re.sectorSize != 0) "--sector-size ${toString re.sectorSize}"} \ + ${lib.optionalString re.allowDiscards "--allow-discards"} \ + -d ${re.source} \ + ${sw.device} ${name} + ${pkgs.util-linux}/bin/mkswap /dev/mapper/${name} + ${pkgs.util-linux}/bin/swapon -o ${lib.concatStringsSep "," options} /dev/mapper/${name} + '' + ); + }; + }; in { imports = [ @@ -130,13 +168,26 @@ in # system.fsPackages = [ pkgs.dosfstools ]; # environment.systemPackages = with pkgs; [ fuse3 fuse ] ++ config.system.fsPackages; + assertions = lib.map (sw: { + assertion = + sw.randomEncryption.enable -> builtins.match "/dev/disk/by-(uuid|label)/.*" sw.device == null; + message = '' + Random-encrypted swap device ${sw.device} 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! @@ -155,8 +206,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 d667dd23..80b53161 100644 --- a/modules/filesystems/options.nix +++ b/modules/filesystems/options.nix @@ -157,6 +157,70 @@ let 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 From 2b7133edc3eeb4cd3e4489527ed408a4cadc9b9c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pawe=C5=82?= <134298056+FixeQD@users.noreply.github.com> Date: Mon, 29 Jun 2026 11:42:47 +0000 Subject: [PATCH 3/4] feat(boot): add boot.resumeDevice option for hibernation --- modules/boot/kernel.nix | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/modules/boot/kernel.nix b/modules/boot/kernel.nix index a864d09a..88dbde93 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. Not implemented in finix."; + }; + 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" From a6a857af0c6d24d69aff73c017eb27db3669a28c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pawe=C5=82?= <134298056+FixeQD@users.noreply.github.com> Date: Tue, 7 Jul 2026 01:23:22 +0000 Subject: [PATCH 4/4] feat(boot): update resumeDevice description to clarify boot parameters usage fix(filesystems): simplify comment on randomEncryption swap entries and escape shell arguments --- modules/boot/kernel.nix | 2 +- modules/filesystems/default.nix | 17 +++++++++-------- 2 files changed, 10 insertions(+), 9 deletions(-) diff --git a/modules/boot/kernel.nix b/modules/boot/kernel.nix index 88dbde93..40f9b534 100644 --- a/modules/boot/kernel.nix +++ b/modules/boot/kernel.nix @@ -132,7 +132,7 @@ boot.resumeDevice = lib.mkOption { type = lib.types.str; default = ""; - description = "Device from which to resume after hibernation. Empty = disabled. Not implemented in finix."; + description = "Device from which to resume after hibernation. Empty = disabled. When set, adds resume= to boot.kernelParams."; }; boot.kernelParams = lib.mkOption { diff --git a/modules/filesystems/default.nix b/modules/filesystems/default.nix index 161584a8..85be0e5c 100644 --- a/modules/filesystems/default.nix +++ b/modules/filesystems/default.nix @@ -103,7 +103,7 @@ 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, mirroring how modules/zram.nix builds its swap device + # 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; @@ -128,14 +128,14 @@ let pkgs.writeShellScript name '' set -eu ${pkgs.cryptsetup}/bin/cryptsetup plainOpen \ - -c ${re.cipher} \ + -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 ${re.source} \ - ${sw.device} ${name} + -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.concatStringsSep "," options} /dev/mapper/${name} + ${pkgs.util-linux}/bin/swapon -o ${lib.escapeShellArg (lib.concatStringsSep "," options)} /dev/mapper/${name} '' ); }; @@ -170,10 +170,11 @@ in assertions = lib.map (sw: { assertion = - sw.randomEncryption.enable -> builtins.match "/dev/disk/by-(uuid|label)/.*" sw.device == null; + sw.label == null + && (builtins.match "/dev/disk/by-(uuid|label)/.*" sw.device == null); message = '' - Random-encrypted swap device ${sw.device} should not be referenced - by UUID or label, since those are erased and regenerated on every + 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. '';