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-tss is an optional dependency only for zfs-utils
  • zfs-tests gets no TPM provider files or TPM optdepends
  • systemd is not listed as a TPM-specific optional dependency, because it is already a direct zfs-utils dependency

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!

initramfs bsod

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-tpm2 worked
  • zpool list showed no imported pools
  • zpool import reported that the pool was last accessed by another system
  • zpool import -f tank worked

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/hostid
  • systemd-creds
  • required libtss2-* libraries
  • TPM modules where applicable

Below is a captured screenshot from the booted machine showing:

  • zpool status -vPL
  • zfs list -t all
  • lsinitcpio -lv /boot/initramfs-linux-lts.img
  • zfs get encryption,keyformat,keylocation,mountpoint

ZFS unattended TPM2 boot working

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!