I run Ubuntu 24.04 with ZFS on my ThinkPad X1 Carbon, and I have an encrypted ZFS dataset for my workspace. By default, ZFS prompts for a passphrase at every login — annoying, but secure. This post explains how I wired it up to the TPM2 chip so the dataset unlocks automatically at boot, without storing the passphrase anywhere on disk.
The idea
Modern laptops have a TPM (Trusted Platform Module) chip that can seal a secret against the current state of the boot chain. The secret can only be unsealed if the firmware, bootloader, and Secure Boot state are identical to when it was sealed. If someone tampers with the boot chain — or steals just the drive — the TPM refuses to hand over the secret.
PCRs (Platform Configuration Registers) are the values the TPM measures during boot. I bind to:
- PCR 0 — UEFI firmware
- PCR 2 — Option ROMs
- PCR 4 — Bootloader (GRUB)
- PCR 7 — Secure Boot policy and signing certificates
With UEFI Secure Boot enabled, PCR 7 is particularly strong — booting a live USB or disabling Secure Boot changes PCR 7 and makes unseal impossible.
Prerequisites
- Ubuntu 24.04 with ZFS
- An encrypted ZFS dataset (mine is
rpool/USERDATA/workspace) - TPM2 chip (
/dev/tpm0) - UEFI Secure Boot enabled
tpm2-toolsinstalled:sudo apt install tpm2-tools- Your user in the
tssgroup:sudo usermod -aG tss $USER
Step 1 — Find a free TPM persistent handle
tpm2_getcap handles-persistent
Pick a handle in the 0x81000000–0x8100FFFF range that isn't listed. I used 0x81000002.
Step 2 — Seal the passphrase
I wrote a script that reads the passphrase interactively (no echo, confirmation prompt), writes it only to /tmp (tmpfs = RAM, never hits disk), seals it into the TPM, then shreds the temp file immediately.
#!/usr/bin/env bash
set -euo pipefail
HANDLE="${1:-0x81000002}"
WORKDIR="$(mktemp -d)"
trap 'rm -rf "$WORKDIR"' EXIT
IFS= read -rs -p "Enter ZFS passphrase: " PASSPHRASE; echo ""
IFS= read -rs -p "Confirm passphrase: " PASSPHRASE2; echo ""
[[ "$PASSPHRASE" == "$PASSPHRASE2" ]] || { echo "Mismatch" >&2; exit 1; }
tpm2_createpolicy --policy-pcr -l sha256:0,2,4,7 -L "$WORKDIR/policy.digest"
tpm2_createprimary -C e -g sha256 -G rsa -c "$WORKDIR/primary.ctx" >/dev/null
printf '%s' "$PASSPHRASE" > "$WORKDIR/secret"
tpm2_create -g sha256 -u "$WORKDIR/obj.pub" -r "$WORKDIR/obj.priv" \
-C "$WORKDIR/primary.ctx" -L "$WORKDIR/policy.digest" \
-i "$WORKDIR/secret" >/dev/null
shred -u "$WORKDIR/secret"
unset PASSPHRASE PASSPHRASE2
tpm2_load -C "$WORKDIR/primary.ctx" -u "$WORKDIR/obj.pub" \
-r "$WORKDIR/obj.priv" -c "$WORKDIR/seal.ctx"
sudo tpm2_evictcontrol -C o -c "$WORKDIR/seal.ctx" "$HANDLE"
echo "Sealed at $HANDLE"
Save it, chmod 700, and run it. You'll be prompted for your ZFS passphrase and then your sudo password for the final persist step.
Verify it worked:
tpm2_getcap handles-persistent # should now include 0x81000002
tpm2_unseal -c 0x81000002 -p pcr:sha256:0,2,4,7 # should print your passphrase
Step 3 — Auto-unlock at boot via systemd
Ubuntu's ZFS tooling auto-generates a systemd unit that prompts for the passphrase. We override it with a drop-in that tries the TPM first and falls back to a password prompt if unseal fails (e.g. after a firmware update).
sudo mkdir -p /etc/systemd/system/zfs-load-key-rpool-USERDATA-workspace.service.d
sudo tee /etc/systemd/system/zfs-load-key-rpool-USERDATA-workspace.service.d/tpm-unseal.conf <<'EOF'
[Service]
ExecStart=
ExecStart=/bin/sh -c '\
set -eu; \
keystatus="$(/sbin/zfs get -H -o value keystatus "rpool/USERDATA/workspace")"; \
[ "$keystatus" = "unavailable" ] || exit 0; \
if tpm2_unseal -c 0x81000002 -p pcr:sha256:0,2,4,7 2>/dev/null \
| /sbin/zfs load-key "rpool/USERDATA/workspace"; then \
exit 0; \
fi; \
echo "TPM unseal failed, falling back to passphrase prompt" >&2; \
count=0; \
while [ $count -lt 3 ]; do \
systemd-ask-password --id="zfs:rpool/USERDATA/workspace" \
"Enter passphrase for rpool/USERDATA/workspace:" \
| /sbin/zfs load-key "rpool/USERDATA/workspace" && exit 0; \
count=$((count + 1)); \
done; \
exit 1'
StandardError=journal
EOF
sudo systemctl daemon-reload
Step 4 — Test without rebooting
sudo zfs unload-key rpool/USERDATA/workspace
sudo systemctl restart zfs-load-key-rpool-USERDATA-workspace.service
zfs get keystatus rpool/USERDATA/workspace
If keystatus shows available with no password prompt — it works.
What happens after a kernel or firmware update?
PCR 4 changes when GRUB is updated. The TPM unseal will fail, and the fallback password prompt kicks in. After entering your passphrase manually, just re-run the seal script to update the sealed blob against the new PCR values.
Security summary
| Threat | Protected? |
|---|---|
| Drive stolen, put in another machine | ✅ Yes — wrong TPM |
| Boot from live USB | ✅ Yes — PCR 7 changes (Secure Boot) |
| Tampered bootloader | ✅ Yes — PCR 4 changes |
| Disabled Secure Boot in UEFI | ✅ Yes — PCR 7 changes |
| Normal boot, attacker has root access | ❌ No — they can run tpm2_unseal too |
For a laptop used on the go, this covers the realistic threat: theft of the device while powered off. The data is inaccessible without the original, unmodified hardware and boot chain.