How to Install NixOS With Full Disk Encryption (FDE) using LUKS2, Detached LUKS Header, and A Separate Boot Partition on an USB/MicroSD Card

This tutorial is a guide on installing NixOS, with a separate /boot partition and full-disk-encryption (FDE) using LUKS2 with a detached header.

A photograph of cyphers written by hand on a piece of paper.
Encryption Examples. Image Source: Wikimedia Commons.

This tutorial is a guide on installing NixOS, with a separate /boot partition and full-disk-encryption (FDE) using LUKS2 with a detached header. This guide is different from other tutorials, because it offers the following features:

  • Full disk encryption using the newer LUKS2 container format, instead of LUKS1.
  • Separate /boot partition on portable device (i.e. USB, MicroSD Card).
  • Detached LUKS2 header on separate portable device, offering deniable encryption.

Example Setup

Using this guide, I installed NixOS on a Purism Librem 14 laptop, running Coreboot with Tianocore, an open-source implementation of UEFI. Using the laptop's on-board MicroSD Card reader, I installed both the \boot partition and the LUKS2 header located on a MicroSD card.

When the MicroSD card is removed from the laptop, the hard drive appears to be unformatted, without the appearance of an operating system at all.

Prerequisites

You must have an USB stick imaged with the appropriate NixOS installation image.

In order to follow this tutorial, two devices are necessary. You will need a primary device (e.g. hard drive, SSD) - which we will denote as /dev/sda. This device holds the LUKS2 container.

You will also need a second, portable device (e.g. USB, MicroSD card) - which we will denote as /dev/sdb. This device will hold the /boot partition, as well as the detached LUKS2 header.

If you plan to use a SD or MicroSD card, make sure to get a high-endurance version that uses SLC or MLC flash. These cards are slower, but allows more write-cycles.

This tutorial is written for a user that has an intermediate familiarity with Linux operating systems, but who is new to NixOS specifically. You do not need to know how to manually install an operating system. Every single step will be presented in this tutorial, including steps for partitioning.

Warning: be extremely careful with disk labels when following this tutorial. Devices and partitions may appear under different labels on your system. Using commands like dd or cryptsetup luksFormat will cause permanent data loss! Use lsblk in order to check disk labels if at all unsure.

All commands must be run as the root user. This may be done using sudo, or by opening a root shell. In order to open a root shell, simply run:

sudo su root

Prepare Disks

This step is entirely optional. We will prepare the disk devices by erasing them. We will first zeroise /dev/sdb, in order to completely wipe it.

dd if=/dev/zero of=/dev/sdb status=progress

Next, we will wipe /dev/sda by filling it with random data.

dd if=/dev/urandom of=/dev/sda status=progress

The reason we wipe the primary device by filling it with random data, instead of simply zero-ising it, is to make the size of the encrypted content undeterminable.

Now /dev/sda and /dev/sdb are fully prepared. We are ready to create partition tables, and partitions within them.

Create Partitions

We will have the following partition scheme. The specific method (or scenario) that we will implement for our Full Disk Encryption (FDE) is to mount an LVM (Logical Volume Manager) on top of our LUKS2 Container. LVM volumes are then created to reflect the different partitions on our root filesystem. This method is referred to as LVM on LUKS and is a common method of implementing FDE on Linux devices.

/dev/sda Primary device

  • /dev/sda1 LUKS2 Container crypted
  • LVM Volume Group vg
  • LVM Logical Volume vg-swap
  • LVM Logical Volume vg-nixos
  • and any other partitions...

/dev/sdb Portable device

  • /dev/sdb1 boot Partition
  • /dev/sdb2 LUKS2 Detached Header

The underlying root \, \home, and swap partitions will be created as LVM virtual volumes within the LUKS2 container.

We will first proceed to make the partition table and partition for the primary /dev/sda device, then for the portable /dev/sdb device. We will be using parted as the tool to create partitions.

Make root partition on primary device /dev/sda

