diff --git a/flake.nix b/flake.nix index 764b07c..62b0673 100644 --- a/flake.nix +++ b/flake.nix @@ -3,10 +3,14 @@ inputs = { nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable"; + home-manager = { url = "github:nix-community/home-manager"; inputs.nixpkgs.follows = "nixpkgs"; }; + + # doesn't offer much above properly used symlinks but it is convenient + impermanence.url = "github:nix-community/impermanence"; }; outputs = {self, nixpkgs, ...} @ inputs: let diff --git a/hosts/herugrim/default.nix b/hosts/herugrim/default.nix index 5f48536..f2f50f7 100644 --- a/hosts/herugrim/default.nix +++ b/hosts/herugrim/default.nix @@ -2,7 +2,6 @@ { imports = [ ./hardware-configuration.nix - ./impermanence.nix ]; boot.supportedFilesystems = [ "btrfs" ]; @@ -23,11 +22,26 @@ system = { 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"; + # Configure automatic root subvolume wiping on boot from initrd + autoBtrfsWipe = { + devicePath = "/dev/disk/by-label/NIXROOT"; + subvolumePath = "root"; + cleanSnapshotPath = "root-blank"; + }; + }; }; + device = { virtual-machine = false; cpu.type = "intel"; }; + home-manager = { enable = true; stateVersion = "23.11"; diff --git a/hosts/herugrim/impermanence.nix b/hosts/herugrim/impermanence.nix deleted file mode 100644 index 9059d69..0000000 --- a/hosts/herugrim/impermanence.nix +++ /dev/null @@ -1,110 +0,0 @@ -{ config, pkgs, ... }: -let - impermanence = builtins.fetchTarball "https://github.com/nix-community/impermanence/archive/master.tar.gz"; -in -{ - imports = [ "${impermanence}/nixos.nix" ]; - - users = { - # This option makes it that users are not mutable outside our configuration - # If you are using impermanence, this will actually be the case regardless of this setting, - # however, setting this explicitly is a good idea, because nix will warn us if - # our users don't have passwords set - mutableUsers = false; - - # Each existing user needs to have a password file defined here - # otherwise, they will not be available to login. - # These password files can be generated using the following command: - # mkpasswd -m sha-512 > /persist/system/passwords/myuser - users = { - root = { - # password file needs to be in a volume marked `neededForRoot = true` - hashedPasswordFile = "/persist/passwords/root"; - }; - itsdrike = { - hashedPasswordFile = "/persist/passwords/itsdrike"; - }; - }; - }; - - # Some people use /nix/persist/system instead, leaving the persistent files in /nix subvolume - # I much prefer using a standalone subvolume for this though. - environment.persistence."/persist/system" = { - hideMounts = true; - directories = [ - "/etc/nixos" # nixos configuration source - "/etc/NetworkManager/system-connections" # saved network connections - "/var/db/sudo" # keeps track of who got the sudo lecture already - "/var/lib/systemd/coredump" # recorded coredumps - ]; - files = [ - "/etc/machine-id" - - # ssh stuff - "/etc/ssh/ssh_host_ed25519_key" - "/etc/ssh/ssh_host_ed25519_key.pub" - "/etc/ssh/ssh_host_rsa_key" - "/etc/ssh/ssh_host_rsa_key.pub" - ]; - }; - - # For some reason, NetworkManager needs this instead of the impermanence mode to not get screwed up - systemd.tmpfiles.rules = [ - "L /var/lib/NetworkManager/secret_key - - - - /persist/system/var/lib/NetworkManager/secret_key" - "L /var/lib/NetworkManager/seen-bssids - - - - /persist/system/var/lib/NetworkManager/seen-bssids" - "L /var/lib/NetworkManager/timestamps - - - - /persist/system/var/lib/NetworkManager/timestamps" - ]; - - boot.initrd.systemd = { - enable = true; # This enables systemd support in stage 1 - required for below setup - services.rollback = { - description = "Rollback BTRFS root subvolume to a pristine state"; - 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 = '' - mkdir -p /mnt - - # We first mount the btrfs root to /mnt - # so we can manipulate btrfs subvolumes. - mount /dev/mapper/cryptfs /mnt - - # While we're tempted to just delete /root and create - # a new snapshot from /root-blank, /root is already - # populated at this point with a number of subvolumes, - # which makes `btrfs subvolume delete` fail. - # So, we remove them first. - # - # /root contains subvolumes: - # - /root/var/lib/portables - # - /root/var/lib/machines - # - # These are probably related to systemd-nspawn, but - # since I don't use it, I'm not 100% sure. - # Anyhow, deleting these subvolumes hasn't resulted in - # any issues so far, except for fairly benign-looking - # errors from systemd-tmpfiles. - btrfs subvolume list -o /mnt/root | - cut -f9 -d' ' | - while read subvolume; do - echo "deleting /$subvolume subvolume..." - btrfs subvolume delete "/mnt/$subvolume" - done && - echo "deleting /root subvolume..." && - btrfs subvolume delete /mnt/root - - echo "restoring blank /root subvolume..." - btrfs subvolume snapshot /mnt/root-blank /mnt/root - - # Once we're done rolling back to a blank snapshot, - # we can unmount /mnt and continue on the boot process. - umount /mnt - ''; - }; - }; -} - diff --git a/options/system/default.nix b/options/system/default.nix index bf9869e..b5eb76a 100644 --- a/options/system/default.nix +++ b/options/system/default.nix @@ -2,7 +2,9 @@ inherit (lib) mkOption; in { - imports = [ ]; + imports = [ + ./impermanence.nix + ]; options.myOptions.system = { hostname = mkOption { diff --git a/options/system/impermanence.nix b/options/system/impermanence.nix new file mode 100644 index 0000000..dd3951d --- /dev/null +++ b/options/system/impermanence.nix @@ -0,0 +1,81 @@ +{ lib, config, ... }: with lib; let + inherit (lib) mkEnableOption mkOption literalExpression; + + cfg = config.myOptions.system.impermanence; +in +{ + options.myOptions.system.impermanence = { + root = { + enable = mkEnableOption '' + the Impermanence module for persisting important state directories. + ''; + + extraFiles = mkOption { + default = []; + example = literalExpression ''["/etc/nix/id_rsa"]''; + description = '' + Additional files in root to link to persistent storage. + ''; + }; + + extraDirectories = mkOption { + default = []; + example = literalExpression ''["/etc/nix/id_rsa"]''; + description = '' + Additional directories in root to link to persistent storage. + ''; + }; + + persistentMountPoint = mkOption { + default = "/persist"; + description = '' + Path to a persistent directory (usually a mount point to a + standalone partition / subvolume), which will hold the persistent + system state files. + ''; + }; + + autoBtrfsWipe = { + enable = mkOption { + default = true; + description = '' + Enable automatic wiping of the root BTRFS subvolume from initrd. + + 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. + ''; + }; + + devicePath = mkOption { + default = "/dev/mapper/cryptfs"; + description = '' + Path to the BTRFS block device containing the subvolume to be wiped. + + This device will be mounted from initrd. + ''; + }; + + 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. + ''; + }; + }; + }; + }; +} diff --git a/system/default.nix b/system/default.nix index f35cdeb..388799b 100644 --- a/system/default.nix +++ b/system/default.nix @@ -4,6 +4,7 @@ _: { ./boot ./services ./nix + ./impermanence ./programs.nix ./system.nix ./network.nix diff --git a/system/impermanence/default.nix b/system/impermanence/default.nix new file mode 100644 index 0000000..cb78773 --- /dev/null +++ b/system/impermanence/default.nix @@ -0,0 +1,7 @@ +{ inputs, ... }: +{ + imports = [ + inputs.impermanence.nixosModules.impermanence + ./root.nix + ]; +} diff --git a/system/impermanence/root.nix b/system/impermanence/root.nix new file mode 100644 index 0000000..584f20a --- /dev/null +++ b/system/impermanence/root.nix @@ -0,0 +1,110 @@ +{ config }: let + cfgSystem = config.myOptions.system; + cfg = config.myOptions.system.impermanence.root; +in +{ + config = mkIf cfg.enable { + users = { + # This option makes it that users are not mutable outside of our configuration. + # If you're using root impermanence, this will actually be the case regardless + # of this setting, however, setting this explicitly is a good idea, because nix + # will warn us if our users don't have passwords set, preventing lock outs. + mutableUsers = false; + + # Each existing user needs to have a password file defined here, otherwise + # they will not be available to login. These password files can be generated with: + # mkpasswd -m sha-512 > /persist/passwords/myuser + users = { + root = { + hashedPasswordFile = "${cfg.persistentMountPoint}/passwords/root"; + }; + ${cfgSystem.username} = { + hashedPasswordFile = "${cfg.persistentMountPoint}/passwords/${cfgSystem.username}"; + }; + }; + }; + + environment.persistence."${cfg.persistentMountPoint}/system" = { + hideMounts = true; + directories = [ + "/etc/nixos" # NixOS configuration source + "/etc/NetworkManager/system-connections" # saved network connections + "/var/db/sudo" # keeps track of who got the sudo lecture already + "/var/lib/systemd/coredump" # captured coredumps + ] ++ cfg.extraDirectories; + + files = [ + "/etc/machine-id" + ] ++ cfg.extraFiles; + }; + + # For some reason, NetworkManager needs this instead of the impermanence mode + # to not get screwed up + systemd.tmpfiles.rules = [ + "L /var/lib/NetworkManager/secret_key - - - - ${cfg.persistentMountPoint}/system/var/lib/NetworkManager/secret_key" + "L /var/lib/NetworkManager/seen-bssids - - - - ${cfg.persistentMountPoint}/system/var/lib/NetworkManager/seen-bssids" + "L /var/lib/NetworkManager/timestamps - - - - ${cfg.persistentMountPoint}/system/var/lib/NetworkManager/timestamps" + ]; + + # Define host key paths in the persistent mount point instead of using impermanence for these. + # This works better, because these keys also get auto-created if they don't already exist. + services.openssh.hostKeys = mkForce [ + { + bits = 4096; + path = "${cfg.persistentMountPoint}/system/etc/ssh/ssh_host_rsa_key"; + type = "rsa"; + } + { + bits = 4096; + path = "${cfg.persistentMountPoint}/system/etc/ssh/ssh_host_ed25519_key"; + 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 + ''; + }; + }; + }; +}