1. Overview and security model
A kernel update changes the early-boot artifacts that the TPM measures. If your Clevis binding includes PCR 9 (GRUB-measured files, initramfs, kernel command line) or PCR 4 (boot loader image), the TPM will refuse to unseal the LUKS key after the update until the new state is explicitly approved.
The procedure has three phases:
- Before the update — add a temporary binding tied to a stable, less-restrictive PCR (PCR 7 is the recommended fallback). Keep your strict slot and your human passphrase.
- First reboot — boot into the new kernel/initramfs state. The temporary slot unlocks the disk automatically. The strict slot is now stale.
- Re-enrollment — with the new state active and measured, create a new strict binding, verify it, then remove the temporary slot.
Security model during the procedure
| Phase | Slots present | Exposure |
|---|---|---|
| Before update (normal state) | Strict TPM2 (PCR 0,1,2,4,5,7,8,9) + Human passphrase | Baseline |
| During maintenance window | Temporary TPM2 (PCR 7) + Human passphrase | Slightly reduced — PCR 7 checks Secure Boot policy only, not boot artifacts. The strict slot is removed in Step 4 once the loose slot is verified. Window should be short. |
| After re-enrollment (final state) | New strict TPM2 (PCR 0,1,2,4,5,7,8,9) + Human passphrase | Baseline restored |
2. Which PCRs change during a kernel update
| PCR | What it measures | Changes on kernel update? |
|---|---|---|
| 0 | UEFI firmware | No (unless firmware also updated) |
| 1 | UEFI config/NVRAM | Rarely |
| 2 | Option ROMs | No |
| 4 | Boot loader image (shim/GRUB EFI binary) | Only if GRUB EFI binary itself updates |
| 5 | GPT partition table | No |
| 7 | Secure Boot policy and db/dbx state | No (unless MOK change occurs simultaneously) |
| 8 | GRUB commands, kernel command line | Possibly — new GRUB entries may change PCR 8 |
| 9 | Files GRUB loads: kernel image, initramfs, microcode | Yes — always changes |
| 11 | systemd/UKI measurements (if applicable) | Yes, if using UKI |
| 14 | MOK list | Only if MOK keys changed |
The dominant change is PCR 9. A new kernel image and new initramfs are loaded by GRUB and their measurements extend PCR 9. Any Clevis policy including PCR 9 will fail on first boot into the new kernel.
3. Pre-update: verify current state
Run these checks before installing the kernel update.
/dev/nvme0n1p3 with the path to your actual crypto_LUKS partition
(find it with lsblk -o NAME,FSTYPE):
export DEV=/dev/nvme0n1p3
All steps in this guide use $DEV.
Confirm the LUKS device and current bindings
export DEV=/dev/nvme0n1p3
# Confirm it is a LUKS2 device
sudo cryptsetup luksDump "$DEV" | head -10
# List all Clevis slots
sudo clevis luks list -d "$DEV"
Identify the existing Clevis slot and verify it unseals
Auto-detect the single Clevis slot and confirm the TPM path is working before touching anything. No passphrase is needed — this uses the TPM directly.
# Auto-detect the existing Clevis TPM2 slot number
STRICT_SLOT=$(sudo clevis luks list -d "$DEV" | awk -F: '/tpm2/ {gsub(/^[ \t]+/,"",$1); print $1; exit}')
echo "Existing Clevis slot: $STRICT_SLOT"
# Verify it unseals against the TPM right now
sudo clevis luks pass -d "$DEV" -s "$STRICT_SLOT" >/dev/null \
&& echo "Clevis slot $STRICT_SLOT OK — TPM unseal works" \
|| echo "FAIL — TPM path broken before update even started"
Confirm a non-Clevis (human) keyslot exists and is active
This checks structurally that a passphrase slot is present without asking you to type it.
In a luksDump, the Keyslots section lists every slot as
N: luks2 (two-space indent). The Tokens section lists Clevis-owned
slots as Keyslot: N under a clevis token entry.
Any keyslot number not referenced by a Clevis token is a human passphrase slot.
sudo cryptsetup luksDump "$DEV" | awk '
# --- Keyslots section: lines like " 0: luks2" (exactly 2-space indent) ---
/^Keyslots:/ { section="ks"; next }
section=="ks" && /^ [0-9]+: / {
n = $1; gsub(/:/, "", n); all_slots[n] = 1
}
# --- Tokens section: lines like " 0: clevis" then " Keyslot: 1" ---
/^Tokens:/ { section="tok"; next }
/^Digests:/ { section="dig"; next }
section=="tok" && /^ [0-9]+: clevis/ { in_clevis=1; next }
section=="tok" && in_clevis && /Keyslot:/ {
n = $NF; gsub(/[^0-9]/, "", n); clevis_slots[n] = 1; in_clevis=0
}
END {
found = 0
for (s in all_slots) {
if (!(s in clevis_slots)) {
print "Human passphrase slot present: slot " s
found = 1
}
}
if (!found) {
print "WARNING: no human passphrase slot found — no manual recovery path"
exit 1
}
}
'
Expected output on a healthy system (your layout): Human passphrase slot present: slot 0
Read stable PCR values for reference
Only PCRs 0, 1, 2, 4, 5, and 7 are stable across boots — firmware, NVRAM config, option ROMs, boot loader image, GPT, and Secure Boot policy. Do not expect pre-update values for PCR 8 or PCR 9 to be meaningful. Both are extended at runtime by GRUB as it executes commands and loads files. They will differ between boots even with no kernel change, and will certainly differ after one. Including them in the strict policy is precisely what makes the TPM detect a changed boot path — their post-update values are what gets sealed into the new binding in Step 7.
# Stable PCRs worth recording before the update: firmware, config, Secure Boot policy
sudo tpm2_pcrread sha256:0,1,2,4,5,7
# PCR 8 and 9 shown for situational awareness only — these are runtime values and will change.
sudo tpm2_pcrread sha256:8,9
4. Add a temporary loose binding
Add a new Clevis slot bound to PCR 7 only — Secure Boot policy state. PCR 7 is stable across kernel updates (it only changes if Secure Boot keys or MOK entries change). This slot will survive the first reboot into the new kernel.
export DEV=/dev/nvme0n1p3
# Identify the existing Clevis slot (carries over from Step 3 if DEV is still exported)
STRICT_SLOT=$(sudo clevis luks list -d "$DEV" | awk -F: '/tpm2/ {gsub(/^[ \t]+/,"",$1); print $1; exit}')
echo "Authorizing from existing slot: $STRICT_SLOT"
FALLBACK_POLICY='{"pcr_bank":"sha256","pcr_ids":"7"}'
# Attempt to authorize via the existing strict slot (no passphrase prompt)
if LUKS_PASS=$(sudo clevis luks pass -d "$DEV" -s "$STRICT_SLOT" 2>/dev/null); then
echo "Authorized via slot $STRICT_SLOT — binding loose slot"
printf '%s' "$LUKS_PASS" | \
sudo clevis luks bind -y -k - -d "$DEV" tpm2 "$FALLBACK_POLICY"
else
echo "Slot $STRICT_SLOT did not unseal via TPM (PCR mismatch or path error)."
echo "Enter your LUKS passphrase to authorize the loose binding instead:"
read -rs LUKS_PASS
echo
printf '%s' "$LUKS_PASS" | \
sudo clevis luks bind -y -k - -d "$DEV" tpm2 "$FALLBACK_POLICY"
fi
# Auto-detect the newly created loose slot
TEMP_SLOT=$(sudo clevis luks list -d "$DEV" | awk -F: '/tpm2/ {gsub(/^[ \t]+/,"",$1); print $1}' | sort -n | tail -1)
echo "Temporary slot created: $TEMP_SLOT"
# Verify the new loose slot seals correctly — only remove the strict slot if it passes
if sudo clevis luks pass -d "$DEV" -s "$TEMP_SLOT" >/dev/null; then
echo "Temporary slot $TEMP_SLOT OK — removing strict slot"
sudo clevis luks unbind -f -d "$DEV" -s "$STRICT_SLOT"
else
echo "FAIL — loose slot $TEMP_SLOT did not unseal (see clevis/TPM error above)."
echo "The strict slot has been left in place. Check that PCR 7 is stable and"
echo "that the TPM path is working before proceeding."
fi
# Confirm final pre-update state: one loose slot + human passphrase
sudo clevis luks list -d "$DEV"
5. Install the kernel update
sudo apt update
sudo apt upgrade # installs the new kernel alongside the old one
# Rebuild initramfs for all installed kernels
sudo update-initramfs -u -k all
# Regenerate GRUB entries — new kernel appears, old entries remain for now
sudo update-grub
# Confirm both old and new kernels are present in /boot
ls -lh /boot/vmlinuz-* /boot/initrd.img-*
6. First reboot: boot into the new kernel
This reboot moves execution onto the new kernel. GRUB still contains entries for the old kernel, so PCR 8 and PCR 9 are not yet at their final settled values — do not re-enroll the strict binding yet.
sudo reboot
The temporary PCR 7 slot will unseal automatically. Confirm the new kernel is running after login:
uname -r # should show the new kernel version
7. Purge the old kernel and settle GRUB
Removing the old kernel packages causes update-grub to drop the stale GRUB
menu entries. This changes PCR 8 (GRUB commands) and possibly PCR 9 (files GRUB measures
at boot). The second reboot after this step produces the final settled PCR state against
which you will enroll the strict binding.
Identify and remove the old kernel
# Show installed kernel packages
dpkg -l 'linux-image-*' | awk '/^ii/ {print $2}' | sort
# Show the currently running kernel — do NOT remove this one
uname -r
# Remove the old kernel. Replace OLD_VERSION with the version string, e.g. 6.1.0-28-amd64
sudo apt purge linux-image-OLD_VERSION linux-headers-OLD_VERSION
# Regenerate GRUB — old entries removed; PCR 8 will reflect this on next boot
sudo update-grub
# Confirm only the new kernel remains in /boot
ls -lh /boot/vmlinuz-* /boot/initrd.img-*
update-grub runs.
Sealing now captures the wrong values.
8. Second reboot: settle PCR 8 and 9
This reboot produces the final measured boot state: new kernel, new initramfs, and a clean GRUB menu with no old kernel entries. PCR 8 and PCR 9 are now stable. This is the state you will seal the strict binding against.
sudo reboot
The temporary PCR 7 slot unseals automatically again. After login, confirm the state is clean:
# Confirm running kernel
uname -r
# Confirm only one kernel is present in /boot
ls /boot/vmlinuz-*
# Read the settled PCR values — these are what the strict binding will seal against
sudo tpm2_pcrread sha256:0,1,2,4,5,7,8,9
9. Re-enroll the strict PCR policy
With PCR 8 and PCR 9 settled against the clean boot state, create the new strict binding. The temporary PCR 7 slot is the only Clevis slot present — auto-detect and use it for authorization.
export DEV=/dev/nvme0n1p3
# Auto-detect the temporary slot (only Clevis slot present at this point)
TEMP_SLOT=$(sudo clevis luks list -d "$DEV" | awk -F: '/tpm2/ {gsub(/^[ ]+/,"",$1); print $1; exit}')
echo "Authorizing from temporary slot: $TEMP_SLOT"
STRICT_POLICY='{"pcr_bank":"sha256","pcr_ids":"0,1,2,4,5,7,8,9"}'
# Bind the new strict slot, authorized by the temporary PCR-7 slot
sudo clevis luks pass -d "$DEV" -s "$TEMP_SLOT" | sudo clevis luks bind -y -k - -d "$DEV" tpm2 "$STRICT_POLICY"
# Auto-detect the newly created strict slot (highest slot number)
STRICT_SLOT=$(sudo clevis luks list -d "$DEV" | awk -F: '/tpm2/ {gsub(/^[ ]+/,"",$1); print $1}' | sort -n | tail -1)
echo "New strict slot: $STRICT_SLOT"
# Verify it seals correctly against the current settled state
sudo clevis luks pass -d "$DEV" -s "$STRICT_SLOT" >/dev/null && echo "Strict slot $STRICT_SLOT OK — ready to reboot" || echo "FAIL — strict slot did not seal; temporary slot left in place — do not reboot"
sudo clevis luks list -d "$DEV"
10. Third reboot: verify the strict binding
This reboot confirms the new strict binding survives a full TPM unseal cycle. Both slots should unseal — the disk unlocks without any passphrase prompt.
sudo reboot
After login, verify both slots and identify them for cleanup:
export DEV=/dev/nvme0n1p3
sudo clevis luks list -d "$DEV"
for slot in $(sudo clevis luks list -d "$DEV" | awk -F: '/tpm2/ {gsub(/^[ ]+/,"",$1); print $1}'); do
sudo clevis luks pass -d "$DEV" -s "$slot" >/dev/null 2>&1 && echo "slot $slot: OK" || echo "slot $slot: FAIL"
done
Both slots should report OK. If the strict slot reports FAIL, do not proceed to cleanup. The PCR state after this reboot did not match the binding — investigate before removing the temporary slot.
11. Cleanup: remove the temporary slot
export DEV=/dev/nvme0n1p3
# Distinguish slots by policy: strict has the full PCR list, temporary has only "7"
STRICT_SLOT=$(sudo clevis luks list -d "$DEV" | awk -F: '/tpm2/ && $0 !~ /"7"/{gsub(/^[ ]+/,"",$1); print $1; exit}')
TEMP_SLOT=$(sudo clevis luks list -d "$DEV" | awk -F: '/tpm2/ && $0 ~ /"7"/{gsub(/^[ ]+/,"",$1); print $1; exit}')
echo "Strict slot: $STRICT_SLOT | Temporary slot: $TEMP_SLOT"
# Verify the strict slot unseals before authorizing removal of the temporary slot
if sudo clevis luks pass -d "$DEV" -s "$STRICT_SLOT" >/dev/null; then
echo "Strict slot $STRICT_SLOT OK — removing temporary slot $TEMP_SLOT"
sudo clevis luks pass -d "$DEV" -s "$STRICT_SLOT" | \
sudo clevis luks unbind -f -d "$DEV" -s "$TEMP_SLOT"
else
echo "FAIL — strict slot $STRICT_SLOT did not unseal (see clevis/TPM error above)."
echo "The temporary slot has been left in place. The new strict binding may not"
echo "have sealed correctly — re-verify Section 9 before removing the temporary slot."
fi
# Confirm final state
sudo clevis luks list -d "$DEV"
sudo cryptsetup luksDump "$DEV" | sed -n '/Keyslots:/,/Tokens:/p'
0,1,2,4,5,7,8,9 and the human passphrase keyslot. Baseline security
posture restored.
12. Helper script: update-clevis-pcrs.sh
update-clevis-pcrs.sh is a lower-level helper that re-enrolls a single
Clevis slot and optionally removes old ones. It is useful for one-off policy changes
and for understanding what the automation does internally. For kernel updates specifically,
prefer the full automation described in Section 13.
Appropriate uses
- Changing the PCR policy on a system that is not yet running the full automation.
- Manually recovering from a failed automated run (see the operations manual).
- Testing slot operations without the reboot cycle.
update-clevis-pcrs.sh mid-way through an automated run.
The automation tracks slot numbers in its sentinel file. Running the helper script outside
of the automation can create slots the sentinel does not know about, or remove slots it
expects to use for authorization.
13. Fully automated operation
The manual procedure in Sections 3–11 describes every step explicitly so the
underlying mechanism is clear. Once understood, the same process can be delegated
entirely to kernel-update-tpm.sh and its APT hook. The automation
implements the identical flow — no steps are skipped or changed — but executes
them without operator involvement across all three phases and both reboots.
What the automation does vs the manual procedure
| Manual step | Automation equivalent |
|---|---|
| Section 3: verify Clevis slot and human keyslot | Phase 1 preflight — identical checks, same awk-based keyslot detection |
| Section 3: read stable PCRs | Phase 1 — reads PCRs 0,1,2,4,5,7 and logs them |
| Section 4: bind loose PCR-7 slot, test, remove strict slot | Phase 1 — auto-detects the single existing slot, same bind/test/unbind sequence |
| Section 5: apt upgrade, update-initramfs, update-grub | Phase 1 (manual mode) or apt postinst scripts (hook mode) |
| Section 6: first reboot | Phase 1 — systemctl reboot after a 5-second countdown |
| Section 7: re-enroll strict slot, test | Phase 2 — runs automatically via systemd oneshot unit |
| Section 8: second reboot | Phase 2 — systemctl reboot |
| Section 9: verify strict slot, remove temp slot, final report | Phase 3 — runs automatically via systemd oneshot unit |
Installing the toolset
Download pcr-apt-update-install.sh
and run the self-contained installer — it writes, permissions, and verifies all four scripts in one step:
sudo bash pcr-apt-update-install.sh
The installer embeds kernel-update-tpm.sh, kernel-update-tpm-apt-hook,
90-kernel-update-tpm.conf, and update-clevis-pcrs.sh. Run it once;
it is not needed afterward. For the full manual copy procedure see the
operations manual.
After installation: apt handles everything
With the hook in place, a routine sudo apt upgrade that includes a
kernel or initramfs package will automatically invoke the full three-phase process.
No manual intervention is required. The system will reboot twice and return to
baseline security posture.
# Routine upgrade — hook fires automatically if boot-sensitive packages are present
sudo apt upgrade
Manual invocation (hook not installed, or on-demand)
# Auto-detects LUKS device and PCR policy from existing Clevis binding
sudo /usr/local/sbin/kernel-update-tpm.sh
Monitoring an automated run
# Check which phase the automation is in
sudo cat /var/lib/kernel-update-tpm/state
# Follow Phase 2 or 3 output in the journal
sudo journalctl -f -u kernel-update-tpm-resume.service
# Review the APT hook log
sudo tail -f /var/log/kernel-update-tpm-apt-hook.log
secureboot-manual.html for the full installation guide,
verification steps, recovery procedures for each failure mode, and an uninstall procedure.
14. Operational rules
- Verify the existing Clevis slot unseals and a human passphrase slot exists structurally before starting.
- Add the temporary PCR 7 slot and remove the existing strict slot before running
apt upgradeandupdate-initramfs. - This procedure requires three reboots: (1) into the new kernel, (2) after purging the old kernel to settle PCR 8 and 9, (3) to verify the new strict binding.
- Do not re-enroll the strict binding until after the second reboot. Old kernel entries in GRUB affect PCR 8 and PCR 9 until purged and
update-grubhas run. - Do not run
update-initramfsafter re-enrolling the strict binding. It changes PCR 9 immediately. - If a MOK change is also occurring, complete that reboot cycle as a separate operation before beginning this procedure.
- Remove the temporary PCR 7 slot in the same session as verifying the strict binding. Do not leave it across unrelated reboots.
- Keep exactly one human-enterable passphrase slot at all times.
- If PCR 11 or PCR 14 are relevant to your deployment (per
matrix.txt), adjust the strict policy string — the procedure steps are identical.