Yubikey based Full Disk Encryption (FDE) on NixOS
This page is a minimalistic guide for setting up LUKS-based full disk encryption with YubiKey pre-boot authentication (PBA) on a UEFI system using the BRTFS file system (although any file system can be used). The YubiKey PBA in NixOS currently features two-factor authentication using a (secret) user passphrase and a YubiKey in challenge-response mode. The described method also works without a user password, although this is not preferred. 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 19.03 as of July 2019. Also see this repository that provides a nix-shell expression as described in this guide.
Requirements
- A NixOS live system booted in UEFI mode on the target machine.
- A YubiKey Standard plugged into the target machine with a free configuration slot (that will be overwritten).
Partitioning
Create a GPT partition table and two partitions on the target disk.
- Partition 1: This will be the EFI system partition: 100MB-300MB
- Partition 2: This will be the Luks-encrypted partition, aka the "luks device": 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 1: Create the necessary filesystem on the efi system partition, which will store the current salt for the PBA, and mount it.
EFI_MNT=/root/boot mkdir "$EFI_MNT" mkfs.vfat -F 32 -n uefi "$EFI_PART" mount "$EFI_PART" "$EFI_MNT"
Step 2: 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 3: Install the packages required by the next steps to the live system and make two bash helper functions available.
Packages:
- A C compiler, e.g. gcc
- The YubiKey Personalization command line tool
- OpenSSL
nix-env -i gcc-wrapper nix-env -i yubikey-personalization nix-env -i 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 ) }
Step 4: Compile a small C program (shipped with NixOS) that allows for direct access to OpenSSL's PBKDF2 implementation.
cc -O3 -I$(find / | grep "openssl/evp\.h" | head -1 | sed -e 's|/openssl/evp\.h$||g' | tr -d '\n') \ -L$(find / | grep "lib/libcrypto" | head -1 | sed -e 's|/libcrypto\..*$||g' | tr -d '\n') \ $(find / | grep "pbkdf2-sha512\.c" | head -1 | tr -d '\n') -o ./pbkdf2-sha512 -lcrypto
Step 5: 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 6: Gather the random secret key for the YubiKey (Important: This key should not be stored anywhere other than the YubiKey, as that poses a security risk).
k_yubi="$(dd if=/dev/random bs=1 count=20 2>/dev/null | rbtohex)"
Step 7: Get the user passphrase used as the second factor in the PBA.
read -s k_user
Step 7.5: Make very sure, that $k_user contains the correct user passphrase, or you will not be able to access your system after shutting down the live system.
Step 8: Calculate the initial challenge to the YubiKey.
challenge="$(echo -n $salt | openssl dgst -binary -sha512 | rbtohex)"
Step 9: Calculate the response the YubiKey should give to that challenge (Only possible because right now we still know the secret key for the YubiKey).
response="$(echo -n $challenge | hextorb | openssl dgst -binary -sha1 -mac HMAC -macopt hexkey:$k_yubi | rbtohex)"
Step 10: Derive the Luks slot key from the two factors.
Set the length of the Luks slot key and the ciphter 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 k_luks="$(echo -n $k_user | ./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
k_luks="$(echo | ./pbkdf2-sha512 $(($KEY_LENGTH / 8)) $ITERATIONS $response | rbtohex)"
Step 11: Create the LUKS device.
- Set the cipher used by LUKS appropriately
- Set the has used by LUKS appropriately
CIPHER=aes-xts-plain64 HASH=sha512 echo -n "$k_luks" | hextorb | cryptsetup luksFormat --cipher="$CIPHER" \ --key-size="$KEY_LENGTH" --hash="$HASH" --key-file=- "$LUKS_PART"
Step 12: Store the salt and iteration count to the EFI systems partition.
echo -ne "$salt\n$ITERATIONS" > $EFI_MNT$STORAGE
Step 13: Store the secret key for the YubiKey on the YubiKey and configure it accordingly.
SLOT=2 ykpersonalize -"$SLOT" -ochal-resp -ochal-hmac -a"$k_yubi"
To test if the key is programmed correctly, you can challenge the yubikey and check that the response is the expected response previously generated.
ykchalresp -2 "$(echo -n $challenge | hextorb)"
LVM setup
The following is one of many methods for setting up the LVM partition. Another popular guide can be found here: https://qfpl.io/posts/installing-nixos/
Step 1: Setup the LUKS device as a physical volume.
The LUKS device first needs to be unlocked.
LUKSROOT=nixos-enc echo -n "$k_luks" | hextorb | cryptsetup luksOpen $LUKS_PART $LUKSROOT --key-file=-
The physical volumje 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
These steps can mostly be followed the same for other filesystem types except calls to the brtfs 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
Configure and install NixOS as you normally would. Add the following to your configuration.nix, noting that more options are available here: https://nixos.org/nixos/options.html#yubikey.
Replace anything that looks like a Bash variable with the value that it currently holds for in your shell and modify as needed.
# Minimal list of modules to use the EFI system partition and the YubiKey
boot.initrd.kernelModules = [ "vfat" "nls_cp437" "nls_iso8859-1" "usbhid" ];
# Crypto setup, set modules accordingly
boot.initrd.luks.cryptoModules = [ "aes" "xts" "sha512" ];
# Enable support for the YubiKey PBA
boot.initrd.luks.yubikeySupport = true;
# Configuration to use your Luks device
boot.initrd.luks.devices = [ {
name = "$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.
storage = {
device = "$EFI_PART";
};
};
} ];
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.
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.
This is a modified copy of the page from the original wiki. The original page can be found here: https://web.archive.org/web/20160911070220/https://nixos.org/wiki/Luks-based_FDE_with_Yubikey_PBA_and_btrfs_on_UEFI_NixOS