We will be using the parted command-line tool to create all of our partitions.

Create a gpt partition table for /dev/sda. Other partition table formats like msdos are not necessary.

parted /dev/sda -- mklabel gpt

Create primary partition for /dev/sda. This will take up all of the space on the primary /dev/sda device, since it will hold a LUKS2 encryption container. Other partitions (such as swap, or /home) will be created within the encryption container.

parted /dev/sda -- mkpart primary 0% 100%

The partition setup for the primary /dev/sda device is now complete. We will now proceed to partition the /dev/sdb portable device (i.e. the USB, MicroSD card).

Make boot partition on portable device /dev/sdb

Create a gpt partition table for /dev/sdb.

parted /dev/sdb -- mklabel gpt

Create boot partition for /dev/sdb. This is the unencrypted partition that will contain the bootloader for the operating system. It will reside on the portable device, which should be stored securely when not in use.

parted /dev/sdb -- mkpart ESP fat32 0% 50%

We must set some flags to indicate that this partition contains the bootloader.

parted /dev/sdb -- set 1 boot on

Now we will use the remaining space on the portable device to create a partition for the LUKS2 detached header. This partition will not actually contain a filesystem, since the LUKS2 detached header will be read as a raw header.

parted /dev/sdb -- mkpart primary 50% 100%

I choose to devote the remaining 50% of the device's space, simply because the MicroSD card will not be used for any other purpose. However, in practice a smaller partition can be allocated for the LUKS2 detached header. Unlike LUKS1, the detached header does not have a static, fixed size. However in practice a partition of 16MB should be more than enough.

Make filesystem for boot partition

We will make a FAT32 filesystem for the boot partition. We are using FAT32 instead of ext4 for greater compatibility with bootloaders.

mkfs.fat -F 32 -n boot /dev/sdb1

It is not necessary to make a filesystem for the LUKS2 header partition /dev/sdb2. This is because the LUKS2 header will reside as a raw header without any underlying file system. This avoids the complications of the bootloader having to mount a filesystem before reading the header file.

Make LUKS2 Encryption Container with detached headers

We will now use the cryptsetup command to create the LUKS2 Encryption container. We will invoke the luksFormat action on /dev/sda1 in order to do so. The command comes with a set of sensible and sane cryptographic defaults, however you may choose to explore further configuration options if you desire.

The location of the LUKS2 wrapper is on the primary hard drive /dev/sda1.

The LUKS2 header which contains the metadata is on raw partition /dev/sdb2

This command will prompt you to enter a password. This password will be used to unlock the primary device when the computer boots.

cryptsetup luksFormat /dev/sda1 --type luks2 --header /dev/sdb2

Open LUKS2 Container

A LUKS2 container has been created on /dev/sda1, with it's corresponding header file located at /dev/sdb2.

In order to use this container, we must unlock it using the following command. You will be asked to input the decryption password from the previous step.

cryptsetup luksOpen /dev/sda1 crypted --header /dev/sdb2

Now the container will be available as crypted, at /dev/mapper/crypted.

Create a LVM Volume within the LUKS2 Container

We will now create a set of LVM volumes within the LUKS2 container. This way we may have other multiple logical partitions within the container itself.

First, we will initialise the physical volume crypted. This step is necessary in order to create logical volumes within it.

pvcreate /dev/mapper/crypted

Next, we will create a volume group upon the newly-initialised physical volume. This volume group will be called vg.

vgcreate vg /dev/mapper/crypted

Now that the volume group is made, we can make arbitrary logical volumes. These logical volumes correspond to the unencrypted partitions of a traditional Linux installation.

Although it is possible to make multiple partitions corresponding to different mount points (such as /home, /etc, var, etc), in practice this is not necessary. This is because the main benefit of having a separate /home partition is easier recovery, where the /home partition can be mounted separately in a rescue process.

Because all partitions reside within the encrypted LUKS2 container, which must be unlocked for access, there is no benefit to having separate partitions. Hence we will only create a root and swap partition.

