Unattended TPM2 unlocking for native ZFS root encryption on ArchZFS
ZFS on Linux is one of those pieces of infrastructure that changes how you think about storage. Checksums, snapshots, send/receive, native encryption, compression, scrubs, mirrors, and clear operational tooling all fit together into something that feels robust and coherent.
On ArchLinux, the ArchZFS project does the hard work of keeping that experience available to users who want both ZFS and a rolling distribution. The kernel compatibility, packages, initramfs integration, and release handling are all done by ArchZFS, which has made this type of deployment practical for years.
Most commercial NAS solutions nowadays come with a third drive for the OS. However, most Tiny-Mini-Micro machines have no such option out of the box, and generally involve some level of DIY, either on the hardware or on the software level.
We will soon see that this is actually a blessing in disguise, which led to a very creative solution.
My Dell OptiPlex Micro only has two M.2 drive slots. This is enough for a mirrored ZFS pool, but it leaves no third drive for a separate OS disk. If I want redundancy, the operating system has to live on the same mirrored pool as the rest of the machine.
That is actually a pretty nice setup. The root filesystem benefits from the same ZFS
redundancy as everything else, and snapshots/replication naturally cover the
OS too.
The hard part is early boot stage: if the root dataset is native-encrypted
with ZFS, somebody or something has to unlock it before root (/) can be mounted.
So I added an opt-in path to ArchZFS for unlocking a native-encrypted ZFS root filesystem from the TPM during early boot.
The short version: if your ArchZFS system uses ZFS native encryption for root (/),
you can now use a TPM2-backed systemd-creds credential to unlock the root
dataset from the initramfs.
This is especially useful for machines that need to
reboot unattended, such as remote boxes, homelab servers, or anything where
typing a passphrase at the console is inconvenient.
This is currently only supported by ArchZFS, not by upstream OpenZFS!
The important bit is that I kept this opt-in. If you do not configure a provider, the normal ZFS initramfs behavior remains unchanged.
What changed
I changed the ArchZFS mkinitcpio hook to support the alternate key-loading provider layout used by upstream OpenZFS:
/etc/zfs/initramfs-tools-load-key
/etc/zfs/initramfs-tools-load-key.d/*
During boot, before falling back to the normal interactive zfs load-key
prompt, the hook checks for those provider scripts in the initramfs. If one is
present, it gets a chance to load the key for the encryption root needed by the
boot dataset.
I implemented TPM2 support as one such provider. The packaged example is installed at:
/usr/share/doc/zfs-utils/initramfs-tools-load-key.d/tpm2-systemd-creds
To enable it, copy it into /etc/zfs:
install -Dm644 \
/usr/share/doc/zfs-utils/initramfs-tools-load-key.d/tpm2-systemd-creds \
/etc/zfs/initramfs-tools-load-key.d/tpm2-systemd-creds
Then create a TPM2-backed credential and include it in your initramfs with
mkinitcpio’s existing FILES=() mechanism:
FILES+=(/etc/hostid /etc/zfs/zfs-key.creds)
The provider uses systemd-creds decrypt to decrypt the credential and pipes
the result into:
zfs load-key -L prompt "${ENCRYPTIONROOT}"
That -L prompt is significant: it tells ZFS to read the key from stdin even if the
dataset has a different configured keylocation.
Why a provider instead of hard-coding TPM support?
My original proof of concept was much more direct: add TPM pieces to the ZFS
hook and make the initramfs carry the credential, systemd-creds, and TPM libraries.
That worked locally, which was the whole point of the proof of concept, but
review quite fairly pushed back on the shape of it.
Not every ArchZFS user wants TPM unlocking.
Not every ZFS root setup has a TPM. And a normal zfs
hook should not suddenly fail because /etc/zfs/zfs-key.creds is missing on a
system that never opted in.
The PR review process was useful here. The first round identified several real problems:
- TPM dependencies being pulled in for all users
- hard-coded TPM library sonames
- unconditional credential inclusion
So I reworked it properly. The next iteration moved the design toward the existing OpenZFS provider contract. That gave us a cleaner split:
- the ZFS hook provides a generic provider seam
- the TPM2 implementation lives as an opt-in provider
- non-TPM users pay no dependency or initramfs payload cost
- other provider styles remain possible later without changing this TPM path
The second review then tightened the boot-path details. I made providers run
in a subshell, so a broken provider cannot accidentally terminate the BusyBox
/init process. I also changed the hook to pass the resolved boot dataset to
providers, so the TPM2 provider does not need to re-parse /proc/cmdline and
risk disagreeing with the actual dataset being mounted.
Keeping the scope narrow
This is not a general ZFS key-management framework.
The TPM2 provider only tries to unlock the encryption root required for the boot root dataset. If other datasets inherit that encryption root, they may become available too. Datasets with separate encryption roots and separate keys continue through the normal ZFS key-loading path.
This keeps the feature focused on the practical problem it set out to solve: booting / unattended.
Packaging details
This landed after the zfs-utils package split work in PR #660, so I had to make the
packaging fit the new zfs-utils / zfs-tests layout.
The final package shape is:
- the TPM2 provider source is included in
zfs-utils - the provider example is installed only by
zfs-utils tpm2-tssis an optional dependency only forzfs-utilszfs-testsgets no TPM provider files or TPM optdependssystemdis not listed as a TPM-specific optional dependency, because it is already a directzfs-utilsdependency
I bumped the stable package release to 2.4.4-3,
which will soon be shipped to the ArchLinux AUR as well.
Building it
For local package generation and build checks, I used an Arch Linux Podman container. That made it possible to easily run the repo’s Arch packaging in a clean Arch environment, without turning the host into the build environment.
The generation flow used a command in this shape:
podman run -it \
--userns=keep-id:uid=0,gid=0 \
-v "$PWD:/src" \
-w /src \
archlinux:latest \
bash -lc 'pacman -Sy --noconfirm --needed \
bash coreutils curl findutils gawk git grep sed
bash ./build.sh -s -d utils update'
Inside the container the command runs as root, which matches what the build
scripts expect. On the host, thanks to --userns=keep-id:uid=0,gid=0, files
created on the bind mount are still owned by my normal user rather than host root.
After regenerating the recipes, I built the stable package with:
makepkg -fcC --nosign
That produced:
zfs-utils-2.4.4-3-x86_64.pkg.tar.zst
zfs-tests-2.4.4-3-x86_64.pkg.tar.zst
I inspected the archives before testing. The zfs-utils package contained the
initcpio hook, install hook, and TPM2 provider example.
The hostid issue
Target-machine testing found a significant boot issue that was not actually TPM related. This was the fun kind of early-boot debugging where the symptom looks dramatic and the root cause is a tiny missing file.
The machine panicked with:
Attempted to kill init!

This now produces a fabulous blue screen - yes even on Linux!
That usually means the initramfs /init process exited. Since the TPM provider
is run in a child pipeline and provider failures fall back to passphrase entry,
the TPM decrypt path itself was unlikely to be the direct cause.
I dropped into the initramfs with break=premount, and that made the situation
clearer right away:
- decrypting the TPM2 credential manually worked
systemd-analyze has-tpm2workedzpool listshowed no imported poolszpool importreported that the pool was last accessed by another systemzpool import -f tankworked
The missing bit was /etc/hostid.
ZFS records which host last imported a pool. If the initramfs does not carry
the same hostid as the real system, ZFS can think the pool belongs to another
machine and refuse to import it without -f. The hook then fails to import the
root pool, exits, and PID 1 going away produces the kernel panic.
The fix was simple but significant:
FILES+=(/etc/hostid /etc/zfs/zfs-key.creds)
The credential is for the TPM2 provider. The hostid is for clean ZFS pool import in the initramfs.
You can also force import with zfs_force=1, but carrying /etc/hostid is the
cleaner solution for the normal root-on-ZFS case.
Keep in mind that ZFS needs the udev hook enabled in order to build the initramfs image correctly, not systemd:
HOOKS=(base udev ... zfs)
Testing on the target machine
I tested the final candidate on the actual TPM/ZFS machine.
The target boot-loader setup used:
root=ZFS=tank/root rw
I inspected the generated initramfs to confirm the expected pieces were present:
- the TPM2 provider script
/etc/zfs/zfs-key.creds/etc/hostidsystemd-creds- required
libtss2-*libraries - TPM modules where applicable
Below is a captured screenshot from the booted machine showing:
zpool status -vPLzfs list -t alllsinitcpio -lv /boot/initramfs-linux-lts.imgzfs get encryption,keyformat,keylocation,mountpoint

And I recorded three short validation videos which can be found in the original PR linked above.
What end-users get
For users, the benefit is straightforward: native ZFS encryption now fits much more comfortably on machines that need unattended reboot.
You still keep ZFS native encryption. You still keep the normal passphrase fallback. You do not need to convert the system to a LUKS-under-ZFS layout just to get TPM-assisted boot. And users who do not opt in do not get TPM libraries, credentials, or provider scripts added to their initramfs.
Before this, unattended encrypted ZFS root setups often meant accepting a clunkier layout: putting LUKS2 underneath ZFS, keeping key material on a separate disk, or wiring together local site-specific boot scripts. Those can all be valid designs, but they are more moving parts than many native ZFS encryption users want for a simple unattended reboot path.
I like this result because it is small, straightforward, and useful - a generic integration point with a practical TPM2 provider:
- better unattended reboot support for encrypted ZFS root systems
- no behavior change for ordinary ZFS-root users
- no mandatory TPM dependency for everyone
- documented setup through
mkinitcpio -H zfs - safer fallback behavior when the credential or provider is missing
It is a modest change in code size, but it removes a lot of operational awkwardness for remote ArchZFS machines. Native ZFS encryption now gets a clean unattended boot option, and that is exactly what I wanted this work to deliver.
The ArchZFS maintainers were very kind to also put together a new wiki entry for this new piece of functionality. Thank you!