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.
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 Containercrypted
- 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!