NixOS disk encryption with YubiKey 1FA/2FA. From scratch, USB bootable, potentially including swap

Why would you need this? What is the threat?

A challenge facing me regularly is trusted computing on untrusted hardware. It is incredibly useful to have a bootable installation of an operating system with you, with all the software stack you know and trust. However, can you actually trust the hardware? Depending on the device you use, yes. Then this is likely an overcomplicated solution looking for a problem. If your answer however is no (as it is for me), then this is the thing. The goal is to reduce the risk of falling victim to key loggers by making passwords alone useless. This is commonly achieved by using 2FA. However, good 2FA on a device you want to use on the go is hard. My solution is Yubikey+Password, with a keylogger never fully compromising your disk unless the yubikey is also stolen/taken by force. For network facing things, you can for example use the yubikey pgp function to unlock a gnu passwordstore as password manager with mfa, alternatives exist. For booting, one can use a combination of yubikey + salted user passphrase to calculate a (ever identical) luks passphrase, without the whole luks passphrase being what you enter as password.

Alternatively, setting up 1FA yubikey based luks, you can achieve an encrypted disk without the slowdown on the start of an exciting day caused by entering a long (secure) password. 1FA yubikey allows you to be lazy, but this was not my primary intention when setting this up.

Resources

None of these guides worked well, yielding half-broken installs or installs that’d combust after the first boot. Or just installs that did have encryption, but no yubikey support. This writeup aims to be functionally complete to achieve a full working setup.

Getting started/Requirements:

nix-shell https://github.com/sgillespie/nixos-yubikey-luks/archive/master.tar.gz

Create the luks disk key

This should be ran in the nix shell specifically prepared to deal with yubikey stuff.

SLOT=2 # either 1 or 2 works, depends on which slots of your yubikey are used


# optional; overrides the existing cryptographic key on that yubikey slot.
#  Making multiple installs with one yubikey, you might not want to override your slot
ykpersonalize -"$SLOT" -ochal-resp -ochal-hmac

SALT_LENGTH=16
SALT="$(dd if=/dev/random bs=1 count=$SALT_LENGTH 2>/dev/null | rbtohex)"

read -s USER_PASSPHRASE # Skip if you want only Yubikey 1FA
echo $USER_PASSPHRASE # check your password! Otherwise, you can pretty much restart from here.
# make sure to not compromise security by storing password in your shell history.

CHALLENGE="$(echo -n $SALT | openssl dgst -binary -sha512 | rbtohex)"

# this is what will happen on unlock to calculate the disk key
RESPONSE="$(ykchalresp -"$SLOT" -x $CHALLENGE 2>/dev/null)" # replace -2 with -1 if you used slot 1 on the yubikey

KEY_LENGTH=512 # if AES-256, use 512 key length. Other algos will have different.
ITERATIONS=1000000 # Make it hard to reverse by doing a bunch of iterations. Too much will make the unlock slow, too little will reduce cracking effort needed.


# run ONLY the one appropriate one!!
# 2FA (with user password):
LUKS_KEY="$(echo -n $USER_PASSPHRASE | pbkdf2-sha512 $(($KEY_LENGTH / 8)) $ITERATIONS $RESPONSE | rbtohex)"

# 1 FA (no user password):
LUKS_KEY="$(echo | pbkdf2-sha512 $(($KEY_LENGTH / 8)) $ITERATIONS $RESPONSE | rbtohex)"


CIPHER=aes-xts-plain64
HASH=sha512

Partitioning

Start by creating a formatted disk:

gdisk /dev/sda # can be ran in any environment that has gdisk

After doing these steps, the planned setup should look as follows:

Command (? for help): p
Disk /dev/sda: 1953525168 sectors, 931.5 GiB
Model: RTL9210 NVME
Sector size (logical/physical): 512/2048 bytes
Disk identifier (GUID): D894E466-C549-40DA-94E8-5503E3814FF7
Partition table holds up to 128 entries
Main partition table begins at sector 2 and ends at sector 33
First usable sector is 34, last usable sector is 1953525134
Partitions will be aligned on 2048-sector boundaries
Total free space is 3437 sectors (1.7 MiB)

Number  Start (sector)    End (sector)  Size       Code  Name
   1            2048         1026047   500.0 MiB   EF00  EFI system partition
   2         1026048         3123199   1024.0 MiB  8300  Linux filesystem
   3         3123200      1953523711   930.0 GiB   8309  Linux LUKS

Command (? for help):

Don’t worry, we didn’t yet actually set up any root partition, that follows in the one LUKS container!

After creating the partition, you’ll have to format them.

# using above defined shell variables to create the luks container:
echo -n "$LUKS_KEY" | hextorb | cryptsetup luksFormat --cipher="$CIPHER" \
  --key-size="$KEY_LENGTH" --hash="$HASH" --key-file=- /dev/sda3

# open the luks container:
echo -n "$LUKS_KEY" | hextorb | cryptsetup open /dev/sda3 encrypted --key-file=-

