Jump to content

Yubikey based Full Disk Encryption (FDE) on NixOS

From NixOS Wiki

This page is a minimalistic guide for setting up LUKS-based full disk encryption with Yubikey pre-boot authentication (PBA) on a UEFI system. The YubiKey PBA in NixOS currently utilizes YubiKey's (HMAC-SHA1) challenge response mode. Two-factor authentication using a (secret) user passphrase as an additional layer of security is also available (and is recommended for more security).

In the 19.03 release (and prior) this method will change the LUKS authentication key on each boot that passes the LVM mount stage by altering a salt value contained on the boot partition.

This guide was tested to work on NixOS 25.05.

This guide utilizes this nix-shell expression which provides the environment for setting up LUKS-based full disk encryption with YubiKey PBA.

Encrypting Multiple drives

If you intend to encrypt multiple drives following this guide, be advised that the file mentioned in steps 6 and 7 is per-drive and using the same file for two drives will result in an un-bootable system. Instead you should create a small partition (a tiny FAT32 partition will do) on every secondary drive and follow the same steps you would for the salt file on those. Obviously you will also need to encrypt the other drives like you would the main drive. See this post for some more (somewhat outdated) information.

Requirements

  • A NixOS live system booted in UEFI mode on the target machine. (i.e. Booting from an installation medium)
  • A YubiKey with challenge response mode support into the target machine.
⚠︎
Warning: Make sure to obtain a YubiKey which supports challenge response mode. As of 2025, only YubiKey 5 Series and YubiKey 5 FIPS Series support it. YubiKey Bio Series and Security Key Series do not.

Setup

Install the packages required by the next steps to the live system and make two bash helper functions available.

Automatic Setup

Enter the nix-shell expression defined by this repository.

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

Now go to #Set up the YubiKey.

Manual Setup

Alternatively, you can manually set up the dependencies.

Packages:

$ nix-shell -p gcc yubikey-personalization openssl

Helper functions:

  • Convert a raw binary string to a hexadecimal string
  • Convert a hexadecimal string to a raw binary string

Note that you can copy and paste these functions into the bash shell directly to define them.

rbtohex() {
    ( od -An -vtx1 | tr -d ' \n' )
}

hextorb() {
    ( tr '[:lower:]' '[:upper:]' | sed -e 's/\([0-9A-F]\{2\}\)/\\\\\\x\1/gI'| xargs printf )
}

We need to compile OpenSSL's key derivation function, which is the same one as run on start up. To compile, run the following command.

Note: Because this will put the program in the current directory (rather than your PATH), replace pbkdf2-sha512 commands with ./pbkdf2-sha512.

cc -O3 \
   -I$(nix-build "<nixpkgs>" --no-build-output -A openssl.dev)/include \
   -L$(nix-build "<nixpkgs>" --no-build-output -A openssl.out)/lib \
   $(nix eval "(with import <nixpkgs> {}; pkgs.path)")/nixos/modules/system/boot/pbkdf2-sha512.c \
   -o ./pbkdf2-sha512 -lcrypto

If the newlines in the above snippet are problematic for your terminal, you can use the snippet below. It is the same command but as one line.

cc -O3 -I$(nix-build "<nixpkgs>" --no-build-output -A openssl.dev)/include -L$(nix-build "<nixpkgs>" --no-build-output -A openssl.out)/lib $(nix eval "(with import <nixpkgs> {}; pkgs.path)")/nixos/modules/system/boot/pbkdf2-sha512.c -o ./pbkdf2-sha512 -lcrypto

Set up the YubiKey

Step 1: Program the YubiKey's Configuration Slot 2 in Challenge-Response mode (HMAC-SHA1) if you haven't done yet.

⚠︎
Warning: This will overwrite existing configuration.
⚠︎
Warning: If you have a YubiKey VIP, Slot 1 should contain a Symantec VIP credential by default, which can only be programmed during manufacture.
# SLOT=2
# ykpersonalize -"$SLOT" -ochal-resp -ochal-hmac

Alternatively, YubiKey Personalization Tool (GUI) yubikey-personalization-gui may be suitable for more complicated operations, such as programming multiple keys at the same time or setting configuration protection.

Step 2: Gather the initial salt for the PBA (set its length to what you find time-feasible on your machine).

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

Step 3: Get the user passphrase used as the second factor in the PBA.

If you plan on using a user password during the boot process (instead of an unassisted boot), enter a user password. Your choice here will change the command you run in step 8.

# read -s USER_PASSPHRASE
⚠︎
Warning: Make very sure, that $USER_PASSPHRASE contains the correct user passphrase, or you will not be able to access your system after shutting down the live system.


