diff --git a/hosts/herugrim/default.nix b/hosts/herugrim/default.nix index f2f50f7..394e4b2 100644 --- a/hosts/herugrim/default.nix +++ b/hosts/herugrim/default.nix @@ -23,16 +23,18 @@ hostname = "herugrim"; username = "itsdrike"; - impermanence.root = { - enable = true; - # Some people use /nix/persist/system for this, leaving persistent files in /nix subvolume - # I much prefer using a standalone subvolume for this though. - persistentMountPoint = "/persist"; + impermanence = { + root = { + enable = true; + # Some people use /nix/persist/system for this, leaving persistent files in /nix subvolume + # I much prefer using a standalone subvolume for this though. + persistentMountPoint = "/persist"; + }; + # Configure automatic root subvolume wiping on boot from initrd - autoBtrfsWipe = { - devicePath = "/dev/disk/by-label/NIXROOT"; - subvolumePath = "root"; - cleanSnapshotPath = "root-blank"; + autoWipeBtrfs = { + enable = true; + devices."/dev/disk/by-label/NIXROOT".subvolumes = [ "root" ]; }; }; }; diff --git a/options/system/impermanence.nix b/options/system/impermanence.nix index dd3951d..fece564 100644 --- a/options/system/impermanence.nix +++ b/options/system/impermanence.nix @@ -1,5 +1,5 @@ { lib, config, ... }: with lib; let - inherit (lib) mkEnableOption mkOption literalExpression; + inherit (lib) mkEnableOption mkOption literalExpression types; cfg = config.myOptions.system.impermanence; in @@ -12,6 +12,7 @@ in extraFiles = mkOption { default = []; + type = types.listOf types.path; example = literalExpression ''["/etc/nix/id_rsa"]''; description = '' Additional files in root to link to persistent storage. @@ -20,6 +21,7 @@ in extraDirectories = mkOption { default = []; + type = types.listOf types.path; example = literalExpression ''["/etc/nix/id_rsa"]''; description = '' Additional directories in root to link to persistent storage. @@ -34,47 +36,52 @@ in system state files. ''; }; + }; - autoBtrfsWipe = { - enable = mkOption { - default = true; - description = '' - Enable automatic wiping of the root BTRFS subvolume from initrd. + autoWipeBtrfs = let + btrfsDeviceOptionType = types.submodule { + options = { + subvolumes = mkOption { + type = types.listOf types.str; + default = []; + description = '' + List of BTRFS subvolumes to be wiped from the device. - Generally, you will want to keep this enabled, as otherwise setting up - impermanence is pointless. However in case you're using a non-BTRFS - system, or you wish to set up a custom handling for this auto-wiping, - which the current handling doesn't support, disable this. - ''; + These subvolumes will be wiped from initrd, before the subvolumes are mounted. + ''; + example = literalExpression ''[ "root" "home" ]''; + }; }; + }; + in { + enable = mkEnableOption '' + automatic wiping of specified BTRFS subvolumes from initrd. - devicePath = mkOption { - default = "/dev/mapper/cryptfs"; - description = '' - Path to the BTRFS block device containing the subvolume to be wiped. + If you're using BTRFS, you will generally want to enable this, however + with a non-BTRFS system, or in case you wish to set up some custom handling + which this module doesn't support, you will need to write your own logic + for automatic root wiping. - This device will be mounted from initrd. - ''; - }; + One option is is to simply have your root get mounted from tmpfs, making it + live in RAM. This does however require dedicating a concrete chunk of RAM. + ''; - subvolumePath = mkOption { - default = "root"; - description = '' - Path to the BTRFS subvolume to be wiped. - - This is a relative path, starting from the BTRFS root. - ''; - }; - - cleanSnapshotPath = mkOption { - default = "root-blank"; - description = '' - Path to the BTRFS snapshot (subvolume) to be restore - `myOptions.system.impermanence.root.autoWipe.btrfsSubvolume` to. - - This should be a blank snapshot to achieve a complete wipe. - ''; - }; + devices = mkOption { + default = {}; + type = types.attrsOf btrfsDeviceOptionType; + description = '' + BTRFS devices and their subvolumes to be wiped. + ''; + example = literalExpression '' + { + "/dev/sda1" = { + subvolumes = [ "root" ]; + }; + "/dev/mapper/cryptfs" = { + subvolumes = [ "homeJohn" "homeBob" ]; + }; + } + ''; }; }; }; diff --git a/system/impermanence/autowipe.nix b/system/impermanence/autowipe.nix new file mode 100644 index 0000000..e4e228d --- /dev/null +++ b/system/impermanence/autowipe.nix @@ -0,0 +1,54 @@ +{ config, lib, ... }: let + inherit (lib) mkIf concatStringsSep flatten mapAttrsToList; + cfg = config.myOptions.system.impermanence.autoWipeBtrfs; +in +{ + config = mkIf cfg.enable { + boot.initrd.systemd = { + enable = true; # This enables systemd support in stage 1 - required for below setup + + services.rollback = { + description = "Rollback BTRFS subvolumes to a pristine state"; + enable = true; + wantedBy = [ "initrd.target" ]; + # Make sure it's done after decryption (i.e. LUKS/TPM process) + after = [ "systemd-cryptsetup@cryptfs.service" ]; + # mount the root fs before clearing + before = [ "sysroot.mount" ]; + unitConfig.DefaultDependencies = "no"; + serviceConfig.Type = "oneshot"; + script = let + wipeScript = devicePath: subvolumes: '' + # Mount the BTRFS device root to a temporary mount point + echo "Mounting BTRFS root from ${devicePath} to /mnt" + mount --mkdir "${devicePath}" /mnt + + # Recreate each specified subvolume + ${concatStringsSep "\n" (map (subvolume: '' + delete_subvolume_recursively "/mnt/${subvolume}" + btrfs subvolume create "/mnt/${subvolume}" + '') subvolumes)} + + # Cleanup: unmount the device + echo "Unmounting BTRFS root from ${devicePath}" + umount /mnt + ''; + in '' + delete_subvolume_recursively() { + IFS=$'\n' + for i in $(btrfs subvolume list -o "$1" | cut -f 9- -d ' '); do + delete_subvolume_recursively "/mnt/$i" + done + + echo "Deleting subvolume $1" + btrfs subvolume delete "$1" + } + + ${concatStringsSep "\n" (mapAttrsToList (devicePath: deviceOpts: + wipeScript devicePath deviceOpts.subvolumes + ) cfg.devices)} + ''; + }; + }; + }; +} diff --git a/system/impermanence/default.nix b/system/impermanence/default.nix index 0a5db09..e09594e 100644 --- a/system/impermanence/default.nix +++ b/system/impermanence/default.nix @@ -2,5 +2,6 @@ { imports = [ ./root.nix + ./autowipe.nix ]; } diff --git a/system/impermanence/root.nix b/system/impermanence/root.nix index 584f20a..b24b053 100644 --- a/system/impermanence/root.nix +++ b/system/impermanence/root.nix @@ -1,4 +1,6 @@ -{ config }: let +{ config, lib, ... }: let + inherit (lib) mkIf mkForce; + cfgSystem = config.myOptions.system; cfg = config.myOptions.system.impermanence.root; in @@ -60,51 +62,5 @@ in type = "ed25519"; } ]; - - boot.initrd.systemd = let - cfgWipe = cfg.autoBtrfsWipe; - in { - enable = true; # This enables systemd support in stage 1 - required for below setup - services.rollback-root = { - description = "Rollback BTRFS root subvolume to a pristine state"; - enable = cfgWipe.enable; - wantedby = [ "initrd.target" ]; - # Make sure it's done after decryption (i.e. LUKS/TPM process) - after = [ "systemd-cryptsetup@cryptfs.service" ]; - # mount the root fs before clearing - before = [ "sysroot.mount" ]; - unitConfig.DefaultDependencies = "no"; - serviceConfig.Type = "oneshot"; - script = '' - # Mount the BTRFS root to /mnt, so we can manipulate the subvolumes - mount --mkdir ${cfgWipe.devicePath} /mnt - - # To restore the root subvolume, we will first delete it, and then create - # a new snapshot from the blank snapshot, which will become our new root subvolume - - # However, at this point, root subvol is already populated and contains a number - # of subvolumes, which would make `btrfs subvolume delete` fail. - # - # These existing subvolumes get created automatically, and we can safely remove - # them. They are: /srv, /var/lib/portables, /var/lib/machines, /var/tmp - sudo btrfs subvolume list -o "/mnt/${cfgWipe.subvolumePath}" | cut -f9 -d' ' | - while read subvolme; do - echo "deleting $subvolume subvolume..." && - btrfs subvolume delete "/mnt/$subvolume" - done - - # Now we can remove the root subvolume, and restore it from a snapshot - echo "deleting ${cfgWipe.subvolumePath} (root) subvolume..." - btrfs subvolume delete "/mnt/${cfg.subvolumePath}" - echo "restoring ${cfgWipe.subvolumePath} (root) subvolume..." - btrfs subvolume snapshot "/mnt/${cfgWipe.cleanSnapshotPath}" - "/mnt/${cfgWipe.subvolumePath}" - - # Once we're done rolling back to a blank snapshot, - # we can unmount /mnt and continue on the boot process - umount /mnt - ''; - }; - }; }; }