DEV Community

Cover image for Stop Typing LUKS Passphrases at Every Boot: Practical systemd-cryptenroll on Linux
Lyra
Lyra

Posted on

Stop Typing LUKS Passphrases at Every Boot: Practical systemd-cryptenroll on Linux

Full-disk encryption that still stops for a passphrase at every reboot is only half done. On a laptop that is fine. On a homelab node, a fleet box, or anything you expect to come back after power loss, interactive unlock becomes the outage.

systemd-cryptenroll is the operator tool for binding LUKS2 volumes to hardware factors systemd already understands at boot: TPM2 chips, FIDO2 tokens (hmac-secret), PKCS#11 smartcards, plus computer-generated recovery keys. Unlock metadata lives in the LUKS2 JSON token area. At boot, systemd-cryptsetup + /etc/crypttab consume it.

This guide is operational. Commands and semantics come from systemd-cryptenroll(1), crypttab(5), and systemd-cryptsetup@.service(8) (Arch man pages / upstream systemd docs, covering features through systemd 262). Examples assume a LUKS2 volume you already unlock with a passphrase today. Do not wipe that passphrase until recovery and hardware unlock are proven.

What this is (and is not)

Tool Job
systemd-cryptenroll Enroll TPM2 / FIDO2 / PKCS#11 / passphrase / recovery into LUKS2
crypttab + systemd-cryptsetup Unlock at boot from those enrollments
Clevis + Tang Network-Bound Disk Encryption (NBDE) — unlock when a Tang server is reachable
systemd-creds Per-service secrets at activation, not disk unlock
sbctl / Secure Boot Boot chain PE signing — complementary, not a LUKS unlocker
cryptsetup luksAddKey Passphrase/keyfile slots only — no TPM2/FIDO2 token JSON

cryptenroll supports LUKS2 only. Older LUKS1 volumes need a format upgrade first (cryptsetup convert --type luks2, after backups).

Supported enrollment kinds (from the man page):

  1. PKCS#11 tokens (RSA or EC key pair)
  2. FIDO2 tokens with the hmac-secret extension
  3. TPM2 security devices
  4. Regular passphrases
  5. Recovery keys (machine-generated, high entropy, QR-friendly)

Prerequisites

# Tooling
command -v systemd-cryptenroll
systemd-cryptenroll --version   # features below note added-in versions

# You need an existing LUKS2 volume and a working unlock factor today
# (usually a passphrase). Root (or equivalent) required.

# Discover candidate LUKS devices (257+)
systemd-cryptenroll --list-devices

# TPM present?
ls /dev/tpmrm0 2>/dev/null || ls /dev/tpm0 2>/dev/null
systemd-cryptenroll --tpm2-device=list

# FIDO2 present?
systemd-cryptenroll --fido2-device=list
Enter fullscreen mode Exit fullscreen mode

Keep a second session or live USB path open before you wipe passphrase slots. A bad wipe without recovery is a brick for the volume key.

Mental model

Enrollment does not store your LUKS master key in the clear on the token.

  • TPM2: a random unlock key is sealed to the TPM (primary key derived from the TPM seed / SRK). Sealed blob + policy metadata land in the LUKS2 JSON token header. Unseal succeeds only when the TPM can reproduce the policy (PCRs, optional PIN, signed PCR policy, pcrlock, …).
  • FIDO2: a random salt/secret is HMAC'd on the token with a secret that never leaves the authenticator; the HMAC output unlocks the volume. Presence (tap), client PIN, and user verification are configurable.
  • Recovery key: like a passphrase, but generated for entropy and typeability; intended as the “token lost / TPM state drifted” escape hatch.

Default device when you omit DEVICE and are not wiping: the block device backing /var/ (which is usually the root FS device when /var is not separate). Prefer naming the device explicitly.

Lab 0 — Safety first: recovery key

Before any TPM or FIDO experiment, enroll a recovery key and store it offline (password manager, printed QR, sealed envelope — not next to the machine).

# Replace with your LUKS partition (by path or /dev/disk/by-uuid/…)
DEV=/dev/disk/by-uuid/YOUR-LUKS-UUID

# Prompts for an existing passphrase to authorize the new slot
sudo systemd-cryptenroll --recovery-key "$DEV"
Enter fullscreen mode Exit fullscreen mode

systemd-cryptenroll prints the recovery key and can show a QR code. Treat it like a root password with higher entropy.

List what is enrolled (via cryptsetup token listing):

sudo cryptsetup luksDump "$DEV" | sed -n '/Tokens/,/Keyslots/p'
# or full dump:
sudo cryptsetup luksDump "$DEV"
Enter fullscreen mode Exit fullscreen mode

Lab 1 — TPM2 automatic unlock (no PCR bind)

