Rework (and fix) impermanence

This commit is contained in:
ItsDrike 2024-04-08 00:36:02 +02:00
parent 326313666f
commit 3b75c09b95
Signed by: ItsDrike
GPG key ID: FA2745890B7048C0
5 changed files with 112 additions and 92 deletions

View file

@ -23,16 +23,18 @@
hostname = "herugrim"; hostname = "herugrim";
username = "itsdrike"; username = "itsdrike";
impermanence.root = { impermanence = {
root = {
enable = true; enable = true;
# Some people use /nix/persist/system for this, leaving persistent files in /nix subvolume # Some people use /nix/persist/system for this, leaving persistent files in /nix subvolume
# I much prefer using a standalone subvolume for this though. # I much prefer using a standalone subvolume for this though.
persistentMountPoint = "/persist"; persistentMountPoint = "/persist";
};
# Configure automatic root subvolume wiping on boot from initrd # Configure automatic root subvolume wiping on boot from initrd
autoBtrfsWipe = { autoWipeBtrfs = {
devicePath = "/dev/disk/by-label/NIXROOT"; enable = true;
subvolumePath = "root"; devices."/dev/disk/by-label/NIXROOT".subvolumes = [ "root" ];
cleanSnapshotPath = "root-blank";
}; };
}; };
}; };

View file

@ -1,5 +1,5 @@
{ lib, config, ... }: with lib; let { lib, config, ... }: with lib; let
inherit (lib) mkEnableOption mkOption literalExpression; inherit (lib) mkEnableOption mkOption literalExpression types;
cfg = config.myOptions.system.impermanence; cfg = config.myOptions.system.impermanence;
in in
@ -12,6 +12,7 @@ in
extraFiles = mkOption { extraFiles = mkOption {
default = []; default = [];
type = types.listOf types.path;
example = literalExpression ''["/etc/nix/id_rsa"]''; example = literalExpression ''["/etc/nix/id_rsa"]'';
description = '' description = ''
Additional files in root to link to persistent storage. Additional files in root to link to persistent storage.
@ -20,6 +21,7 @@ in
extraDirectories = mkOption { extraDirectories = mkOption {
default = []; default = [];
type = types.listOf types.path;
example = literalExpression ''["/etc/nix/id_rsa"]''; example = literalExpression ''["/etc/nix/id_rsa"]'';
description = '' description = ''
Additional directories in root to link to persistent storage. Additional directories in root to link to persistent storage.
@ -34,48 +36,53 @@ in
system state files. 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 { autoWipeBtrfs = let
default = "/dev/mapper/cryptfs"; btrfsDeviceOptionType = types.submodule {
options = {
subvolumes = mkOption {
type = types.listOf types.str;
default = [];
description = '' description = ''
Path to the BTRFS block device containing the subvolume to be wiped. List of BTRFS subvolumes to be wiped from the device.
This device will be mounted from initrd. 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.
subvolumePath = mkOption { If you're using BTRFS, you will generally want to enable this, however
default = "root"; 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.
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.
'';
devices = mkOption {
default = {};
type = types.attrsOf btrfsDeviceOptionType;
description = '' description = ''
Path to the BTRFS subvolume to be wiped. BTRFS devices and their subvolumes to be wiped.
This is a relative path, starting from the BTRFS root.
''; '';
example = literalExpression ''
{
"/dev/sda1" = {
subvolumes = [ "root" ];
}; };
"/dev/mapper/cryptfs" = {
cleanSnapshotPath = mkOption { subvolumes = [ "homeJohn" "homeBob" ];
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.
''; '';
}; };
}; };
}; };
};
} }

View file

@ -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)}
'';
};
};
};
}

View file

@ -2,5 +2,6 @@
{ {
imports = [ imports = [
./root.nix ./root.nix
./autowipe.nix
]; ];
} }

View file

@ -1,4 +1,6 @@
{ config }: let { config, lib, ... }: let
inherit (lib) mkIf mkForce;
cfgSystem = config.myOptions.system; cfgSystem = config.myOptions.system;
cfg = config.myOptions.system.impermanence.root; cfg = config.myOptions.system.impermanence.root;
in in
@ -60,51 +62,5 @@ in
type = "ed25519"; 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
'';
};
};
}; };
} }