Step 4: Calculate the initial challenge and response to the YubiKey.

# CHALLENGE="$(echo -n $SALT | openssl dgst -binary -sha512 | rbtohex)"
# RESPONSE=$(ykchalresp -2 -x $CHALLENGE 2>/dev/null)

Step 5: Derive the Luks slot key from the two factors.

Set the length of the Luks slot key and the cipher appropriately.

As an example, we will use AES-256, so we set the Luks device slot key length to 512 bit.

Set the iteration count used for PBKDF2 to a high value still time-feasible for your machine.

# KEY_LENGTH=512
# ITERATIONS=1000000

If you choose to authenticate with a user password, use the following line to generate the luks key.

# LUKS_KEY="$(echo -n $USER_PASSPHRASE | pbkdf2-sha512 $(($KEY_LENGTH / 8)) $ITERATIONS $RESPONSE | rbtohex)"

If you choose to authenticate without a user passphrase (not recommended), use this instead of the line above

# LUKS_KEY="$(echo | pbkdf2-sha512 $(($KEY_LENGTH / 8)) $ITERATIONS $RESPONSE | rbtohex)"

To test if the key is programmed correctly, you can challenge the yubikey and check that the response is the expected response previously generated (echo $response).

Partitioning

Create a GPT partition table and two partitions on the target disk.

  • Partition 1: This will be the EFI system partition: FAT32 etc., EF00 Linux filesystem (boot/esp), >100MB
  • Partition 2: This will be the Luks-encrypted partition, aka the "luks device": 8300 Linux filesystem, Rest of your disk

In the following we will use variables for identification, so set them to match your partition setup, e.g. like this:

# EFI_PART=/dev/sda1
# LUKS_PART=/dev/sda2

If you use an nvme drive your partition names will be something like /dev/nvme0n1p1 instead of /dev/sda1.

Setup the LUKS device

Step 6: Create the necessary filesystem on the efi system partition, which will store the current salt for the PBA, and mount it.

# EFI_MNT=/mnt/boot
# mkdir "$EFI_MNT" # make mount point
# mkfs.vfat -F 32 -n uefi "$EFI_PART" # format with FAT32
# mount "$EFI_PART" "$EFI_MNT" # mount to store salt and iteration later


Step 7: Decide where on the efi system partition to store the salt and prepare the directory layout accordingly.

# STORAGE=/crypt-storage/default
# mkdir -p "$(dirname $EFI_MNT$STORAGE)"


Step 8: Store the salt and iteration count to the EFI systems partition.

# echo -ne "$SALT\n$ITERATIONS" > $EFI_MNT$STORAGE


Step 9: Create the LUKS device.

  • Set the cipher used by LUKS appropriately
  • Set the hash used by LUKS appropriately
# CIPHER=aes-xts-plain64
# HASH=sha512
# echo -n "$LUKS_KEY" | hextorb | cryptsetup luksFormat --cipher="$CIPHER" --key-size="$KEY_LENGTH" --hash="$HASH" --key-file=- "$LUKS_PART"


Step 10: Open the LUKS device.

Open the LUKS device.

# LUKSROOT=encrypted
# echo -n "$LUKS_KEY" | hextorb | cryptsetup luksOpen $LUKS_PART $LUKSROOT --key-file=-

We can now access the volume at /dev/mapper/$LUKSROOT. For example, to format it as ext4

# mkfs.ext4 /dev/mapper/$LUKSROOT

Now go to #NixOS_installation.

LVM setup (optional)

The following is one of many methods for setting up the LVM partition. For more information on creating logical volumes, see LVM.

Step 1: Setup the LUKS device as a physical volume.

The physical volume map can then be created.

# pvcreate "/dev/mapper/$LUKSROOT"


Step 2: Setup a volume group on the LUKS device.

Set the name for the volume group appropriately

# VGNAME=partitions
# vgcreate "$VGNAME" "/dev/mapper/$LUKSROOT"


Step 3: Setup two logical volumes on the Luks device.

  • Volume 1: This will be the swap partition: choose appropriate size, 2GB for example
  • Volume 2: This will be the main btrfs volume, of which all filesystem partitions will be subvolumes: Rest of the free space
# lvcreate -L 2G -n swap "$VGNAME"
# FSROOT=fsroot
# lvcreate -l 100%FREE -n "$FSROOT" "$VGNAME"

# vgchange -ay


Step 4: Create the swap filesystem.

# mkswap -L swap /dev/partitions/swap


Btrfs setup (optional)

These steps can mostly be followed the same for other filesystem types except calls to the btrfs command can be ignored.

Step 1: Create the main btrfs volume's filesystem.