Simplest useful path for a trusted physical host: seal to the TPM with no PCR policy. The volume unlocks whenever this TPM is present. Stolen disk alone is not enough; stolen disk plus the same machine’s TPM (or a cloned TPM state, which you should assume is hard on discrete/fTPM designs) is the threat you are accepting.

# List TPM devices
systemd-cryptenroll --tpm2-device=list

# Enroll; unlocks with your existing passphrase unless you pass --unlock-*
sudo systemd-cryptenroll --tpm2-device=auto "$DEV"

# Empty string / omitted --tpm2-pcrs= means: bind to no PCRs (man page default)
Enter fullscreen mode Exit fullscreen mode

Wire crypttab so boot uses the TPM:

# /etc/crypttab — third field "none" when token metadata is in the LUKS header
# volume-name  encrypted-device                         key-file  options
rootfs UUID=YOUR-LUKS-UUID none tpm2-device=auto,x-initrd.attach
Enter fullscreen mode Exit fullscreen mode

Notes from crypttab(5):

  • tpm2-device=auto discovers the chip; or pin /dev/tpmrm0
  • x-initrd.attach is recommended for root (and other initrd-attached crypt devices) so detach ordering during shutdown is correct
  • headless=true refuses interactive password fallback — only enable after TPM path is proven
  • token-timeout= (default 30s) waits for tokens before falling back to password

Rebuild initramfs so early unlock sees the TPM stack (distro-specific):

# Debian/Ubuntu
sudo update-initramfs -u

# Fedora
sudo dracut -f

# Arch
sudo mkinitcpio -P
Enter fullscreen mode Exit fullscreen mode

Test without rebooting when the volume is not your live root (extra data disk):

sudo systemctl start systemd-cryptsetup@datadisk.service
# or:
sudo /usr/lib/systemd/systemd-cryptsetup attach datadisk "$DEV" - tpm2-device=auto
Enter fullscreen mode Exit fullscreen mode

For root, plan a controlled reboot and keep recovery key + passphrase slots until you have one clean boot.

Lab 2 — Bind to Secure Boot / UKI-friendly PCRs

PCR binding refuses unseal if measured boot state drifts. The man page’s practical guidance:

In general, encrypted volumes would be bound to some combination of PCRs 7, 11, and 14 (if shim/MOK is used). Prefer certificate-backed measurements over raw firmware code PCRs (0/2), which change on every firmware update.

PCR Name (systemd) Why it matters
7 secure-boot-policy Secure Boot on/off and db/KEK/PK material
11 kernel-boot systemd-stub UKI measurements; pcrphase milestones
14 shim-policy MOK certificates/hashes when shim is in the path
4 boot-loader-code Boot loader PE — changes on bootloader updates
9 kernel-initrd Kernel-measured initrds (Linux 5.17+ LOAD_FILE2 path)
# Example: Secure Boot policy + UKI/kernel-boot measurements
sudo systemd-cryptenroll \
  --tpm2-device=auto \
  --tpm2-pcrs=7+11 \
  "$DEV"

# Named form also works:
# --tpm2-pcrs=secure-boot-policy+kernel-boot
Enter fullscreen mode Exit fullscreen mode

Operational cost: kernel/UKI/Secure Boot changes that alter those PCRs break unlock until you re-enroll or fall back to recovery/passphrase. That is the point — and why a recovery key is mandatory.

Signed PCR policies (update-friendly)

Instead of sealing to today’s PCR digests, bind to a public key that signs allowed PCR states (--tpm2-public-key=, --tpm2-public-key-pcrs=, optional --tpm2-signature=). Vendors or your own UKI pipeline (see systemd-measure) can ship new signatures when kernels change without re-sealing every disk. Default public-key PCR set is 11 when unset. systemd 252+.

# Sketch — paths follow man-page search order if omitted:
# /etc/systemd/tpm2-pcr-public-key.pem and tpm2-pcr-signature.json
sudo systemd-cryptenroll \
  --tpm2-device=auto \
  --tpm2-public-key=/etc/systemd/tpm2-pcr-public-key.pem \
  --tpm2-public-key-pcrs=11 \
  --tpm2-signature=/etc/systemd/tpm2-pcr-signature.json \
  "$DEV"
Enter fullscreen mode Exit fullscreen mode

If a signature file is provided, cryptenroll verifies it unlocks the current PCR state before writing the slot — a safety net against locking yourself out mid-enrollment.

systemd 262 adds --tpm2-public-key-policyref= so one signing key can scope slots to a boot phase (for example initrd-only) when systemd-measure used a matching --policyref=.

Lab 3 — TPM2 + PIN (second factor)

A bare TPM unseal is “something you have” (the machine). Add a PIN so a cold-booted chassis still needs knowledge:

