In this post, I will show you how you can declaratively partition our drives using Nix(OS).

TL;DR; We can use a tool called disko to partition our drives declaratively and combine it with NixOS anywhere for a remote install. Showing an example setting up LUKS encryption with BTRFS file system.

Background

If you’re like me, then when you started playing with NixOS, You found yourself constantly reinstalling it and starting again. Either setting up new machines. Playing around with it in a VM or even realising how to set up LUKS and hibernate properly. Or even when setting up impermanence.

Every time I ended using the GUI to reinstall, which was a bit tedious. Then I discovered disko.

Disko

Disko is a fantastic tool. Which allows us to declaratively declare how to partition our disk(s), in nix configuration.

Why

The great about moving our partition state into code means it far easier to fully automate our installation or setup new machines with the same partitions.

Previously, I would have to update the hardware-configuration. Nix file manually to change the ID of the drives. As these UUIDs are unique to each install.

I’ll be honest, this has become much less of an issue since my config is very much now stable. But the completion in me is a lot happier, one I’ve automated one step further from setting up a new machine or reinstall the same one.

My Setup

So my setup creates a BTRFS file system and LUKS encryption.

I set up BTRFS, so I can have impermanence, which can be allowed to remove files that we don’t have in our persistence storage. To complete this, I create a persistent sub-volume (think folder). We also want LUKS so until we enter our password or a secret, our drive(s) will be encrypted. So no one can access our files.

Disko

{
  disko.devices = {
    disk = {
      nvme0n1 = {
        type = "disk";
        device = "/dev/nvme0n1";
        content = {
          type = "gpt";
          partitions = {
            ESP = {
              label = "boot";
              name = "ESP";
              size = "512M";
              type = "EF00";
              content = {
                type = "filesystem";
                format = "vfat";
                mountpoint = "/boot";
                mountOptions = [
                  "defaults"
                ];
              };
            };
            luks = {
              size = "100%";
              label = "luks";
              content = {
                type = "luks";
                name = "cryptroot";
                extraOpenArgs = [
                  "--allow-discards"
                  "--perf-no_read_workqueue"
                  "--perf-no_write_workqueue"
                ];
                # https://0pointer.net/blog/unlocking-luks2-volumes-with-tpm2-fido2-pkcs11-security-hardware-on-systemd-248.html
                settings = {crypttabExtraOpts = ["fido2-device=auto" "token-timeout=10"];};
                content = {
                  type = "btrfs";
                  extraArgs = ["-L" "nixos" "-f"];
                  subvolumes = {
                    "/root" = {
                      mountpoint = "/";
                      mountOptions = ["subvol=root" "compress=zstd" "noatime"];
                    };
                    "/home" = {
                      mountpoint = "/home";
                      mountOptions = ["subvol=home" "compress=zstd" "noatime"];
                    };
                    "/nix" = {
                      mountpoint = "/nix";
                      mountOptions = ["subvol=nix" "compress=zstd" "noatime"];
                    };
                    "/persist" = {
                      mountpoint = "/persist";
                      mountOptions = ["subvol=persist" "compress=zstd" "noatime"];
                    };
                    "/log" = {
                      mountpoint = "/var/log";
                      mountOptions = ["subvol=log" "compress=zstd" "noatime"];
                    };
                    "/swap" = {
                      mountpoint = "/swap";
                      swap.swapfile.size = "64G";
                    };
                  };
                };
              };
            };
          };
        };
      };
    };
  };

  fileSystems."/persist".neededForBoot = true;
  fileSystems."/var/log".neededForBoot = true;
}

Let’s break this file down:

Firstly, we only have one drive which we called nvme0n1, which is a 2TB NVMe drive in my pc. You can find examples on the Disko repo how to partition multiple drives and even how to have in a raid(0) configuration. For this post, we will keep it simple for now. We need to point it to where our drive exists device = "/dev/nvme0n1";.

Then we create a boot partition of 512M.

❯ eza -al /dev/nvme0n1
brw-rw---- 259,0 root 17 Jul 04:52 /dev/nvme0n1

Next, we define the LUKS drive, which encrypts everything else, including our swap “drive”.