# create volume groups and partitions
pvcreate /dev/mapper/encrypted
vgcreate vg /dev/mapper/encrypted
lvcreate -L 8G -n swap vg # optional
lvcreate -l '100%FREE' -n root vg

# format the partitions
mkfs.fat /dev/sda1 -n BOOT
mkfs.ext4 -L salt /dev/sda2
mkfs.ext4 -L root /dev/vg/root
mkswap -L swap /dev/vg/swap # optional, do if you created swap

# store salts, mount partitions
mkdir /mnt
mount /dev/vg/root /mnt
mkdir /mnt/boot
mkdir /mnt/crypt-storage
mount /dev/sda1 /mnt/boot
mount /dev/sda2 /mnt/crypt-storage
echo -ne "$SALT\n$ITERATIONS" | tee /mnt/crypt-storage/default # store salt and iterations. tee because thats easier to sudo :3

Once this is done, lsblk -f should output the partitions and mount points along with IDs we’ll need in the next step.

NAME          FSTYPE      FSVER    LABEL UUID                                   FSAVAIL FSUSE% MOUNTPOINTS
sda
├─sda1        vfat        FAT16    BOOT  12CE-A600                               499,7M     0% /mnt/boot
├─sda2        ext4        1.0      salt  541a42a8-3423-4e01-8006-6d50e67ed2b8    906,2M     0% /mnt/crypt-storage
└─sda3        crypto_LUKS 2              e2e2c887-b7f8-4e47-b536-fe1722619836
  └─encrypted LVM2_member LVM2 001       4Wt72L-clvC-gsag-AWHk-r3rp-Zxqd-W3hgSs
    ├─vg-swap swap        1        swap  6ee108ac-800e-486b-8219-de70cb61d077
    └─vg-root ext4        1.0      root  77eb233e-3131-471b-8829-8b832eae6e1d    860,3G     0% /mnt

Fat16 is afaik spec for external bootables, it might make sense to format to fat32 if fat16 does not work or you want to install to internal drive.

Actually installing NixOS

nixos-generate-config --root /mnt

After generating the config, do the standard nix stuff like changing locale, maybe adding wifi, creating a user account etc etc.

Make sure your configuration uses systemd-boot:

boot.loader.systemd-boot.enable = true;
boot.loader.efi.canTouchEfiVariables = true;

Add kernel modules to allow inputs on systemd-boot screen to unlock the disk:

boot.kernelModules = [ "kvm-intel" "vfat" "nls_cp437" "nls_iso8859-1" "usbhid" "usb_storage" "nvme" ];

I added usb_storage and nvme modules because i run an external SSD via USB, nvme is for (modern) SSDs and usb storage is for USB. Depending on your hardware, you’d want to change it to sd_mod and/or ahci or similar, see https://gist.github.com/CMCDragonkai/810f78ee29c8fce916d072875f7e1751

Add the mount points for your system:

  fileSystems."/" = {
    # /dev/vg/root, use the ID for the partition inside the volume group!
    device = "/dev/disk/by-uuid/77eb233e-3131-471b-8829-8b832eae6e1d";
    fsType = "ext4";
  };

  fileSystems."/crypt-storage" = {
    device = "/dev/disk/by-uuid/541a42a8-3423-4e01-8006-6d50e67ed2b8"; # /dev/sda2
    fsType = "ext4";
    options = [ "umask=077" ]; # read only so a fat-finger can't accidentially bonk our salts, rendering the disk useless.
    # Keep backups of this partition, as well as of your luks headers!!!
  };

  fileSystems."/boot" = {
    device = "/dev/disk/by-uuid/12CE-A600"; # /dev/sda1
    fsType = "vfat";
    options = [ "fmask=0022" "dmask=0022" "umask=077" ];
  };

  # if you set up swap:
  swapDevices = [ {
    device = "/dev/disk/by-uuid/6ee108ac-800e-486b-8219-de70cb61d077"; # /dev/vg/swap
  }];

Make systemd-boot use the yubikey:

  boot.initrd.luks.yubikeySupport = true; # enable yubikey support

  boot.initrd.luks.devices."root" = {
      device = "/dev/disk/by-uuid/e2e2c887-b7f8-4e47-b536-fe1722619836"; # /dev/sda3
      preLVM = true;
      allowDiscards = true;

      yubikey = {
        slot = 2;
        twoFactor = true; # Set to false for 1FA
        gracePeriod = 30; # Time in seconds to wait for Yubikey to be inserted
        keyLength = 64; # Set to $KEY_LENGTH/8
        saltLength = 16; # Set to $SALT_LENGTH

        storage = {
          device = "/dev/disk/by-uuid/541a42a8-3423-4e01-8006-6d50e67ed2b8"; # same ID as the crypt-storage mount earlier
          fsType = "ext4";
          path = "/default";
        };
      };
    };

Once everything is ready, run the install:

nixos-install