# Notes for 14th of March, 2023 Today was the day I finally got to setting up both "erase your darlings" and proper disk encryption. This general setup concept utilizes NixOS' ability to boot off of a disk that contains only `/nix` and `/boot`, linking appropriate devices and blocks during the boot process and deleting all state that programs may have left over my system. The end result, for me, was a fully encrypted that uses btrfs snapshots to restore `/` to its original state on each boot. ## Resources - [This discourse post](https://discourse.nixos.org/t/impermanence-vs-systemd-initrd-w-tpm-unlocking/25167) - [This blog post](https://elis.nu/blog/2020/06/nixos-tmpfs-as-home) - [This other blog post](https://guekka.github.io/nixos-server-1/) - [And this post that the previous post is based on](https://mt-caret.github.io/blog/posts/2020-06-29-optin-state.html) - [Impermanence](https://github.com/nix-community/impermanence) ## The actual set-up (and reproduction steps) I've had to go through a few guides before I could figure out a set up that I really like. The final decision was that I would have an encrypted disk that restores itself to its former state during boot. Is it fast? Absolutely not. But it sure as hell is cool. And stateless! To return the root (and only the root) we use a systemd service that fires shortly after the disk is encrypted but before the root is actually mounted. That way, we can unlock the disk, restore the disk to its pristine state using the snapshot we have taken during installation and mount the root to go on with our day. ### Reproduction steps #### Partitioning First you want to format your disk. If you are really comfortable with bringing parted to your pre-formatted disks, by all means feel free to skip this section. I, however, choose to format a fresh disk. Start by partitioning the sections of our disk (sda1, sda2 and sda3) _Device names might change if you're using a nvme disk, i.e nvme0p1._ ```bash # Set the disk name to make it easier DISK=/dev/sda # replace this with the name of the device you are using # set up the boot partition parted "$DISK" -- mklabel gpt parted "$DISK" -- mkpart ESP fat32 1MiB 1GiB parted "$DISK" -- set 1 boot on mkfs.vfat -n BOOT "$DISK"1 ``` ```bash # set up the swap partition parted "$DISK" -- mkpart Swap linux-swap 1GiB 9GiB mkswap -L SWAP "$DISK"2 swapon "$DISK"2 ``` _I do in fact use swap in the civilized year of 2023[^1]. If I were a little more advanced, and if I did not disable hibernation due to overly-hardened kernel parameters, I would also be encrypting the swap to secure the hibernates... but that is *currently* out of my scope. You may find this desirable, however, I will not be providing instructions on that._ Encrypt your partition, and open it to make it available under `/dev/mapper/enc`. ```bash cryptsetup --verify-passphrase -v luksFormat "$DISK"3 # /dev/sda3 cryptsetup open "$DISK"3 enc ``` Now partition the encrypted device block. ```bash parted "$DISK" -- mkpart primary 9GiB 100% mkfs.btrfs -L NIXOS /dev/mapper/enc ``` ```bash mount -t btrfs /dev/mapper/enc /mnt # First we create the subvolumes, those may differ as per your preferences btrfs subvolume create /mnt/root btrfs subvolume create /mnt/home btrfs subvolume create /mnt/nix btrfs subvolume create /mnt/persist # some people may choose to put /persist in /mnt/nix, I am not one of those people. btrfs subvolume create /mnt/log ``` Now that we have created the btrfs subvolumes, it is time for the _readonly_ snapshot of the root subvolume. ```bash btrfs subvolume snapshot -r /mnt/root /mnt/root-blank # Make sure to unmount, or nixos-rebuild will try to remove /mnt and fail umount /mnt ``` #### Mounting After the subvolumes are created, we mount them with the options that we want. Ideally, on NixOS, you want the `noatime` option [^2] and zstd compression, especially on your `/nix` partition. The following is my partition layout. If you have created any other subvolumes in the step above, you will also want to mount them here. Below setup assumes that you have been following the steps as is. ```bash # / mount -o subvol=root,compress=zstd,noatime /dev/mapper/enc /mnt # /home mkdir /mnt/home mount -o subvol=home,compress=zstd,noatime /dev/mapper/enc /mnt/home # /nix mkdir /mnt/nix mount -o subvol=nix,compress=zstd,noatime /dev/mapper/enc /mnt/nix # /persist mkdir /mnt/persist mount -o subvol=persist,compress=zstd,noatime /dev/mapper/enc /mnt/persist # /var/log mkdir -p /mnt/var/log mount -o subvol=log,compress=zstd,noatime /dev/mapper/enc /mnt/var/log # do not forget to mount the boot partition mkdir /mnt/boot mount "$DISK"1 /mnt/boot ``` And finally let NixOS generate the hardware configuration. ```bash nixos-generate-config --root /mnt ``` The genereated configuration will be available at `/mnt/etc/nixos`. Before we move on, we need to add the `neededForBoot = true;` to some mounted subvolumes in `hardware-configuration.nix`. It will look something like this: ```nix # Do not modify this file! It was generated by ‘nixos-generate-config’ # and may be overwritten by future invocations. Please make changes # to /etc/nixos/configuration.nix instead. { config, lib, pkgs, modulesPath, ... }: { imports = [ (modulesPath + "/installer/scan/not-detected.nix") ]; boot.initrd.availableKernelModules = ["xhci_pci" "ahci" "usb_storage" "sd_mod" "rtsx_pci_sdmmc"]; boot.initrd.kernelModules = []; boot.kernelModules = ["kvm-intel"]; boot.extraModulePackages = []; fileSystems."/" = { device = "/dev/disk/by-uuid/b79d3c8b-d511-4d66-a5e0-641a75440ada"; fsType = "btrfs"; options = ["subvol=root"]; }; boot.initrd.luks.devices."enc".device = "/dev/disk/by-uuid/82144284-cf1d-4d65-9999-2e7cdc3c75d4"; fileSystems."/home" = { device = "/dev/disk/by-uuid/b79d3c8b-d511-4d66-a5e0-641a75440ada"; fsType = "btrfs"; options = ["subvol=home"]; }; fileSystems."/nix" = { device = "/dev/disk/by-uuid/b79d3c8b-d511-4d66-a5e0-641a75440ada"; fsType = "btrfs"; options = ["subvol=nix"]; }; fileSystems."/persist" = { device = "/dev/disk/by-uuid/b79d3c8b-d511-4d66-a5e0-641a75440ada"; fsType = "btrfs"; options = ["subvol=persist"]; neededForBoot = true; # <- add this }; fileSystems."/var/log" = { device = "/dev/disk/by-uuid/b79d3c8b-d511-4d66-a5e0-641a75440ada"; fsType = "btrfs"; options = ["subvol=log"]; neededForBoot = true; # <- add this }; fileSystems."/boot" = { device = "/dev/disk/by-uuid/FDED-3BCF"; fsType = "vfat"; }; swapDevices = [ {device = "/dev/disk/by-uuid/0d1fc824-623b-4bb8-bf7b-63a3e657889d";} # if you encrypt your swap, it'll also need to be configured here ]; nixpkgs.hostPlatform = lib.mkDefault "x86_64-linux"; powerManagement.cpuFreqGovernor = lib.mkDefault "powersave"; } ``` Do keep in mind that the NixOS hardware scanner **cannot** pick up your mount options. Which means that you should specifiy the options (i.e `noatime`) for each btrfs volume that you have created in `hardware-configuration.nix`. You can simply add them in the `options = [ ]` list in quotation marks. I recommend adding at least zstd compression, and optionally `noatime`. ### Closing Notes And that should be all. By this point you are pretty much ready to install with your existing config. I generally use my configuration flake to boot, so there is no need to make any revisions. If you are starting from scratch, you may consider tweaking your configuration.nix before you install the system. An editor, such as Neovim, or your preferred DE/wm make good additions to your configuration. Once it's all done, take a deep breath and `nixos-install`. Once the installation is done, you'll be prompted for the root password and after that you can reboot. Now you are running NixOS on an encrypted disk. Nice! Next up, if you are feeling _really_ fancy today, is to configure disk erasure and impermanence. #### Impermanence For BTRFS snapshots, I use a systemd service that goes ```nix boot.initrd.systemd = { enable = true; # this enabled systemd support in stage1 - required for the below setup services.rollback = { description = "Rollback BTRFS root subvolume to a pristine state"; wantedBy = [ "initrd.target" ]; after = [ # LUKS/TPM process "systemd-cryptsetup@enc.service" ]; 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 -o subvol=/ /dev/mapper/enc /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 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 ''; }; }; ``` > You may opt in for `boot.initrd.postDeviceCommands = lib.mkBefore ''` > as [this blog post](https://mt-caret.github.io/blog/posts/2020-06-29-optin-state.html) > suggests. I am not exactly sure how exactly those options actually > compare, however, a systemd service means it will be accessible through the > the systemd service interface, which is why I opt-in for a service. ##### Implications What this implies is that certain files such as saved networks for network-manager will be deleted on each reboot. While a little clunky, [Impermanence](https://github.com/nix-community/impermanence) is a great solution to our problem. Impermanence exposes to our system an `environment.persistence.""` option that we can use to make certain directories or files permanent. My module goes like this: ```nix imports = [inputs.impermanence.nixosModules.impermanence]; # the import will be different if flakes are not enabled on your system environment.persistence."/persist" = { directories = [ "/etc/nixos" "/etc/NetworkManager/system-connections" "/etc/secureboot" "/var/db/sudo" ]; 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" # if you use docker or LXD, also persist their directories ]; }; ``` And that is pretty much it. If everything went well, you should now be telling your friends about your new system boasting full disk encryption _and_ root rollbacks. ## Why? Honestly, why not? [^1]: I could be using `tmpfs` for `/` at this point in time. Unfortunately, since I share this setup on some of my low-end laptops, I've got no RAM to spare - which is exactly why I have opted out with BTRFS. It is a reliable filesystem that I am used to, and it allows for us to use a script that we'll see later on. [^2]: https://opensource.com/article/20/6/linux-noatime