Create a swap partition within the LVM volume

Create a swap partition with the label swap. Note that we use the option -L which denotes a numerical size. In the following command, a 16 GB swap partition is created.

lvcreate -L 16G -n swap vg

There are differing opinions on the appropriate size for a swap partition on modern Linux. In particular, if you wish to to enable hibernation you must have the same amount of swap as your RAM.

Create a root partition within the LVM volume

Create a root partition with the label nixos using the remaining free space. Note that we use the option -l which denotes a percentage.

lvcreate -l '100%FREE' -n nixos vg

Now the LVM volumes are created, and ready to be used.

Create Filesystem and Swap

After creating the volumes, we must create filesystems that will reside on them. For this guide, we will use a relatively ordinary ext4 filesystem. You may substitute more exotic filesystems such as ZFS or btfs if desired.

mkfs.ext4 -L nixos /dev/vg/nixos
mkswap -L swap /dev/vg/swap

After creating the filesystems, we will mount them (and activate swap).

Mount filesystems

Mount the root partition

mount /dev/disk/by-label/nixos /mnt

Mount the boot partition

mkdir -p /mnt/boot
mount /dev/sdb1 /mnt/boot

Activate swap

swapon /dev/vg/swap

Now our filesystems are mounted. The future NixOS installation's root will be accessible at /mnt, and it's boot partition at /mnt/boot.

Configure NixOS

We can now instruct NixOS to generate a set of configuration files for our installation. Make sure to pass the --root /mnt flag, in order to indicate where the root filesystem resides.

nixos-generate-config --root /mnt

Now the configuration files will be available at /mnt/etc/nixos. We must modify this file in order to add the appropriate settings.

Configure configurations.nix

We can now specify the configuration of the NixOS installation using configurations.nix. The included example configuration file has various options that can be explored. In particular, you should check out the following:

Once generic configurations are complete, we must add the specific boot.initrd.luks.devices settings which will allow the system to boot.

The following section must be added. We are specifying for NixOS that there is a dictionary of boot.initrd.luks.devices, where there exists a device crypted with configuration options enclosed in another dictionary.

# Configuration options for LUKS Device
boot.initrd.luks.devices = {
  crypted = {
    device = "/dev/disk/by-partuuid/<PARTUUID of /dev/sda1>";
    header = "/dev/disk/by-partuuid/<PARTUUID of /dev/sdb2>";
    allowDiscards = true; # Used if primary device is a SSD
    preLVM = true;
  };
};

We must specify the device directive which is a path that points to the encrypted primary partition /dev/sda1, as well as header which is a path that points to the LUKS2 Header located on /dev/sdb2.

These paths can be specified in more than one way. You can see the various ways that this path is specified by listing the subdirectories in /dev/disk.

I recommend specifying the paths using /dev/disk/by-partuuid instead of /dev/disk/by-uuid, because /dev/sda1 does not have a uuid assigned to it, since it doesn't have a filesystem.

Hence in order to find the UUIDs of /dev/sda1 and /deb/sdb2, run:

blkid /dev/sda1 -s PARTUUID

Or you may simply run ls -l /dev/disk/by-partuuid

Installation

After setting the configuration options, it is time to install the device. You will need to have an internet connection, as NixOS will be downloading and compiling sources. Either connect to an ethernet cable, or a WiFi network.

Now run nixos-install. We will specify the option --cores 0 to let NixOS use all CPU cores when compiling binaries.

nixos-install --root /mnt --cores 0

This command will take anywhere from 5 to 25 minutes, depending on the configuration of the system and the speed of the internet connection.

Once the installation is nearly complete, it will prompt you to set a root password for the newly installed system. After setting the password, the installation is complete.

reboot

Post-Installation

When the installation is complete, perform a reboot. Now you must enter the BIOS/UEFI interface of your computer's firmware, and change it's boot settings to use the bootloader located on the portable device /dev/sdb1 by default.

Now your NixOS installation is complete!