Using a YubiKey to unlock LUKS and Root on ZFS with native encryption
Recreating the partition layout of Ubuntu 25.04 with encrypted ZFS and using a YubiKey with a custom dracut module instead of a passphrase to unlock and boot Gentoo Linux.
First published:
1 June 2025.
I have been using Gentoo Linux with root on ZFS for a while now, but I have not yet used an encrypted setup. When attempting to create an encrypted root pool, I decided to first examine how Ubuntu implemented this and then enhance it with hardware-based authentication using a YubiKey.
ZFS: A Brief History and Overview
ZFS (Zettabyte File System) was originally developed by Sun Microsystems for Solaris in the mid-2000s as a revolutionary approach to storage management. Following Oracle's acquisition of Sun, the open-source community created ZFS on Linux (ZoL) to bring ZFS capabilities to Linux systems. Today, the OpenZFS project maintains a unified codebase that runs across multiple operating systems, including Linux, FreeBSD, and others.
ZFS stands out as a combined file system and logical volume manager with several compelling advantages:
- Data integrity: Built-in checksumming and automatic error correction protect against silent data corruption.
- Snapshots and clones: Instant, space-efficient snapshots enable easy backups and rollbacks.
- Compression: Transparent compression can significantly reduce storage requirements.
- RAID-Z: Software RAID that combines multiple drives for redundancy.
- Copy-on-write: Ensures consistency and enables advanced features like snapshots.
- Native encryption: Dataset-level encryption support eliminates the need for separate full-disk encryption layers.
Understanding ZFS Terminology
To work effectively with ZFS, it's essential to understand its core concepts:
- Zpool: The top-level storage pool consisting of one or more storage devices, similar to a RAID array but with more advanced features.
- Dataset: A file system within a zpool that can be mounted and has its own properties like compression, encryption, and quotas.
- Zvol: A block device (volume) within a zpool that can be used like a traditional partition.
Ubuntu's Encrypted ZFS Implementation
Ubuntu provides encrypted ZFS in its distribution, offering a well-designed reference implementation. While I'm primarily interested in the ZFS features rather than Ubuntu specifically, their layout provides an excellent foundation to build upon. Ubuntu creates four partitions for encrypted ZFS systems:
- An EFI System Partition (ESP) formatted with FAT32 and used for GRUB's EFI executable.
- A ZFS
/boot
partition (calledbpool
) to store the kernel, initramfs, and bootloader configuration. - An encrypted LUKS container that contains the swap partition.
- An encrypted ZFS pool (
rpool
) that stores the root partition and all relevant datasets.
The /boot
partition remains separate from the root's ZFS pool because GRUB does not support all ZFS features. Thus the /boot
partition is created with a feature set compatible with GRUB.
Interestingly, rpool
contains an unencrypted zvol called keystore-rpool
. This zvol contains a LUKS container that is encrypted with a passphrase. Within that LUKS container, there is an ext4 partition that stores a key file which is ultimately used to decrypt the swap partition and rpool
. When booting, Ubuntu asks for a password for the zvol containing the LUKS container with the key file, then uses that key file to unlock swap and rpool
.
Enhanced Design with YubiKey Authentication
At this point, I wondered if it was possible to replace the password-protected zvol with a YubiKey while keeping the remaining layout compatible. This approach offers several compelling benefits over traditional password-based encryption.
Hardware Security Keys
A YubiKey is a hardware security key manufactured by Yubico that provides strong two-factor authentication and supports multiple protocols including OpenPGP. For our use case, the YubiKey serves as a secure storage device for cryptographic keys, eliminating the need to enter passphrases during boot while maintaining strong security.
The YubiKey's advantages include:
- Hardware-based security: Private keys never leave the device.
- Portability: Small form factor makes it easy to carry.
- Durability: Designed to withstand physical stress.
- Multi-protocol support: Works with OpenPGP, FIDO2, PIV, and other standards.
Cryptographic Architecture
Instead of Ubuntu's symmetric encryption approach, our design uses asymmetric cryptography:
- GPG (GNU Privacy Guard): Handles asymmetric encryption and decryption operations.
- Private key: Stored securely on the YubiKey hardware, never accessible to software.
- Public key: Used to encrypt the key files and can be freely shared or backed up.
- Key files: Raw binary keys for LUKS and ZFS, encrypted with the public key.
- Backup strategy: Both the encrypted key files and the private key can be safely backed up.
Eventually, I decided to generate separate key files for swap and rpool
and encrypt them with an OpenPGP private key stored on my YubiKey. The encrypted key files are then built into the initramfs. During boot, the initramfs will prompt to unlock the YubiKey, write the decrypted key files to a tmpfs at /run/keys
, and then unlock swap and rpool
.
Design Considerations and Limitations
The partition layout remains identical to Ubuntu's implementation, providing several advantages:
- Compatibility: Future migration to Ubuntu's keystore approach remains possible.
- Flexibility: The encrypted key files could be stored on
/boot
or any other storage accessible to the initramfs. - Recovery: Traditional backup and recovery procedures still apply.
This implementation includes some intentional limitations that could be addressed in future iterations:
- The dracut module hard-codes file paths for simplicity.
- A more sophisticated design with dynamic path discovery is out of scope for this post.
- Enhanced cleanup mechanisms may be implemented in later versions.
Security and Memory Management
The keys are decrypted to /run/keys
which is mounted on a tmpfs (memory-based filesystem). While keys residing in memory don't pose a significant security risk for encrypted-at-rest data, minimizing their lifetime is still desirable. The dracut module includes a cleanup script that attempts to overwrite the keys with zeros as soon as they are no longer needed.
However, I must acknowledge uncertainty about whether this approach effectively overwrites the keys, as I'm not entirely familiar with how tmpfs manages memory allocation and deallocation. Even if the explicit overwriting doesn't guarantee memory clearing, the data remains protected at rest due to the full-disk encryption, and the keys have a limited lifetime within the boot process.
When setting up this project, I decided to use the desktop live ISO from Ubuntu 25.04 «Plucky Puffin» and I will assume an empty storage device. In this case, I will install to /dev/vda
. To keep the overhead of configuring Gentoo Linux minimal, I decided to use gentoo-kernel-bin
along with dracut
.
Setting up the YubiKey
We will use GnuPG to create the OpenPGP key locally, then move the private key to the YubiKey. This has the advantage that the secret key can be backed up and possibly moved to a second YubiKey. Of course, you can also generate the key pair on the YubiKey itself if you prefer. In this case, you cannot export the private key, but you can still back up the key files. However, this is out of scope for this blog post.
For this tutorial, I decided it is easiest to generate the OpenPGP keys when booted into the Ubuntu live installer. So at this stage, boot up the Ubuntu live installer, close the setup window, and open a shell. Initially, I will not work as root
. You can see the current user and environment in the shell prompt. Begin by installing packages:
Ensure that the YubiKey is detected. gpg --card-status
should show your plugged-in YubiKey. You need some workarounds for scdaemon
according to this [blog post][1].
We will now create the OpenPGP key pair locally, then create an export/backup, and finally move the secret key to the YubiKey. Begin by generating the key pair:
When prompted, I prefer to select ECC (sign and encrypt)
and Curve 25519
. Also fill in the remaining data, i.e., set an expiration time and your key information for identification. Make sure to select a strong password and store it in your password manager. You are only expected to use the password in case of recovery.
You can then list the keys with the --keyid-format long
parameter:
Copy your key identifier (the part after ed25519/
—in this case AEBE2C18B81E0004
) and export the public key:
And also export the secret key:
Now is a good time to create a backup of these two keys. public.asc
contains no secrets. private.asc
contains your secret private key, which is encrypted with the password you chose earlier. If you have chosen a good (!) password, you can send them via email.
Now we want to move the secret key to the YubiKey. The YubiKey should still be plugged in. In fact, don't remove it before we have booted into our Gentoo system.
; )
)
)
Select the appropriate key. In this case, type key 1
.
)
Now move the secret key to the YubiKey:
()
)
Finalize by typing save
:
We have finished setting up the YubiKey. We can now move on to creating the key files.
Creating the key files
We create the key files by piping 32 bytes (256 bits) from /dev/random
to gpg
and encrypting them with our public key:
|
|
Next, we decrypt the keys into /run/keys
. /run
is mounted on a tmpfs and we will also use it later in our initramfs. First, create the /run/keys
folder and give it user permissions for now:
Now, decrypt the keys to /run/keys/rpool.key
and /run/keys/swap.key
.
Note that we could have directly generated the keys in /run/keys
and then encrypted them with our public key. However, this serves as a useful test that decryption works correctly.
From now on, we want to start working as root
and we do not need user access to the keys anymore. So we change the permissions to be readable only by root
:
&&
Now become root
:
Disk partitioning and file system creation
I intend to create four partitions on /dev/vda
just as the Ubuntu installer does. Using parted
, we will create a 512 MiB EFI System Partition, a 1 GiB partition for /boot
, an 8 GiB partition for swap
, and use the remaining space for the ZFS root pool:
()
()
()
()
()
()
()
()
)
)
()
Next, the EFI System Partition is formatted with FAT32:
)
The /boot
partition is formatted with ext4
:
)
)
Next, we will create the encrypted container that is used for swap
. It uses the swap.key
key file in /run/keys
. Using --batch-mode
prevents it from asking questions. After creation, we open the container and it will create the block device /dev/mapper/luks-swap
:
Now we format the device with mkswap
and enable swap with swapon
:
)
If we want the root dataset to be encrypted, we can only do so at zpool creation. Any child dataset will automatically inherit the encryption settings from its parent. Generate a hostid
and then create the zpool called rpool
on /dev/vda4
with the respective parameters:
We will create the datasets rpool/ROOT
and rpool/ROOT/gentoo
and set the latter to be the bootfs
of rpool
. Ubuntu creates more datasets than these, but I want to keep it simple here.
Now we will reimport rpool
and mount it to /mnt/gentoo
so that we can continue later with the official Gentoo handbook.
With /mnt/gentoo
being mounted, we can also mount the EFI System Partition into /mnt/gentoo/efi
:
And also mount our /boot
partition into /mnt/gentoo/boot
:
Ensure everything was mounted correctly:
|
)
)
)
Setting up for the Gentoo installation
Before we enter the chroot
, it makes sense to copy some files from Ubuntu to the Gentoo mountpoint. I like to have the GPG-encrypted key files in /boot/keys
. We need to have them accessible somewhere for our setup and I also like this location for recovery purposes.
Also copy the hostid
generated earlier from the Ubuntu host to the Gentoo installation:
Gentoo setup
Continue with «[The stage file][2]» and «[Installing base system][3]» from the «[Gentoo AMD64 Handbook][4]», then come back. You should now be within a chroot
in your Gentoo installation.
Configure /etc/fstab
with the appropriate UUIDs:
()()()()()()()UUID=BD91-ADB6
UUID=3cf82667-5f92-47b7-b48e-4b119b48632a
UUID=5ce443e6-2ea2-47f5-9cce-dcc5af917af9
Make sure to set USE="dist-kernel dracut zfs"
in your make.conf
when configuring USE
flags. Also set GRUB_PLATFORMS="efi-64"
in make.conf
. Also ensure that the USE
flags smartcard
and usb
are set for app-crypt/gnupg
:
()
I like to keep a set
with the essentials for booting the system in /etc/portage/sets
:
()# Base tools for system setup
# Kernel and firmware
# Bootloader
# ZFS support
# Encryption
# Optional tools
Now we can just emerge @essentials
.
Our custom dracut module
Dracut is a modern initramfs generation tool that creates initial ramdisk images for Linux systems. Unlike traditional approaches that copy static files, dracut dynamically assembles the initramfs based on the current system configuration and required modules. It provides a modular architecture where functionality is organized into discrete modules that can be selectively included. For our YubiKey integration, we need to create a custom dracut module that handles the GPG decryption process at boot time. The module must install the necessary hooks at precise points in the boot sequence:
- The decryption must occur before ZFS attempts to load keys
- The decryption must occur before LUKS attempts to open encrypted devices
- The cleanup must happen after both systems have successfully used the keys
Now we need to write our custom dracut module, configure dracut, and generate the initramfs. The module consists of three files: decrypt-keys.sh
, cleanup-keys.sh
, and module-setup.sh
. We will put these into /usr/lib/dracut/modules.d/99yubikey/
, which first needs to be created:
()
decrypt-keys.sh
waits for the YubiKey to be plugged in and asks the user for the PIN. It then starts to decrypt the GPG-encrypted key files into /run/keys
where they are expected by ZFS.
()#!/bin/sh
COLORS="\033[1;33m \033[1;36m \033[1;35m \033[1;32m"
RESET="\033[0m"
MSG=" Waiting for YubiKey..."
# Wait for the YubiKey to become available
while ; do
if ; then
break
fi
for; do
done
done
# Prepare key destination directory
# Import public key
# Suppress kernel messages during PIN entry
# Try to decrypt keys up to 3 times
success=0
for; do
if && \
; then
success=1
break
fi
done
# Restore kernel messages
# Check if decryption succeeded
if [ || [; then
fi
cleanup-keys.sh
attempts to overwrite the keys with zeros as soon as they are no longer needed. Since we are operating within a tmpfs
, I should note that I'm uncertain whether this approach effectively overwrites the keys in memory, as the behavior of tmpfs memory management isn't entirely clear to me.
()#!/bin/sh
module-setup.sh
installs the above files into the initramfs, along with all their dependencies, and ensures that they are run at the appropriate time. decrypt-keys.sh
must be run before ZFS loads the keys and before LUKS tries to open the device. cleanup-keys.sh
must run after the devices have been opened.
()#!/bin/bash
When creating these files, don't forget to make them executable with chmod +x
.
We need to tell dracut to use our yubikey
module. Also, dracut needs to know which partition contains our LUKS container. For this, we create a custom.conf
in /etc/dracut.conf.d
and populate it with the relevant information:
()()()()()kernel_cmdline="rd.luks.uuid=1b2408a5-28df-4f21-9eff-dbd2c5749c9b rd.luks.name=1b2408a5-28df-4f21-9eff-dbd2c5749c9b=luks-swap rd.luks.key=/run/keys/swap.key:/"
add_dracutmodules+=" crypt yubikey zfs "
Finally, generate the new initramfs with dracut. We need to pass the kernel version with --kver
as the running kernel from Ubuntu will not match the installed kernel within Gentoo. The corresponding kernel version can be obtained with ls /boot
.
()
From the installation of the kernel and zfs-kmod
, there are likely old files in /boot
. These can be removed:
()
Finalizing the Installation
We are ready to finalize the installation.
We need to tell GRUB to boot the correct dataset. As of this writing, automatically finding the correct pool is unreliable, so we do it manually. In /etc/default/grub
, make sure to add root=ZFS=rpool/ROOT/gentoo
to GRUB_CMDLINE_LINUX
and make sure it is uncommented:
()GRUB_CMDLINE_LINUX="root=ZFS=rpool/ROOT/gentoo"
Now install GRUB to the /efi
directory:
()
Generate the configuration file for GRUB:
()
done
Finally, we configure the ZFS service to run at startup:
()
()
()
()
()
Don't forget to set a password for root:
()
Now we can exit the chroot:
()
Unmount the file systems of the chroot. Remember to also close the LUKS container, unmount the ZFS datasets, and export the zpool.
Conclusion and Future Work
Now you should be ready to boot into your new Gentoo installation and unlock swap and the root pool with your YubiKey.
This setup successfully combines hardware-based authentication with ZFS encryption while maintaining Ubuntu's proven partition layout. The YubiKey eliminates the need to enter passphrases during boot, which is particularly useful for systems that need to boot unattended or in environments where keyboard input might be compromised. The compatibility with Ubuntu's layout means that migration paths remain open, and the encrypted key files provide a robust backup strategy.
However, this implementation has several areas that could benefit from improvement. The hard-coded paths in the dracut module work well for a single-user setup but limit flexibility for different deployment scenarios. The memory cleanup approach using dd may not effectively clear keys from tmpfs, though the practical security impact is minimal given the full-disk encryption. Error handling could be more sophisticated, particularly for scenarios where the YubiKey is unavailable or malfunctioning.
For future iterations, dynamic path discovery would make the module more maintainable and adaptable to different system configurations. Support for multiple YubiKeys would provide hardware redundancy, addressing the single point of failure that the current design introduces. Network-based unlocking capabilities would be valuable for headless systems, though this would require careful consideration of the security implications.
The modular dracut approach proved effective for this implementation, making it straightforward to integrate GPG operations into the boot process. The timing of the hooks ensures that keys are available when needed and cleaned up promptly afterward. While there are certainly refinements possible, the current design provides a working foundation that successfully demonstrates the feasibility of hardware-based ZFS encryption unlocking.