I recently set up a Linux workstation so the encrypted root disk unlocks automatically at boot using the machine’s TPM2 chip. The goal was simple: keep full-disk encryption, stop typing the LUKS passphrase every reboot, and avoid turning the boot process into a tiny daily ritual.
This post walks through the general approach using LUKS, Clevis, TPM2, and Dracut. The examples are intentionally generic, but the flow is aimed at Fedora, RHEL, CentOS Stream, Oracle Linux, and similar systems that use Dracut for initramfs generation.
What This Setup Does
LUKS still protects the disk. Your existing passphrase still works. Clevis adds another LUKS unlock method by sealing a generated key to the TPM2 chip.
At boot, Dracut includes the Clevis TPM2 unlock tooling in the initramfs. When the system asks for the encrypted root disk password, Clevis asks the TPM to unseal the key. If the TPM policy matches, the disk unlocks automatically.
Tiny magic box says yes. Disk opens. Coffee remains uninterrupted.
Important Security Caveat
TPM auto-unlock is not a universal security force field. It mainly protects against someone removing the disk and reading it elsewhere. If someone steals the entire laptop or workstation and the TPM policy allows unlock, the machine may boot without a passphrase.
You can make the binding stricter by using PCRs, commonly PCR 7 for Secure Boot state:
sudo clevis luks bind -d /dev/nvme0n1p3 tpm2 '{"pcr_bank":"sha256","pcr_ids":"7"}'
That improves tamper resistance, but it also means firmware, Secure Boot, or boot policy changes may require rebinding. Security is a set of tradeoffs, not a vending machine.
Before You Touch Anything
Find the encrypted device:
lsblk -f
findmnt -no SOURCE,FSTYPE,OPTIONS /
cat /proc/cmdline
On many LVM-on-LUKS installs, you will see something like:
/dev/nvme0n1p3
luks-xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx
vg-root /
The device you bind is the actual LUKS block device, for example /dev/nvme0n1p3, not the mounted root logical volume.
Check that TPM2 exists:
ls -l /dev/tpm* /sys/class/tpm
tpm2_getcap properties-fixed
Install the usual packages:
sudo dnf install -y clevis clevis-luks clevis-dracut clevis-systemd tpm2-tools cryptsetup
Package names vary slightly by distro, but those are the common pieces.
Manual Version
Back up the LUKS header first:
sudo mkdir -p /root/luks-header-backups
sudo cryptsetup luksHeaderBackup /dev/nvme0n1p3 \
--header-backup-file /root/luks-header-backups/root-luks-header.img
sudo chmod 600 /root/luks-header-backups/root-luks-header.img
Bind the LUKS device to TPM2:
sudo clevis luks bind -d /dev/nvme0n1p3 tpm2 '{}'
Or bind to Secure Boot state with PCR 7:
sudo clevis luks bind -d /dev/nvme0n1p3 tpm2 '{"pcr_bank":"sha256","pcr_ids":"7"}'
List bindings:
sudo clevis luks list -d /dev/nvme0n1p3
Verify the TPM can unseal the Clevis passphrase. Some Clevis versions require the slot:
sudo clevis luks pass -d /dev/nvme0n1p3 -s 1 >/dev/null
Force Clevis TPM2 into future initramfs builds:
sudo tee /etc/dracut.conf.d/90-clevis-tpm2.conf >/dev/null <<'EOF'
force_add_dracutmodules+=" crypt clevis clevis-pin-tpm2 "
EOF
Rebuild the initramfs:
sudo dracut -fv --force-add "crypt clevis clevis-pin-tpm2"
Then reboot with your original LUKS passphrase nearby. The first reboot is not the time to discover you forgot where you put your recovery notes.
Generic Helper Script
Here is a generic script that does the boring parts: detects the root LUKS device, backs up the header, binds TPM2, writes a Dracut config snippet, rebuilds the current initramfs, and verifies that the key can be unsealed.
Save it as enroll-luks-tpm2-clevis.sh, review it, then run it with sudo.
#!/usr/bin/env bash
set -euo pipefail
usage() {
cat <<'USAGE'
Usage:
sudo ./enroll-luks-tpm2-clevis.sh [options]
Options:
--device DEV LUKS device to bind. Default: auto-detect root LUKS device.
--pcr-ids IDS Optional PCR ids, for example 7.
--pcr-bank BANK PCR bank for --pcr-ids. Default: sha256.
--yes Skip confirmation prompts where possible.
-h, --help Show help.
USAGE
}
log() { printf '\n==> %s\n' "$*"; }
die() { printf 'ERROR: %s\n' "$*" >&2; exit 1; }
run() { printf '+'; printf ' %q' "$@"; printf '\n'; "$@"; }
DEVICE=""
PCR_IDS=""
PCR_BANK="sha256"
YES=0
while [[ $# -gt 0 ]]; do
case "$1" in
--device) DEVICE="${2:?missing device}"; shift 2 ;;
--pcr-ids) PCR_IDS="${2:?missing PCR ids}"; shift 2 ;;
--pcr-bank) PCR_BANK="${2:?missing PCR bank}"; shift 2 ;;
--yes) YES=1; shift ;;
-h|--help) usage; exit 0 ;;
*) die "unknown option: $1" ;;
esac
done
[[ ${EUID} -eq 0 ]] || exec sudo -E bash "$0" "$@"
for cmd in cryptsetup clevis clevis-luks-bind clevis-luks-list clevis-luks-pass dracut tpm2_getcap findmnt lsblk; do
command -v "$cmd" >/dev/null 2>&1 || die "missing command: $cmd"
done
detect_luks_device() {
if [[ -n "$DEVICE" ]]; then
readlink -f "$DEVICE"
return
fi
local uuid path root_source parent_part
uuid="$(tr ' ' '\n' </proc/cmdline | sed -n 's/^rd\.luks\.uuid=luks-//p' | head -n 1)"
if [[ -n "$uuid" && -e "/dev/disk/by-uuid/$uuid" ]]; then
readlink -f "/dev/disk/by-uuid/$uuid"
return
fi
root_source="$(findmnt -no SOURCE /)"
parent_part="$(lsblk -rspno NAME,TYPE "$root_source" | awk '$2 == "part" { print $1; exit }')"
[[ -n "$parent_part" ]] || die "could not auto-detect root LUKS device; use --device"
readlink -f "$parent_part"
}
[[ -e /dev/tpmrm0 || -e /dev/tpm0 ]] || die "no TPM device found"
run tpm2_getcap properties-fixed >/dev/null
DEV="$(detect_luks_device)"
run cryptsetup isLuks "$DEV"
if [[ -n "$PCR_IDS" ]]; then
[[ "$PCR_IDS" =~ ^[0-9]+(,[0-9]+)*$ ]] || die "--pcr-ids must be comma-separated numbers"
TPM2_CONFIG="{\"pcr_bank\":\"$PCR_BANK\",\"pcr_ids\":\"$PCR_IDS\"}"
else
TPM2_CONFIG="{}"
fi
LUKS_UUID="$(cryptsetup luksUUID "$DEV")"
BACKUP_DIR="/root/luks-header-backups"
BACKUP_FILE="$BACKUP_DIR/${LUKS_UUID}-$(date +%Y%m%d-%H%M%S).img"
log "Planned enrollment"
printf 'Device: %s\n' "$DEV"
printf 'LUKS UUID: %s\n' "$LUKS_UUID"
printf 'TPM2 config: %s\n' "$TPM2_CONFIG"
printf 'Backup: %s\n' "$BACKUP_FILE"
if [[ "$YES" -ne 1 ]]; then
read -r -p "Type ENROLL to continue: " confirm
[[ "$confirm" == "ENROLL" ]] || die "cancelled"
fi
log "Backing up LUKS header"
run mkdir -p "$BACKUP_DIR"
run chmod 700 "$BACKUP_DIR"
run cryptsetup luksHeaderBackup "$DEV" --header-backup-file "$BACKUP_FILE"
run chmod 600 "$BACKUP_FILE"
log "Binding LUKS device to TPM2"
if [[ "$YES" -eq 1 ]]; then
run clevis luks bind -y -d "$DEV" tpm2 "$TPM2_CONFIG"
else
run clevis luks bind -d "$DEV" tpm2 "$TPM2_CONFIG"
fi
log "Finding TPM2 Clevis slot"
clevis luks list -d "$DEV"
SLOT="$(clevis luks list -d "$DEV" | awk -F: '/tpm2/ { gsub(/[[:space:]]/, "", $1); print $1; exit }')"
[[ -n "$SLOT" ]] || die "no TPM2 Clevis slot found"
run clevis luks pass -d "$DEV" -s "$SLOT" >/dev/null
log "Configuring Dracut"
run mkdir -p /etc/dracut.conf.d
cat >/etc/dracut.conf.d/90-clevis-tpm2.conf <<'EOF'
force_add_dracutmodules+=" crypt clevis clevis-pin-tpm2 "
EOF
log "Rebuilding initramfs"
run dracut -fv --force-add "crypt clevis clevis-pin-tpm2"
cat <<EOF
Done.
Reboot once with your original LUKS passphrase available.
If auto-unlock fails, enter the passphrase and inspect:
journalctl -b | grep -Ei 'clevis|cryptsetup|luks|tpm'
Header backup:
$BACKUP_FILE
EOF
Verifying After Reboot
After rebooting, check the journal:
journalctl -b --no-pager | grep -Ei 'clevis|cryptsetup|luks|tpm'
A good sign looks like:
clevis-luks-askpass: Unlocked /dev/disk/by-uuid/... successfully
systemd: Started Cryptography Setup for luks-...
If you see that, congratulations: your disk unlocked through TPM2. You have automated the boring part without removing your recovery path.
Cleanup and Rollback
List bindings:
sudo clevis luks list -d /dev/nvme0n1p3
Remove a binding:
sudo clevis luks unbind -d /dev/nvme0n1p3 -s SLOT_NUMBER
sudo dracut -fv
Keep the original LUKS passphrase. Keep the header backup safe. And remember that /boot filling up can break future kernel updates, which is Linux’s way of reminding you that even successful automation still wants snacks.