# systemd 262+: --tpm2-with-pin=yes hardens the PIN with Argon2id before the TPM
# (compromised TPM alone should not yield the volume key without the PIN)
sudo systemd-cryptenroll \
  --tpm2-device=auto \
  --tpm2-pcrs=7+11 \
  --tpm2-with-pin=yes \
  "$DEV"

# Older compatibility path: --tpm2-with-pin=direct (PBKDF2, systemd 253+ unlockers)
Enter fullscreen mode Exit fullscreen mode

crypttab:

rootfs UUID=… none tpm2-device=auto,tpm2-pin=yes,x-initrd.attach
Enter fullscreen mode Exit fullscreen mode

Dictionary lockout: wrong PINs hit the TPM’s global DA lockout. systemd does not own that policy — use tpm2_getcap / tpm2_dictionarylockout from tpm2-tools if you need to inspect or tune it. Keep recovery keys offline.

Optional Argon2id knobs (262+): --tpm2-argon2id-memory=, --tpm2-argon2id-iterations=, --tpm2-argon2id-parallelism=, --tpm2-argon2id-iter-time= (default target ~2s).

Lab 4 — FIDO2 token (YubiKey and friends)

Prefer FIDO2 over PKCS#11 on tokens that speak both — simpler enrollment, modern defaults.

systemd-cryptenroll --fido2-device=list

# Defaults: clientPin yes, user presence (tap) yes, user verification no
sudo systemd-cryptenroll --fido2-device=auto "$DEV"

# Explicit policy example:
sudo systemd-cryptenroll \
  --fido2-device=auto \
  --fido2-with-client-pin=yes \
  --fido2-with-user-presence=yes \
  --fido2-with-user-verification=no \
  "$DEV"
Enter fullscreen mode Exit fullscreen mode

crypttab:

rootfs UUID=… none fido2-device=auto,x-initrd.attach
Enter fullscreen mode Exit fullscreen mode

Credential algorithm default is es256; rs256 and eddsa are available via --fido2-credential-algorithm= (251+). systemd 257 adds --fido2-salt-file= and --fido2-parameters-in-header= (header storage is the default and what auto unlock expects).

Multi-token caveat: several FIDO2 enrollments work, but tokens with user verification may force sequential PIN/UV prompts because pre-flight identity is limited. PKCS#11 does not have that limitation.

Lab 5 — Rotate and wipe without bricking

--wipe-slot= accepts numeric indexes or type names: password, recovery, tpm2, fido2, pkcs11, empty, all (with safety refusal of wiping everything with no remaining unlock path in some combinations — still: never wipe recovery until the new factor boots cleanly).

Re-enroll TPM and drop old TPM slots in one shot:

sudo systemd-cryptenroll \
  --wipe-slot=tpm2 \
  --tpm2-device=auto \
  --tpm2-pcrs=7+11 \
  --unlock-tpm2-device=auto \
  "$DEV"
Enter fullscreen mode Exit fullscreen mode

Enrollment runs first; the new slot is excluded from the wipe. Other patterns from the man page:

# Empty password → TPM (image factory pattern)
sudo systemd-cryptenroll --wipe-slot=empty --tpm2-device=auto "$DEV"

# PKCS#11 → FIDO2
sudo systemd-cryptenroll --wipe-slot=pkcs11 --fido2-device=auto "$DEV"
Enter fullscreen mode Exit fullscreen mode

Unlock helpers when stdin passphrase is inconvenient (versions in man page):

Flag Use
--unlock-key-file=PATH Key file instead of typed passphrase (252+)
--unlock-fido2-device=auto Unlock with existing FIDO2 to enroll something else (253+)
--unlock-tpm2-device=auto Unlock with existing TPM2 (256+)
--unlock-empty Empty passphrase volumes (262+)
--unlock-headless Try TPM2 then empty — provisioning automation (262+)

Lab 6 — Loop-device dry run (no production disk)

When you want to practice without touching root:

set -euo pipefail
IMG=/tmp/cryptenroll-lab.img
MNT=/tmp/cryptenroll-mnt
sudo truncate -s 256M "$IMG"
LOOP=$(sudo losetup --find --show "$IMG")

sudo cryptsetup luksFormat --type luks2 "$LOOP"
sudo cryptsetup open "$LOOP" cryptenroll-lab
sudo mkfs.ext4 /dev/mapper/cryptenroll-lab
sudo mkdir -p "$MNT"
sudo mount /dev/mapper/cryptenroll-lab "$MNT"
echo ok | sudo tee "$MNT/proof" >/dev/null

# Enroll recovery + TPM (if present) while passphrase still works
sudo systemd-cryptenroll --recovery-key "$LOOP"
if systemd-cryptenroll --tpm2-device=list 2>/dev/null | grep -q .; then
  sudo systemd-cryptenroll --tpm2-device=auto "$LOOP"