{
    luks = {
      size = "100%";
      label = "luks";
      content = {
        type = "luks";
        name = "cryptroot";
        extraOpenArgs = [
          "--allow-discards"
          "--perf-no_read_workqueue"
          "--perf-no_write_workqueue"
        ];
      }
      # https://0pointer.net/blog/unlocking-luks2-volumes-with-tpm2-fido2-pkcs11-security-hardware-on-systemd-248.html
      settings = {crypttabExtraOpts = ["fido2-device=auto" "token-timeout=10"];};
    };
}

We will call this drive cryptroot, when setup we can find it at /dev/mapper.

/dev/mapper🔒
❯ eza -al /dev/mapper
crw------- 10,236 root 17 Jul 04:52 control
lrwxrwxrwx      - root 17 Jul 04:52 cryptroot -> ../dm-0

Then in the settings, we add support to allow us to decrypt using Fido, i.e. our YubiKey if we can to set it up settings = {crypttabExtraOpts = ["fido2-device=auto" "token-timeout=10"];};.

{
    content = {
      type = "btrfs";
      extraArgs = ["-L" "nixos" "-f"];
      subvolumes = {
        "/root" = {
          mountpoint = "/";
          mountOptions = ["subvol=root" "compress=zstd" "noatime"];
        };
        "/home" = {
          mountpoint = "/home";
          mountOptions = ["subvol=home" "compress=zstd" "noatime"];
        };
        "/nix" = {
          mountpoint = "/nix";
          mountOptions = ["subvol=nix" "compress=zstd" "noatime"];
        };
        "/persist" = {
          mountpoint = "/persist";
          mountOptions = ["subvol=persist" "compress=zstd" "noatime"];
        };
        "/log" = {
          mountpoint = "/var/log";
          mountOptions = ["subvol=log" "compress=zstd" "noatime"];
        };
        "/swap" = {
          mountpoint = "/swap";
          swap.swapfile.size = "64G";
        };
      };
    };

  # ...
  fileSystems."/persist".neededForBoot = true;
  fileSystems."/var/log".neededForBoot = true;
}

Then the final part defines the different sub-volumes we want. Which we can just think of as folders on our system. We label this partition as nixos as well, so we can refer to it via this label (hence the -L flag passed to it).

type = "btrfs";
extraArgs = ["-L" "nixos" "-f"];

We also want to create a BTRFS file system on our main partition here, simply so that we can revert to a blank state and copy files over from our persist sub-volume. So we can do, impermanence.

❯ df -h
Filesystem      Size  Used Avail Use% Mounted on
/dev/dm-0       1.9T  846G 1006G  46% /
/dev/dm-0       1.9T  846G 1006G  46% /nix
/dev/dm-0       1.9T  846G 1006G  46% /persist
/dev/dm-0       1.9T  846G 1006G  46% /var/log
tmpfs           7.7G   15M  7.7G   1% /run
devtmpfs        1.6G     0  1.6G   0% /dev
tmpfs            16G   23M   16G   1% /dev/shm
tmpfs            16G  1.3M   16G   1% /run/wrappers
efivarfs        148K   93K   51K  65% /sys/firmware/efi/efivars
/dev/dm-0       1.9T  846G 1006G  46% /home
/dev/dm-0       1.9T  846G 1006G  46% /swap
/dev/nvme0n1p1  511M  236M  276M  47% /boot

If we call this file disks.nix we can refer to it in our main configuration like so. Similar to how we refer to hardware-configuration.nix.

{
  pkgs,
  lib,
  ...
}: {
  imports = [
    ./hardware-configuration.nix
    ./disks.nix
  ];
}

Hibernate

To enable your PC to hibernate using your swap, you can add the following boot configuration.

{
  boot = {
    kernelParams = [
      "resume_offset=533760"
    ];
    resumeDevice = "/dev/disk/by-label/nixos";
  };
}

We need to add an offset because the swap is part of our main partition, rather than being its own partition. Which is how I used to set it up. But I believe if you are also using a 2TB drive, you will have the same offset.

To figure out the exact off set, you can follow the Arch wiki here.

Fido2

If you want to decrypt your LUKS drive with a YubiKey, you can do something like: sudo -E -s systemd-cryptenroll --fido2-device=auto /dev/nvme0n1p2. Then you need to press the button on your YubiKey to register. Then during LUKS decryption you can use your YubiKey. However, I think from memory this can cause decryption to be a bit slower as it waits for the YubiKey.

You can read more about it here.

Appendix