Replaced TPM2 auto-unlock on telep-mainframe with a YubiKey-gated LUKS unlock. The goal the user asked for: “when I unplug my YubiKey nothing can be read.” Final model — the key lives plugged into the box; while present the box auto-boots/unlocks unattended (survives mains cuts, no prompt); pull the key → the box syncs and powers off; boot without the key → it won’t unlock (falls back to the passphrase prompt). Done and verified in production 2026-08-15; the clevis TPM binding is removed, no TPM token remains on the header.
Live-lock is ARMED — pulling the YubiKey powers the box OFF
A udev remove rule fires a clean systemctl poweroff the instant the enrolled YubiKey (PRODUCT==1050/407/*) leaves USB. Any operation that re-enumerates the key counts as a removal — most importantly ykman config …. Before ANY such op, disarm the rule first:
sudo mv /etc/udev/rules.d/99-yk-lock.rules /root/99-yk-lock.rules.disabledsudo udevadm control --reload-rules
Host:telep-mainframe (Debian 13, initramfs-tools, Secure Boot OFF). Access:ssh levander@telep-mainframe, passwordless sudo.
Root volume: LUKS2 /dev/nvme1n1p3 (UUID a3a8e37d-79fa-484b-bc3f-40c56df95337) → mapper nvme1n1p3_crypt → VG telep-mainframe-vg / LV root. NVMe node names drift across reboots — always resolve by UUID, never by nvmeXn1 path (see the drive gotcha).
cam-archive volume: LUKS2 /dev/sdb1 (UUID 7af64460-…) → mapper cam-archive → /mnt/cam-archive.
Unlock mechanism:HMAC-SHA1 challenge-response, OTP slot 2, no-touch, on a YubiKey 5C NFC (fw 5.7.4, serial 32875420, USB id 1050:0407 after enabling OTP). Custom initramfs keyscript — NOT FIDO2, NOT systemd-cryptenroll, NOT clevis.
Root keyslots now:0 = passphrase (break-glass, user knows it), 2 = YubiKey. No slot 1, no TPM token (clevis unbound).
Check it:sudo cryptsetup luksDump /dev/nvme1n1p3 (slots 0 + 2, Tokens empty); dmesg | grep YK-KEYSCRIPT after boot (root key from YubiKey ok).
Header backups (post-YK): on the Mac at ~/telep/root-nvme1n1p3-20260815-postyk.img and ~/telep/camarchive-sdb1-20260815-postyk.img (600). The pre-YK backup was SHREDDED — it contained the clevis TPM token and would bypass the key if restored.
Why this replaced TPM auto-unlock
The old clevis + TPM2 setup auto-unlocked on this hardware with no possession factor — it protected against bare-drive theft (pull the NVMe → the TPM-sealed key isn’t on it) but not whole-box theft, and there was nothing the user could physically remove to make the disk unreadable. The user wanted a possession token: a key they can pull to render the box unreadable, while still keeping unattended auto-boot so the box self-recovers from the frequent mains events (now on a UPS, but reboots still must be hands-off — there is no dropbear/remote-unlock on this box).
The YubiKey, kept plugged in, satisfies both: it is present at boot so unlock is unattended, but it is physically removable, and a live udev rule turns removal into an immediate power-off.
Why NOT FIDO2 (and why HMAC challenge-response instead)
The modern approach — systemd-cryptenroll --fido2-device=auto — enrolls a FIDO2 keyslot fine, but Debian’s initramfs-tools initramfs does not consume systemd-cryptenroll FIDO2/TPM2 tokens (same root reason clevis was used for the TPM instead of systemd-cryptenroll — see the old note). There is no systemd in this initramfs to read the token. So the unlock is built on YubiKey HMAC-SHA1 challenge-response (ykchalresp) driven by a custom initramfs keyscript, which works with the classic cryptsetup hook.
How it works
Challenge-response, not a stored key. The LUKS key for each volume is derived at unlock time by sending a fixed challenge to the YubiKey’s HMAC-SHA1 slot 2; the key computes HMAC-SHA1(secret, challenge) and returns 40 hex chars. The secret never leaves the YubiKey. The challenge is not secret and is stored on disk / baked into the initramfs.
Per-volume challenges live in /etc/ykluks/ (mode 600): root.challenge, cam-archive.challenge. Distinct per volume so one key derives a different LUKS key for each.
The -x flag takes a hex challenge; the response is the LUKS passphrase for that keyslot. The trailing newline matters — enroll and unlock must both strip it (tr -d '\n') or the keys won’t match.
Boot-time unlock (root)
/usr/local/sbin/yk-keyscript.sh — the crypttab keyscript. Reads /etc/ykluks/root.challenge, modprobe usbhid, retries ykchalresp for up to ~10 s (the key takes a moment to enumerate in early boot), prints the 40-hex key to stdout (which cryptsetup consumes). Prints nothing if the key is absent → cryptsetup falls through to the passphrase prompt. Logs progress to /dev/kmsg (YK-KEYSCRIPT: …).
/etc/initramfs-tools/hooks/yk-unlock — the initramfs hook. copy_execs ykchalresp and yk-keyscript.sh into the initramfs, manual_add_modules usbhid, and copies root.challenge in so the challenge is available before the root fs is mounted.
/etc/crypttab root line got ,keyscript=/usr/local/sbin/yk-keyscript.sh appended (backup at /etc/crypttab.bak-preyk), then sudo update-initramfs -u.
Result on boot: dmesg | grep YK-KEYSCRIPT shows root key from YubiKey ok (0s) and root mounts with no prompt. Remove the key and the same path prints nothing → the passphrase prompt appears.
Clean poweroff, deliberately NOT a hard sysrq cut — a power-loss-style hard cut is exactly what was damaging this NVMe (2026-08-05-power-root-cause-nvme-damage-ups-kb-handover); sync + graceful poweroff avoids adding to the media-error count. The disk is still encrypted-at-rest once powered off; the point is availability-denial + shutdown, not a crash.
What was done (exact mechanics, for reproduction/recovery)
Enabled OTP on the YubiKey. It shipped as U2F+CCID only (USB id 1050:0406). ykman config usb --enable OTP --force → now OTP+U2F+CCID (id 1050:0407, PRODUCT 1050/407/574). Programmed a no-touch HMAC slot 2: ykman otp chalresp --generate --force 2 (no --touch = unattended boot works; the secret is randomly generated and stays in the key). Tooling installed: yubikey-manager (ykman), yubikey-personalization (ykchalresp), pcscd.
Stored per-volume challenges in /etc/ykluks/{root,cam-archive}.challenge (600).
Enrolled keyslots.
cam-archive: added a YubiKey keyslot using its existing keyfile (no passphrase interaction needed).
root: added a YubiKey keyslot (→ slot 2). This required the user to type the existing passphrase at a real terminal — an agent has no TTY and cannot supply it. The working command:
De-risked before removing TPM (see the reusable method below).
Removed the clevis/TPM binding:sudo clevis luks unbind -d /dev/nvme1n1p3 -s 1 -f (prints a harmless Nothing to read on input but succeeds). Verified via luksDump: slot 1 gone, Tokens empty. Rebooted → root auto-unlocked via the YubiKey, zero clevis tokens.
Armed the live-lock udev rule + handler (above), verified with a temporary log-only test rule first (below).
Fresh post-YK header backups to the Mac; shredded the pre-YK backup from both Mac and box.
GOTCHA — do NOT pipe the new key into luksAddKey DEV -
ykchalresp … | cryptsetup luksAddKey /dev/nvme1n1p3 -FAILS with No key available with this passphrase. The pipe occupies stdin, so cryptsetup reads the piped key as the EXISTING passphrase (which it isn’t) and never gets to prompt for the real one. Write the new key to a /dev/shm keyfile instead (… > /dev/shm/yk.key; luksAddKey DEV /dev/shm/yk.key; shred -u) so stdin stays free for the interactive passphrase prompt. This is why root enrollment needs a human at the terminal.
De-risking method (reusable — proved the key before removing the fallback)
Removing the TPM binding while relying on an untested YubiKey path could have stranded the box at a prompt with no console. Two zero-risk checks were run while clevis/TPM still did the real unlock:
Early-boot reachability probe. An init-premount script ran ykchalresp inside the real initramfs and logged the result to /dev/kmsg, while clevis still unlocked the disk. After boot: dmesg | grep YK-PROBE-RESULT → OK len=40 after 0s → proved the YubiKey is reachable that early in boot.
Output-unlocks-root check on the running system. Fed the keyscript’s output to cryptsetup luksOpen --test-passphrase --key-file - → confirmed the derived key actually opens the root LUKS slot.
Only after both passed was the clevis/TPM binding removed. This probe-first pattern is reusable for any future initramfs-unlock change on this box.
Verify the live-lock WITHOUT powering off
Test the udev trigger with a temporary log-only rule (same match, RUN+= a script that just loggers a marker + touches /run/yk-removal-test), reload rules, pull+reinsert the key, and check syslog for the marker (tag yk-lock). Confirms the match fires without an actual poweroff. Swap in the real poweroff handler only after the marker is seen.
Current state
Volume
Device (by UUID)
Mapper
Keyslots
Unlock at boot
root
nvme1n1p3 (a3a8e37d-…)
nvme1n1p3_crypt
0 passphrase · 2 YubiKey
keyscript (YK present) → passphrase (absent)
cam-archive
sdb1 (7af64460-…)
cam-archive
keyfile · YubiKey
(mounted post-boot; /mnt/cam-archive)
No TPM token, no slot 1 on root. clevis luks list -d /dev/nvme1n1p3 is empty.
Post-YK header backups on the Mac (600); the TPM-bearing pre-YK backup is shredded everywhere.
Caveats, gotchas, open items
ykman config re-enumerates the key = a udev remove = poweroff
Never run any ykman config … (or anything that resets the key’s USB descriptor) while the live-lock is armed — the re-enumeration looks like a removal and the box powers off mid-command. Disarm 99-yk-lock.rules first (see the banner at the top), do the config change, re-arm.
SINGLE POINT OF FAILURE — only one YubiKey is enrolled (OPEN ITEM)
If the one enrolled key is lost or breaks, boot requires the passphrase (slot 0, break-glass). Strongly recommended: enroll a backup YubiKey in its own keyslot (repeat the root enroll with the 2nd key’s challenge; disarm the live-lock during enrollment so inserting/removing keys doesn’t power the box off). This is deferred.
Whole-box theft is still NOT covered — by deliberate choice
Because the key stays plugged in and the box auto-boots, stealing the whole box with the key still boots it (same limitation as the old TPM setup). The new protection is drive-only theft (the key’s secret isn’t on the NVMe) plus the live-lock (pull the key → off). Adding a PIN/touch would close whole-box theft but breaks unattended auto-boot — the user explicitly chose possession-only auto-boot over a PIN.
ykman needs pcscd + root to read THIS key
With OTP+U2F+CCID enabled, plain-user ykman access is denied; sudo ykman info works (needs pcscd running). Budget sudo for any key inspection.
Deferred / open
Backup YubiKey (above) — the biggest gap.
Encrypt sda (empty 447 G) and the Windows NTFS disk (nvme0n1) — only after the user migrates data off and wipes them (user said the Windows boot is expendable).
Booted without the key / at a passphrase prompt → type the passphrase at the physical console (no dropbear/remote-unlock exists on this box). Then plug the key back in for future unattended boots.
Box powered off unexpectedly → likely the live-lock fired (key was pulled/jostled). Reseat the key, power on; it auto-unlocks.
Need to run ykman config / swap keys → disarm99-yk-lock.rules first (banner at top).
LUKS metadata corrupted → restore the header from the Mac: ~/telep/root-nvme1n1p3-20260815-postyk.img (root) / ~/telep/camarchive-sdb1-20260815-postyk.img (cam-archive) via cryptsetup luksHeaderRestore. Never restore the shredded pre-YK backup (it re-adds the TPM bypass).
Related
2026-07-24-luks-tpm-autounlock — the SUPERSEDED clevis/TPM2 auto-unlock this replaced; that mechanism is now REMOVED (no TPM token on the header)