fi

sudo cryptsetup luksDump "$LOOP"

# Cleanup
sudo umount "$MNT"
sudo cryptsetup close cryptenroll-lab
sudo losetup -d "$LOOP"
rm -f "$IMG"
Enter fullscreen mode Exit fullscreen mode

On hosts without a TPM, skip the TPM block and practice FIDO2 with a real key, or recovery + password only.

crypttab options worth knowing

From crypttab(5), focused on token unlock:

# TPM2
name UUID=… none tpm2-device=auto,tpm2-pin=yes,x-initrd.attach

# FIDO2
name UUID=… none fido2-device=auto,x-initrd.attach

# PKCS#11
name UUID=… none pkcs11-uri=auto,x-initrd.attach

# Headless server: no password prompt (only after hardware path is proven)
name UUID=… none tpm2-device=auto,headless=true,x-initrd.attach
Enter fullscreen mode Exit fullscreen mode

Related knobs:

  • tpm2-pcrs= — manual PCR list when enrollment metadata is not in the header (unusual if you used cryptenroll defaults)
  • tpm2-signature= / tpm2-pcrlock= — signed policy and pcrlock policy paths (auto-search under /etc/systemd, /run/systemd, …)
  • tpm2-measure-pcr=yes — measure the activated volume key into PCR 15 (253+), useful so later TPM objects can bind to “this install’s root volume key”
  • token-timeout=30s — how long to wait for hardware before password fallback
  • _netdev — for network-backed keying (NBDE/Clevis territory); not required for local TPM/FIDO

Initrd checklist (root volume)

  1. Enroll recovery key; store offline
  2. Enroll TPM2 and/or FIDO2; leave passphrase slot intact
  3. Update /etc/crypttab with tpm2-device=auto and x-initrd.attach
  4. Ensure initramfs includes systemd cryptsetup TPM/FIDO hooks (distro packages vary: libtss2, libfido2, cryptsetup token plugins)
  5. update-initramfs / dracut / mkinitcpio
  6. Reboot once; confirm unlock path in journal: journalctl -b -u systemd-cryptsetup@*
  7. Only then consider --wipe-slot=password if policy requires it — many operators keep a long passphrase slot forever

Failure modes and recovery

Symptom Likely cause Move
Drops to passphrase after TPM enroll crypttab missing tpm2-device=, initrd missing TSS stack, or wrong device Fix crypttab; rebuild initrd; check tpm2-device=list in initrd rescue
Works until kernel/Secure Boot change PCR bind (7/11/…) changed Unlock with recovery; re-enroll TPM with same or signed policy
TPM PIN lockout DA counter Wait out lockout or reset per site TPM policy; use recovery key
FIDO prompts repeatedly Multiple UV tokens Unplug extras; prefer single UV token or PKCS#11
Cannot enroll from PKCS#11-only volume Limitation: PKCS#11 cannot authorize new enrollments Always keep passphrase, recovery, FIDO2, or TPM2 for roll-over

Compatibility guarantee from the man page: old enrollments unlock on newer cryptsetup; the reverse is not guaranteed. Match systemd-cryptenroll and systemd-cryptsetup versions when possible; re-enroll after major upgrades if you want new hardening (Argon2id PIN, policyref, …).

Boundary: when to use Clevis/Tang instead

Use Clevis + Tang when unlock should depend on network presence (machine in the right building/VLAN), not on the local TPM. Use cryptenroll TPM2 when the trust anchor is the device (and optional PCRs/PIN). They can coexist as separate LUKS slots; they solve different threat models. Neither replaces off-host backups.

Quick reference

# Discover
systemd-cryptenroll --list-devices          # 257+
systemd-cryptenroll --tpm2-device=list
systemd-cryptenroll --fido2-device=list

# Enroll
systemd-cryptenroll --recovery-key "$DEV"
systemd-cryptenroll --tpm2-device=auto --tpm2-pcrs=7+11 "$DEV"
systemd-cryptenroll --tpm2-device=auto --tpm2-with-pin=yes "$DEV"   # 262+ Argon2id
systemd-cryptenroll --fido2-device=auto "$DEV"
systemd-cryptenroll --password "$DEV"

# Rotate TPM slots
systemd-cryptenroll --wipe-slot=tpm2 --tpm2-device=auto \
  --unlock-tpm2-device=auto "$DEV"

# crypttab core
# name UUID=… none tpm2-device=auto,x-initrd.attach
Enter fullscreen mode Exit fullscreen mode

Sources


Passphrase-at-boot encryption is better than nothing. LUKS2 plus systemd-cryptenroll is how you keep the confidentiality without making every power cycle a human dependency — as long as recovery keys are real, PCR choices match your update story, and you wipe old slots only after a boot you have watched succeed.

Top comments (0)