# mkfs.btrfs -L "$FSROOT" "/dev/partitions/$FSROOT"

Should the above fail, you might have encountered a bug that can be solved with doing the following, then attempting the above again:

# mkdir /mnt-root
# touch /mnt-root/nix-store.squashfs


Step 2: Mount the main btrfs volume.

# mount "/dev/partitions/$FSROOT" /mnt


Step 3: Create the subvolumes, for example "root" and "home".

# cd /mnt
# btrfs subvolume create root
# btrfs subvolume create home


Step 4: Create mountpoints on the root subvolume and finalise things for NixOS installation.

# umount /mnt
# mount -o subvol=root "/dev/partitions/$FSROOT" /mnt

# mkdir /mnt/home
# mount -o subvol=home "/dev/partitions/$FSROOT" /mnt/home

# mkdir /mnt/boot
# mount "$EFI_PART" /mnt/boot

# swapon /dev/partitions/swap


NixOS installation

Step 1: Mount drives.

# umount $EFI_PART
# mount "/dev/mapper/$LUKSROOT" /mnt # mount luks device first
# mkdir /mnt/boot # create mount point
# mount "$EFI_PART" /mnt/boot # mount efi partition next

Step 2: Generate (hardware) config.

# nixos-generate-config

Step 3: Modify config.

Replace anything that looks like a Bash variable with the value that it currently holds for in your shell and modify as needed.

❄︎ /mnt/etc/nixos/configuration.nix
# Minimal list of modules to use the EFI system partition and the YubiKey
boot.initrd.kernelModules = [ "vfat" "nls_cp437" "nls_iso8859-1" "usbhid" ];

# Enable support for the YubiKey PBA
boot.initrd.luks.yubikeySupport = true;

# Configuration to use your Luks device
boot.initrd.luks.devices = {
  "$LUKSROOT" = {
    device = "$LUKS_PART";
    # preLVM = true; # You may want to set this to false if you need to start a network service first
    yubikey = {
      slot = "$SLOT";
      twoFactor = true; # Set to false if you did not set up a user password.
      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 = "$EFI_PART";
      };
    };
  }; 
};
Note: More options are available (See boot.initrd.luks.devices.<name>.yubikey).

Step 4: Install NixOS.

# nixos-install

Headless setup note

If you have set up your system to not use a user password and attempt to boot the system, you may find the system stalls with the following message:

"Gathering entropy for new salt (please enter random keys to generate entropy if this blocks for long)..."

If you see this message and no more dots appear after a while, you have run into a situation where the random number generator does not have enough entropy stored up. You can mitigate this by starting a network interface (assuming the device is on a network), which should fill the entropy pool and allow the computer to boot headless. Below is an example configuration that has been tested to work in a headless configuration.

❄︎ /mnt/etc/nixos/configuration.nix
boot = {
    # Used to make this device dhcp enabled during boot.
    kernelParams = [
      "ip=:::::eth0:dhcp" # Change to the appropriate IP kernel command.
    ];

    initrd = {
      network.enable = true;

      # This is the driver for a particular ethernet card. See `boot.network.enable` for more details.
      availableKernelModules = ["alx"]; 

      kernelModules = ["vfat" "nls_cp437" "nls_iso8859-1" "usbhid" "alx"];
      luks = {
        cryptoModules = [ "aes" "xts" "sha512" ];
        yubikeySupport = true;
        devices = [ {
          name = "$LUKSROOT";
          device = "$LUKS_PART";
          preLVM = false;
          yubikey = {
            slot = $SLOT;
            twoFactor = false;
            storage = {
              device = "$EFI_PART";
            };
          };
        } ];
      };
    };
  };

Finally, clean up and you should be ready to reboot into your new system.

Maintenance

Prerequisite: You'll need the environment, defined in #Automatic Setup.

You may want to modify your LUKS setup. The following commands (values from above assumed, replace them to match your configuration) will help you in generating the necessary value for the --key-file option.

# Be sure to delete luks.key afterwards
KEY_LENGTH=512
ITERATIONS=1000000
read -s USER_PASSPHRASE
CHALLENGE=$(head -n1 /boot/crypt-storage/default | tr -d '\n' | openssl dgst -binary -sha512 | rbtohex)
RESPONSE="$(ykchalresp -2 -x $CHALLENGE 2>/dev/null)"
echo -n $USER_PASSPHRASE | pbkdf2-sha512 $(($KEY_LENGTH / 8)) $ITERATIONS $RESPONSE > luks.key
# Now, you can pass the luks.key to any cryptsetup command. For instance,
# if you want to add another key to your setup.
cryptsetup luksAddKey /dev/nvme0n1p2 luks.key
rm luks.key