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 on a UEFI system. The YubiKey Pre Boot Authentication (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.
This guide was tested to work on NixOS 19.03 as of July 2019.
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 setup, e.g. like this:
EFI_PART=/dev/sda1 LUKS_PART=/dev/sda2
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, with these changes:
Remove all detected filesystems from hardware-configuration.nix.
Add the following to your configuration.nix (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;
# You may want to enable the network interface. During
# 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 = {
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 partially 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 = {
twoFactor = false;
storage = {
device = "$EFI_PART";
};
};
} ];
};
};
};
Finally, clean up and you should be ready to reboot into your new system.
This is a 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