Yubikey based Full Disk Encryption (FDE) on NixOS

From NixOS Wiki
Revision as of 06:59, 5 April 2024 by MikiBot (talk | contribs) (Add warning about the new wiki)
Jump to: navigation, search
Warning: You are reading an article on the deprecated unofficial wiki. For the up to date version of this article, see https://wiki.nixos.org/wiki/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 22.11 as of January 2023. Also see this repository that provides a nix-shell expression as described in this guide. It provides the environment for setting up the yubikey.

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.
  • A YubiKey Standard plugged into the target machine with a free configuration slot (that will be overwritten).

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

Manual Setup

Alternatively, you can manually set up the dependencies.

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 )
}

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: Configure the Yubikey

SLOT=2
ykpersonalize -"$SLOT" -ochal-resp -ochal-hmac

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 k_user

Step 3.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 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.

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)"

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: 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 6: 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 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 "$k_luks" | hextorb | cryptsetup luksFormat --cipher="$CIPHER" \ 
  --key-size="$KEY_LENGTH" --hash="$HASH" --key-file=- "$LUKS_PART"


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 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

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://search.nixos.org/options/?query=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" ];

# 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.
      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.

Maintenance

Prerequisite: You'll need the environment, defined in Yubikey based Full Disk Encryption (FDE) on NixOS#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 k_user
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 $k_user | 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


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