As of 2026-10-22
This build boots only code signed with your own Secure Boot key, keeps the kernel, initramfs and command line inside one signed file, and lets the TPM release the disk key only to that file after you type a PIN. A signed recovery image on its own partition boots with Secure Boot still on.
Secure Boot with only your key blocks foreign code, the TPM policy refuses to release the disk key to anything else, and LUKS2 covers the rest. Two rows matter most for your brief: the first two.
| Attack | What stops it | What is left |
|---|---|---|
| Booting a USB stick or other external OS | USB boot disabled and an admin password in the firmware. If that is bypassed, the firmware still holds only your key (no Microsoft keys), so a stock live image is rejected. | Anything you sign yourself is trusted. Sign nothing but your own kernel images. |
| Removing the SSD and editing the kernel, initramfs, command line or bootloader | Kernel, initramfs and command line live in one signed UKI file, and systemd-boot is signed too. There is no unsigned /boot and no GRUB config to edit. An edited file fails the signature check. | An attacker can still delete files (denial of service). |
| Editing the encrypted partition | LUKS2 XTS is not authenticated, but altered ciphertext decrypts to garbage and Btrfs checksums flag it. The attacker cannot inject chosen content. | Corruption is detected, not prevented. Replaying an older full copy of the volume is possible. |
| Changing firmware settings (Secure Boot off, own key enrolled) to run their own code | PCR 7 changes, so the TPM refuses to release the key. | You will have to type the recovery key. |
| Booting an older signed UKI, or the recovery image, to reach the key | The recovery image carries no PCR signature, so the TPM never unseals for it. Old UKIs are the weak spot. | Delete old UKIs. Rotating the PCR key revokes every old one at once. |
| Guessing the PIN on a stolen laptop | TPM+PIN with the TPM’s anti-hammering lockout. | A short PIN. Use 8 or more characters. |
| Sniffing the TPM bus | If the TPM is Intel PTT (firmware TPM) there is no external bus. | Not confirmed for this model. Check in Step 0. |
| DMA attacks over USB4 | VT-d on, kernel DMA protection and USB4 security set to user authorization in the BIOS. | Depends on the firmware doing its part. |
| Cold-boot or suspended-RAM attacks | Not covered. Shut down fully when travelling. | Accepted. |
| Root compromise of the running system | Not covered. Your signing keys sit on the encrypted root and root can read them. | Secure Boot protects against offline attackers, not live ones. |
The shipping X14 Pro uses Intel Core Ultra 9 185H or Ultra 5 125H (Meteor Lake) with Arc integrated graphics only, not the Arrow Lake chips first announced (Liliputing review). That helps: no discrete GPU means no add-in option ROMs that might need Microsoft’s keys.
| Item | What the reviews say | What it means for this build |
|---|---|---|
| Storage | One M.2 2280 PCIe 4.0 NVMe slot, user-accessible | One disk, no RAID; the SSD can be pulled, which is your threat model |
| Wi-Fi | MediaTek MT7922, soldered, Wi-Fi 6E | Needs MediaTek firmware from non-free-firmware; use the included Ethernet dongle for the install |
| Dongle | USB-C dongle with 1 GbE, 2 USB-A, HDMI is in the box | Plug it in for debootstrap |
| BIOS | Two UIs. F7 at power-on opens a boot menu with Setup (simple). Fully power off, then hold Esc+Ctrl+H for the advanced one (WinFuture) | The simple BIOS only offered a Secure Boot on/off switch in the review. Key management, if it exists, will be in the advanced one |
| Firmware updates | Reported to work only through GEEKOM PC Manager in Windows (Neowin, read as a search excerpt) | Update the firmware before you wipe Windows |
| Linux status | Ubuntu 25.10 ran; the fingerprint reader and the Fn+P fan-mode key did not work out of the box | Expect no fingerprint login |
Do these before wiping Windows
tpm.msc shows manufacturer INTC for PTT.Microsoft keys: leave them out. With integrated graphics only, soldered Wi-Fi and one NVMe slot, nothing here should need a Microsoft-signed option ROM. A smaller trust set also means no shim or third-party boot chain can ever satisfy the firmware. Step 0 backs up the factory keys, and a BIOS reset restores them if the screen stays black.
If step 2 fails (no way to clear the PK), stop. The alternatives are weaker: signing with Microsoft’s shim and MOK keeps the Microsoft chain in play and can’t give you the “only my key boots” property. Tell me what the advanced BIOS shows and I’ll adapt.
One encrypted volume, two small FAT partitions, one signed file per kernel. Nothing the firmware loads is unsigned, and nothing unsigned is trusted by the TPM.
Disk layout (the whole NVMe; Debian 13 “trixie”, currently 13.7 per the Debian errata page, ships systemd 257 and kernel 6.12 LTS)
| # | Label | Size | GPT type (sgdisk) | Filesystem | Mount | Holds |
|---|---|---|---|---|---|---|
| 1 | ESP | 2 GiB | EF00 | FAT32 | /efi | Signed systemd-boot, one signed UKI per kernel |
| 2 | RECOVERY | 4 GiB | EA00 (XBOOTLDR) | FAT32 | /recovery, noauto | The signed recovery image |
| 3 | cryptroot | rest | 8309 (Linux LUKS) | LUKS2, then Btrfs | none | Everything else, including /boot |
Btrfs subvolumes (mounted with compress=zstd:1,noatime)
| Subvolume | Mount | Why separate |
|---|---|---|
@ |
/ | Snapshotted and rolled back as a unit |
@home |
/home | Survives a system rollback |
@snapshots |
/.snapshots | Snapshots must not nest inside what they snapshot |
@var_log |
/var/log | Logs survive a rollback and stay out of snapshots |
Swap is zram only. Hibernation does not work with the kernel lockdown that Debian turns on under Secure Boot, so a swap file buys you nothing.
Boot chain
/efi/EFI/Linux and on the RECOVERY partition. Its editor is off, so the command line cannot be changed.enter-initrd phase. You type the PIN.leave-initrd. After that nothing can unseal the key again. Then root switches over.The recovery UKI is built without a PCR signature, so step 4 can never unseal for it. It asks for a passphrase or the recovery key instead.
Keys
| Key | Does | Lives |
|---|---|---|
| PK and KEK | Only needed to change what the firmware trusts | Offline after enrolment (encrypted USB) |
| db | Signs systemd-boot, every UKI, the recovery image | /etc/sb-keys on the encrypted root |
| PCR key | Signs the TPM policy embedded in each UKI | /etc/sb-keys on the encrypted root |
| Passphrase and recovery key | Unlock when the TPM will not | Paper or password manager, never on this laptop |
Three choices worth knowing about
@. Only signed UKIs are on unencrypted storage.You need a USB stick (4 GB or more), a second stick or another computer to keep the factory key backup, and the Ethernet dongle plugged in.
1. Get the live image. Download the official Debian 13 live image (the standard flavour is enough) from debian.org/CD/live. Check it against SHA512SUMS and its signed checksum file, then write it to the stick:
dd if=debian-live-*.iso of=/dev/sdX bs=4M conv=fsync status=progress
2. Boot it with the factory firmware settings. Press F7 at power-on and pick the USB stick. Do this before touching any Secure Boot setting, so the factory keys are still there to copy.
3. Check for UEFI mode and a TPM. Open a root shell and run:
test -d /sys/firmware/efi && echo UEFI ok
apt update && apt install -y efitools tpm2-tools
ls /dev/tpm0 /dev/tpmrm0
tpm2_getcap properties-fixed | grep -A2 TPM2_PT_MANUFACTURER
Manufacturer 0x494E5443 (“INTC”) means Intel PTT, a firmware TPM. Anything else is a discrete chip, and then the PIN becomes essential.
4. Back up the factory Secure Boot variables. This is your way back if something goes wrong:
mkdir ~/factory-sb && cd ~/factory-sb
for v in PK KEK db dbx; do efi-readvar -v $v -o $v.esl; done
ls -l
Copy the four files to the second stick or another machine, and keep them.
5. Put the firmware in Setup Mode. Power off, open the advanced BIOS (Esc+Ctrl+H from a full power-off), and:
6. Reboot the live USB and confirm Setup Mode.
bootctl status | grep -i 'secure boot' # expect: disabled (setup)
efi-readvar -v PK # expect: no entries
If bootctl still reports Secure Boot as enabled, the PK is still there. Go back to the BIOS.
Still in the live session, as root, with the firmware in Setup Mode and the Ethernet dongle connected. This wipes the whole SSD, so confirm the device name first.
1. Identify the disk and partition it. The internal NVMe is usually /dev/nvme0n1. NVMe partitions take a p suffix (nvme0n1p3).
lsblk -o NAME,SIZE,MODEL,TRAN
export DISK=/dev/nvme0n1
blkdiscard -f $DISK # optional: TRIM everything, removes Windows remnants
sgdisk --zap-all $DISK
sgdisk -n1:0:+2GiB -t1:EF00 -c1:ESP $DISK
sgdisk -n2:0:+4GiB -t2:EA00 -c2:RECOVERY $DISK
sgdisk -n3:0:0 -t3:8309 -c3:cryptroot $DISK
partprobe $DISK
2. Create the LUKS2 volume. Choose a long passphrase (six or more random words). It is the fallback you type if the TPM refuses, so you must be able to remember it. argon2id uses a lot of RAM per unlock, which is fine with 32 GB.
cryptsetup luksFormat --type luks2 --cipher aes-xts-plain64 --key-size 512 \
--hash sha512 --pbkdf argon2id --sector-size 4096 --label cryptroot ${DISK}p3
cryptsetup open --allow-discards --perf-no_read_workqueue --perf-no_write_workqueue \
--persistent ${DISK}p3 cryptroot
--persistent stores the flags in the LUKS header, so the kernel command line does not have to repeat them. --allow-discards passes TRIM through, which keeps the SSD fast but reveals which blocks are unused. Drop it if you care more about that than about SSD wear.
3. Make the filesystems and the Btrfs subvolumes. -m dup keeps two copies of the metadata, which costs almost nothing and lets a scrub repair it.
mkfs.vfat -F32 -n ESP ${DISK}p1
mkfs.vfat -F32 -n RECOVERY ${DISK}p2
mkfs.btrfs -L debian -m dup /dev/mapper/cryptroot
mount /dev/mapper/cryptroot /mnt
for sv in @ @home @snapshots @var_log; do btrfs subvolume create /mnt/$sv; done
umount /mnt
4. Mount everything under /mnt.
O=compress=zstd:1,noatime
mount -o subvol=@,$O /dev/mapper/cryptroot /mnt
mkdir -p /mnt/{home,.snapshots,var/log,efi,recovery}
mount -o subvol=@home,$O /dev/mapper/cryptroot /mnt/home
mount -o subvol=@snapshots,$O /dev/mapper/cryptroot /mnt/.snapshots
mount -o subvol=@var_log,$O /dev/mapper/cryptroot /mnt/var/log
mount -o umask=0077 ${DISK}p1 /mnt/efi
mount -o umask=0077 ${DISK}p2 /mnt/recovery
5. Install the base system and enter it.
apt update && apt install -y debootstrap
debootstrap --components=main,contrib,non-free,non-free-firmware \
--include=locales,sudo,ca-certificates trixie /mnt http://deb.debian.org/debian
cp -L /etc/resolv.conf /mnt/etc/resolv.conf
for d in dev proc sys run; do mount --rbind /$d /mnt/$d && mount --make-rslave /mnt/$d; done
chroot /mnt /bin/bash
The rest of Steps 2 and 3 happens inside this chroot. The bind-mounted /sys also gives you the firmware variables, which you need for enrolment.
The kernel goes in last (end of Step 3), after the signing keys and the build script exist, so its install hook produces a signed UKI straight away.
1. Basics and apt sources. Debian 13 uses the deb822 format.
echo x14 > /etc/hostname # your own hostname
rm -f /etc/apt/sources.list
cat > /etc/apt/sources.list.d/debian.sources <<'EOF'
Types: deb
URIs: http://deb.debian.org/debian
Suites: trixie trixie-updates
Components: main contrib non-free non-free-firmware
Signed-By: /usr/share/keyrings/debian-archive-keyring.gpg
Types: deb
URIs: http://security.debian.org/debian-security
Suites: trixie-security
Components: main contrib non-free non-free-firmware
Signed-By: /usr/share/keyrings/debian-archive-keyring.gpg
EOF
apt update
apt install -y tzdata keyboard-configuration console-setup
dpkg-reconfigure locales tzdata keyboard-configuration
2. fstab. There is deliberately no /etc/crypttab: the initramfs unlocks the root volume from the kernel command line.
ROOT_UUID=$(blkid -s UUID -o value /dev/mapper/cryptroot)
ESP_UUID=$(blkid -s UUID -o value ${DISK}p1)
REC_UUID=$(blkid -s UUID -o value ${DISK}p2)
O=compress=zstd:1,noatime
cat > /etc/fstab <<EOF
UUID=$ROOT_UUID / btrfs subvol=@,$O 0 0
UUID=$ROOT_UUID /home btrfs subvol=@home,$O 0 0
UUID=$ROOT_UUID /.snapshots btrfs subvol=@snapshots,$O 0 0
UUID=$ROOT_UUID /var/log btrfs subvol=@var_log,$O 0 0
UUID=$ESP_UUID /efi vfat umask=0077 0 2
UUID=$REC_UUID /recovery vfat umask=0077,noauto 0 0
EOF
3. dracut config, then packages. dracut goes in before the kernel so the kernel’s hooks use it. hostonly=no builds a generic initramfs, so the keyboard and NVMe drivers are never missing when you need to type the PIN. It is larger (roughly 100 MB) but the 2 GiB ESP has room.
mkdir -p /etc/dracut.conf.d
cat > /etc/dracut.conf.d/10-uki.conf <<'EOF'
hostonly="no"
add_dracutmodules+=" systemd crypt btrfs tpm2-tss "
add_drivers+=" tpm_crb tpm_tis "
compress="zstd"
early_microcode="yes"
EOF
apt install -y dracut
apt install -y systemd-boot systemd-boot-efi systemd-ukify systemd-cryptsetup cryptsetup \
btrfs-progs tpm2-tools sbsigntool efitools efibootmgr openssl python3-cryptography \
dosfstools intel-microcode firmware-linux network-manager systemd-zram-generator gnupg
dpkg -l initramfs-tools 2>/dev/null | grep ^ii && apt purge -y initramfs-tools
firmware-linux covers the MediaTek MT7922 Wi-Fi firmware and Intel microcode loading. If the last line removes initramfs-tools, that is correct: you want exactly one initramfs tool.
4. Kernel command line. This single line is embedded in every UKI, so it is signed and measured.
LUKS_UUID=$(cryptsetup luksUUID ${DISK}p3)
mkdir -p /etc/kernel
cat > /etc/kernel/cmdline <<EOF
root=UUID=$ROOT_UUID rootfstype=btrfs rootflags=subvol=@,compress=zstd:1,noatime rw rd.luks.name=$LUKS_UUID=cryptroot rd.luks.options=$LUKS_UUID=tpm2-device=auto rd.shell=0 rd.emergency=reboot intel_iommu=on
EOF
rd.luks.name and rd.luks.options make systemd-cryptsetup open the volume, trying the TPM token in the LUKS header first and falling back to a passphrase prompt.rd.shell=0 rd.emergency=reboot removes the initramfs rescue shell. That shell would run while the TPM policy still allows unsealing, so a forced boot failure could otherwise hand an attacker a root prompt.intel_iommu=on enables DMA remapping against USB4 DMA attacks.quiet, so you can see problems. Add it once everything works.5. zram swap, a user, and a locked root.
cat > /etc/systemd/zram-generator.conf <<'EOF'
[zram0]
zram-size = ram / 2
compression-algorithm = zstd
EOF
adduser yourname && usermod -aG sudo yourname
passwd -l root
With root locked, emergency and rescue mode on the installed system cannot hand anyone a shell. Install a desktop (task-gnome-desktop, task-kde-desktop) after the first successful boot, not now.
Still in the chroot. This uses only tools that are in Debian 13: openssl, efitools, sbsigntool, systemd-ukify. sbctl is convenient, but it was only in Debian’s NEW queue (0.18-1) when I looked, and I could not confirm it is in trixie, so nothing here depends on it.
1. Create the keys. RSA-2048 is accepted by every firmware. The PK and KEK are only used to change what the firmware trusts.
install -d -m 0700 /etc/sb-keys && cd /etc/sb-keys
uuidgen --random > GUID.txt
for k in PK KEK db; do
openssl req -new -x509 -newkey rsa:2048 -sha256 -days 3650 -nodes \
-subj "/CN=X14 $k/" -keyout $k.key -out $k.crt
done
openssl genpkey -algorithm RSA -pkeyopt rsa_keygen_bits:2048 -out pcr.key
openssl rsa -pubout -in pcr.key -out pcr.pub
chmod 600 *.key
2. Convert them into the format the firmware takes.
GUID=$(cat GUID.txt)
cert-to-efi-sig-list -g $GUID PK.crt PK.esl && sign-efi-sig-list -k PK.key -c PK.crt PK PK.esl PK.auth
cert-to-efi-sig-list -g $GUID KEK.crt KEK.esl && sign-efi-sig-list -k PK.key -c PK.crt KEK KEK.esl KEK.auth
cert-to-efi-sig-list -g $GUID db.crt db.esl && sign-efi-sig-list -k KEK.key -c KEK.crt db db.esl db.auth
3. Tell ukify how to build and sign. The PCR signature covers only the enter-initrd phase, because unlocking the disk is the only thing the TPM policy is for.
cat > /etc/kernel/uki.conf <<'EOF'
[UKI]
SecureBootSigningTool=sbsign
SignKernel=true
SecureBootPrivateKey=/etc/sb-keys/db.key
SecureBootCertificate=/etc/sb-keys/db.crt
Cmdline=@/etc/kernel/cmdline
PCRBanks=sha256
[PCRSignature:initrd]
Phases=enter-initrd
PCRPrivateKey=/etc/sb-keys/pcr.key
PCRPublicKey=/etc/sb-keys/pcr.pub
EOF
ls /usr/lib/systemd/systemd-measure # ukify needs this; if missing: apt-file search systemd-measure
4. The build script. It builds the initramfs, wraps it with the kernel and command line into a UKI, signs it, checks the signature, and swaps it into place atomically. It also re-signs systemd-boot, because Debian’s package ships it unsigned.
cat > /usr/local/sbin/build-uki <<'EOF'
#!/bin/bash
# build-uki <kernel-version> | --all | --auto | --verify
set -euo pipefail
ESP=/efi; REC=/recovery; KEYS=/etc/sb-keys
STAMP=/var/lib/build-uki.stamp
SDBOOT=/usr/lib/systemd/boot/efi/systemd-bootx64.efi
mountpoint -q "$ESP" || { echo "build-uki: $ESP is not mounted" >&2; exit 1; }
TMP=$(mktemp -d /var/tmp/build-uki.XXXXXX)
trap 'rm -rf "$TMP"; rm -f "$ESP"/EFI/*/*.new' EXIT
sign() { sbsign --key "$KEYS/db.key" --cert "$KEYS/db.crt" --output "$2" "$1" >/dev/null; }
install_bootloader() {
local d
for d in EFI/systemd/systemd-bootx64.efi EFI/BOOT/BOOTX64.EFI; do
install -d "$ESP/${d%/*}"
sign "$SDBOOT" "$ESP/$d.new"
mv -f "$ESP/$d.new" "$ESP/$d"
done
}
build_one() {
local kver=$1 out="$ESP/EFI/Linux/debian-$1.efi"
[[ -f /boot/vmlinuz-$kver ]] || { echo "build-uki: no kernel for $kver" >&2; return 0; }
install -d "$ESP/EFI/Linux"
dracut --force --kver "$kver" "$TMP/initrd"
ukify build --config=/etc/kernel/uki.conf --linux="/boot/vmlinuz-$kver" \
--initrd="$TMP/initrd" --uname="$kver" --output="$out.new"
sbverify --cert "$KEYS/db.crt" "$out.new" >/dev/null
mv -f "$out.new" "$out"
echo "build-uki: wrote $out"
}
build_all() {
local k f v
for k in /lib/modules/*; do build_one "${k##*/}"; done
for f in "$ESP"/EFI/Linux/debian-*.efi; do
[[ -e $f ]] || continue
v=${f##*/debian-}; v=${v%.efi}
[[ -f /boot/vmlinuz-$v ]] || { echo "build-uki: removing stale $f"; rm -f "$f"; }
done
}
verify() {
local f bad=0
for f in "$ESP"/EFI/BOOT/BOOTX64.EFI "$ESP"/EFI/systemd/systemd-bootx64.efi \
"$ESP"/EFI/Linux/*.efi "$REC"/EFI/Linux/*.efi; do
[[ -e $f ]] || continue
if sbverify --cert "$KEYS/db.crt" "$f" >/dev/null 2>&1; then echo "ok $f"
else echo "BAD $f" >&2; bad=1; fi
done
return $bad
}
state() {
{ dpkg-query -W -f='${Package} ${Version}\n' systemd systemd-boot-efi systemd-cryptsetup \
systemd-ukify cryptsetup-bin dracut dracut-core btrfs-progs intel-microcode \
tpm2-tools firmware-linux-nonfree firmware-misc-nonfree firmware-mediatek 2>/dev/null || true
ls /lib/modules
sha256sum /etc/kernel/cmdline /etc/kernel/uki.conf /etc/dracut.conf.d/*.conf
} | sha256sum | cut -d' ' -f1
}
case "${1:-}" in
--verify) verify ;;
--all) install_bootloader; build_all; state > "$STAMP"; verify ;;
--auto) verify >/dev/null 2>&1 || { echo "build-uki: re-signing bootloader" >&2; install_bootloader; }
if [[ "$(cat "$STAMP" 2>/dev/null || true)" != "$(state)" ]]; then
build_all; state > "$STAMP"
fi
verify ;;
"") echo "usage: build-uki <kernel-version>|--all|--auto|--verify" >&2; exit 2 ;;
*) install_bootloader; build_one "$1"; verify ;;
esac
EOF
chmod 755 /usr/local/sbin/build-uki
5. Hooks. One runs on kernel install, one on kernel removal, and one after every apt run, so a microcode, firmware, systemd or cryptsetup update rebuilds the UKIs and a stray unsigned bootloader gets re-signed. The last line stops systemd from replacing the signed loader with the unsigned shipped copy.
install -d /etc/kernel/postinst.d /etc/kernel/postrm.d
printf '#!/bin/sh\nset -e\nexec /usr/local/sbin/build-uki "$1"\n' > /etc/kernel/postinst.d/zz-build-uki
printf '#!/bin/sh\nrm -f "/efi/EFI/Linux/debian-$1.efi"\n' > /etc/kernel/postrm.d/zz-build-uki
chmod 755 /etc/kernel/postinst.d/zz-build-uki /etc/kernel/postrm.d/zz-build-uki
echo 'DPkg::Post-Invoke { "/usr/local/sbin/build-uki --auto"; };' > /etc/apt/apt.conf.d/99-build-uki
systemctl mask systemd-boot-update.service
6. Install the kernel. The hook builds and signs the first UKI.
apt install -y linux-image-amd64
build-uki --verify
ukify inspect /efi/EFI/Linux/debian-*.efi | grep -E '^\.|^Section|pcr'
You should see .linux, .initrd, .cmdline, .osrel, .uname, .pcrsig and .pcrpkey, and ok for every file. Then check that the initramfs really contains the TPM and PCR pieces:
K=$(ls /lib/modules | head -n1); dracut --force /tmp/test-initrd "$K"
lsinitrd /tmp/test-initrd | grep -Ei 'pcrextend|pcrphase|systemd-cryptsetup|libtss2|btrfs.ko'; rm /tmp/test-initrd
The pcrextend or pcrphase lines matter: they are what extends PCR 11 at leave-initrd and closes the unseal window. If they are missing, stop and tell me before going on.
7. Boot menu and firmware entry.
install -d /efi/loader
cat > /efi/loader/loader.conf <<'EOF'
default debian-*
timeout 0
editor no
console-mode keep
secure-boot-enroll off
EOF
efibootmgr --create --disk $DISK --part 1 --loader '\EFI\systemd\systemd-bootx64.efi' --label 'Debian (systemd-boot)'
timeout 0 hides the menu. Hold Space while booting to get it. editor no stops anyone changing the command line. \EFI\BOOT\BOOTX64.EFI is also signed, so the machine boots even if the firmware forgets its boot entries.
1. Enroll the keys. Run this in the chroot, with the firmware still in Setup Mode. Order matters: the PK goes last, because writing it ends Setup Mode.
cd /etc/sb-keys
efi-updatevar -f db.auth db
efi-updatevar -f KEK.auth KEK
efi-updatevar -f PK.auth PK
efi-readvar -v PK | head
efi-readvar should now show your PK. If efi-updatevar refuses (invalid argument, permission denied), the firmware is not in Setup Mode or it rejects .auth files. Check the BIOS again. If the BIOS has an “enroll key from file” page, copy the .crt files to a FAT stick and use that instead.
2. Leave the chroot and back up the keys. Do this from the live session, onto a stick that is not the installer. The archive is encrypted with a passphrase you choose.
exit
tar -C /mnt/etc -cz sb-keys | gpg --symmetric --cipher-algo AES256 -o /path/to/stick/sb-keys.tgz.gpg
3. Shut down cleanly.
umount -R /mnt
cryptsetup close cryptroot
poweroff
4. In the advanced BIOS, confirm Secure Boot is switched on (some firmwares need it enabled again after the PK is set) and the TPM is enabled. Leave USB boot enabled until the tests in the test plan are done. Remove the installer stick and boot the SSD.
5. First-boot checks. You will be asked for the LUKS passphrase, because the TPM is not enrolled yet. Log in and run:
bootctl status | grep -iE 'secure boot|tpm2|firmware'
cat /sys/kernel/security/lockdown # expect: none [integrity] confidentiality
systemd-analyze has-tpm2
sudo build-uki --verify
Expected: Secure Boot enabled (user), TPM2 support yes, lockdown in [integrity], and ok on every file. Debian’s kernel switches on lockdown by itself under Secure Boot, which is also why hibernation and unsigned out-of-tree modules (DKMS) will not work.
If the machine shows a “Security Violation” or goes straight back to the firmware, a file is not signed by the key that was enrolled. Boot the live stick again (turn Secure Boot off in the BIOS first if needed), mount the system as in Step 1, and run build-uki --all in the chroot. If nothing helps, use the BIOS option to restore the factory keys. The .esl files from Step 0 are your reference copy of what was there.
Do this only after Step 4 booted correctly with Secure Boot on. The key is sealed to the Secure Boot state, which only exists in its final form now.
1. Add a recovery key. It is a random 256-bit string. Write it on paper and keep it apart from the laptop. Do not photograph it.
sudo systemd-cryptenroll /dev/nvme0n1p3 --recovery-key
2. Seal the disk key to the TPM, with a PIN. The policy is: PCR 7 (Secure Boot state and keys) plus a PCR 11 policy signed by your PCR key. A new kernel gets a new signature, so kernel updates never require re-enrolling. Choose a PIN of 8 or more characters; letters are allowed.
sudo systemd-cryptenroll /dev/nvme0n1p3 --tpm2-device=auto --tpm2-pcrs=7 \
--tpm2-public-key=/etc/sb-keys/pcr.pub --tpm2-public-key-pcrs=11 --tpm2-with-pin=yes
sudo cryptsetup luksDump /dev/nvme0n1p3 | grep -A12 -i tokens
The token list should show a systemd-tpm2 entry with the PIN flag set, tpm2-pcrs containing 7, and the public-key PCR 11.
3. Reboot and test. You should be asked for the TPM PIN, not the long passphrase. If you get the passphrase prompt instead, the TPM refused. Look at why:
journalctl -b | grep -iE 'tpm2|cryptsetup' | head -30
4. Optional hardening: bind PCR 12. systemd-stub measures credentials and add-ons it finds next to the UKI into PCR 12. An attacker with the SSD could plant a credential file on the ESP, and it would reach the initramfs. Binding PCR 12 makes that change the TPM state. I could not verify this value on your hardware, so measure first: reboot twice and compare.
sudo systemd-analyze pcrs 12
If the value is identical across boots (normally all zeros when nothing is injected), re-enroll with it included, then reboot and confirm the PIN prompt still unlocks:
sudo systemd-cryptenroll /dev/nvme0n1p3 --wipe-slot=tpm2 --tpm2-device=auto --tpm2-pcrs=7+12 \
--tpm2-public-key=/etc/sb-keys/pcr.pub --tpm2-public-key-pcrs=11 --tpm2-with-pin=yes
If it varies between boots, skip this step. From then on, anything that changes PCR 12 (for example a credential you add on purpose) means typing the recovery key.
5. Back up the LUKS header and encrypt it like the keys. Anyone holding the header and an old passphrase can unlock the disk, so destroy old copies whenever you change a passphrase.
sudo cryptsetup luksHeaderBackup /dev/nvme0n1p3 --header-backup-file /tmp/luks-header.img
gpg --symmetric --cipher-algo AES256 -o /path/to/stick/luks-header.img.gpg /tmp/luks-header.img
sudo shred -u /tmp/luks-header.img
6. Take the PK and KEK private keys off the laptop. First prove the encrypted key backup from Step 4 opens, then delete the two keys. You only need them to change what the firmware trusts.
gpg -d /path/to/stick/sb-keys.tgz.gpg | tar tz | head
sudo rm /etc/sb-keys/PK.key /etc/sb-keys/KEK.key
The recovery system is one signed EFI file on the RECOVERY partition: a small Debian whose root filesystem is packed inside the UKI’s initramfs. Because it is signed with your db key, it boots with Secure Boot on. Because it carries no PCR signature, the TPM never releases the disk key to it, so it asks for your passphrase or recovery key. systemd-boot finds it on its own, since the partition has the XBOOTLDR type.
1. Install the build tools on the running system.
sudo apt install mkosi debootstrap cpio zstd
mkosi --version
2. Write the image config. The simplest start is the Codeberg project swsnr/rescue-image, whose README describes exactly this: an mkosi config that builds a single EFI executable rescue system, with SecureBootKey= and SecureBootCertificate= for signing. Fork it, set the distribution to Debian trixie, and use these ideas. I could not verify the exact option names against the mkosi version in trixie, so check man mkosi if a key is rejected.
mkdir -p ~/recovery && cd ~/recovery
printf 'hashed:%s\n' "$(openssl passwd -6)" > mkosi.rootpw && chmod 600 mkosi.rootpw
cat > mkosi.conf <<'EOF'
[Distribution]
Distribution=debian
Release=trixie
[Output]
Format=uki
Output=recovery
ImageId=debian-recovery
[Content]
Packages=linux-image-amd64
systemd systemd-sysv udev
cryptsetup systemd-cryptsetup btrfs-progs dosfstools e2fsprogs gdisk parted
tpm2-tools efitools sbsigntool efibootmgr
iproute2 iwd firmware-linux nano less
EOF
cat > mkosi.local.conf <<'EOF'
[Validation]
SecureBoot=yes
SecureBootKey=/etc/sb-keys/db.key
SecureBootCertificate=/etc/sb-keys/db.crt
SecureBootSignTool=sbsign
SignExpectedPcr=no
EOF
The root password is the only thing standing between someone at the keyboard and a shell on the recovery system, so make it a strong one. Keep PCR signing off for this image (SignExpectedPcr=no) and never give it the PCR key. That is what keeps it from ever unsealing the disk. build-uki --verify only checks signatures, so use the .pcrsig check below.
3. Build it and put it on the partition.
sudo mkosi -f build
sudo mount /recovery
sudo install -D -m 644 mkosi.output/recovery*.efi /recovery/EFI/Linux/recovery.efi
sudo build-uki --verify
ukify inspect /recovery/EFI/Linux/recovery.efi | grep -i pcrsig || echo "no .pcrsig: good"
sudo umount /recovery
4. Add a direct firmware entry. This lets you reach recovery from the F7 boot menu even if systemd-boot itself is damaged.
sudo efibootmgr --create --disk /dev/nvme0n1 --part 2 --loader '\EFI\Linux\recovery.efi' --label 'Debian Recovery (direct)'
5. Use it. Hold Space at power-on and pick recovery, or go there once with sudo systemctl reboot --boot-loader-entry=recovery.efi (the entry ID is the file name). Log in as root. A typical repair session:
cryptsetup open /dev/nvme0n1p3 cryptroot # passphrase or recovery key
mount -o subvol=@,compress=zstd:1 /dev/mapper/cryptroot /mnt
mount /dev/nvme0n1p1 /mnt/efi
mount /dev/nvme0n1p2 /mnt/recovery
for d in dev proc sys run; do mount --rbind /$d /mnt/$d; done
chroot /mnt
build-uki --all # rebuild and re-sign every UKI and systemd-boot
The signing keys are on the encrypted root, so once you have unlocked it, the chroot can sign without any USB stick. Re-enroll the TPM from the repaired main system after a normal reboot, not from recovery.
Rules for the recovery image
EFI/BOOT/BOOTX64.EFI. It will boot only if you allow USB boot in the BIOS.Do this last, after tests 2 and 3 of the test plan, which need USB boot still enabled. Menu names differ between BIOS builds, and I have not seen this model’s advanced BIOS, so treat the list as what to look for. Not every item may exist.
| Setting | Target | Why |
|---|---|---|
| Administrator or supervisor password | Set a long one and store it with the recovery key | Stops casual changes to Secure Boot and boot order. It is a speed bump, not a defence against a determined attacker, because flashing or a CMOS reset changes PCR 7 anyway |
| USB or removable boot | Disabled | First line against booting an external drive |
| Boot order | NVMe only; network (PXE) boot off | Nothing else to load |
| Secure Boot | Enabled, user mode, your keys | The core of the design |
| VT-d, kernel DMA protection | On | Needed for intel_iommu=on to protect against DMA over USB4 |
| USB4 or Thunderbolt security | User authorization (or the strictest level offered) | Blocks unknown PCIe devices from attaching |
| TPM (Intel PTT) | On. Never clear it after enrolment | Clearing destroys the sealed key; you would need the recovery key |
Two cautions. First, do not switch Secure Boot off and on again after the TPM is enrolled, because PCR 7 changes and the TPM will refuse until you re-enroll. Second, if the BIOS has no way to disable USB boot, the F7 menu will still list a stick, but it cannot boot: your key is the only one the firmware trusts, so a stock live image fails the signature check.
Updates. apt upgrade is all you do. The kernel hook builds and signs a new UKI, and the post-apt hook rebuilds everything if microcode, firmware, systemd, cryptsetup, dracut or your config changed. sudo build-uki --verify lists what is signed. Keep the current kernel plus one previous one; apt autoremove deletes older kernels and their UKIs. A kernel you remove can no longer be booted by an attacker as a rollback.
Revoking old UKIs. An old signed UKI still satisfies the TPM policy, so if a kernel you once ran turns out to be exploitable, rotate the PCR key. Every UKI signed with the old key then stops unsealing the disk.
cd /etc/sb-keys
sudo openssl genpkey -algorithm RSA -pkeyopt rsa_keygen_bits:2048 -out pcr.key
sudo openssl rsa -pubout -in pcr.key -out pcr.pub
sudo chmod 600 pcr.key
sudo build-uki --all
# reboot, type the recovery key once, then re-enroll:
sudo systemd-cryptenroll /dev/nvme0n1p3 --wipe-slot=tpm2 --tpm2-device=auto --tpm2-pcrs=7 \
--tpm2-public-key=/etc/sb-keys/pcr.pub --tpm2-public-key-pcrs=11 --tpm2-with-pin=yes
The same command (with --wipe-slot=tpm2) also changes your PIN. If you bound PCR 12 in Step 5, use --tpm2-pcrs=7+12.
Snapshots with Btrfs. snapper uses the @snapshots subvolume you already mount at /.snapshots.
sudo apt install snapper
sudo umount /.snapshots && sudo rmdir /.snapshots
sudo snapper -c root create-config /
sudo btrfs subvolume delete /.snapshots
sudo mkdir /.snapshots && sudo mount /.snapshots && sudo chmod 750 /.snapshots
sudo snapper -c root create -d "fresh install"
sudo systemctl enable --now snapper-timeline.timer snapper-cleanup.timer
Two things to remember. A snapshot restores /boot and /lib/modules but not the UKIs on the ESP, so after any rollback run sudo build-uki --all (from the recovery chroot if the system will not boot). And snapshots on the same disk are not backups: send them to an external encrypted disk with btrfs send or btrbk.
Btrfs upkeep. Scrub monthly (sudo apt install btrfsmaintenance, or sudo btrfs scrub start -B /) and glance at sudo btrfs device stats /. For VM images or databases, make an empty directory chattr +C before filling it. That turns off copy-on-write, and with it checksums, for those files.
Firmware updates. This is the one routine event that breaks the TPM seal, because a BIOS update can reset the Secure Boot keys or change PCR 7. GEEKOM’s updater is reported to be a Windows tool, and I did not find a Linux route or a standalone flasher for the X14 Pro. Before flashing, have the recovery key and your .auth files (they stay in /etc/sb-keys). Afterwards:
efi-updatevar commands from Step 4 with the .auth files, turn Secure Boot on, and re-enroll the TPM.Carrying the laptop. Shut it down rather than suspending. A suspended machine keeps the disk key in RAM, and cold-boot attacks are outside this design. Keep the lock screen on a strong password.
Run tests 2 and 3 while USB boot is still enabled, so you learn that Secure Boot alone stops them, then do Step 7. Run the rest afterwards. Nothing here destroys data, but keep the recovery key at hand.
| # | Test | Expected result |
|---|---|---|
| 1 | Cold boot | systemd-boot is hidden, you see the TPM PIN prompt, then the login screen. No passphrase prompt |
| 2 | Boot a stock Debian live USB (F7 menu) | Refused with a security or signature error, or silently skipped. It must never reach a desktop |
| 3 | Tampered UKI. sudo cp /efi/EFI/Linux/debian-$(uname -r).efi /efi/EFI/Linux/tamper-test.efi, then overwrite a few bytes: sudo dd if=/dev/zero of=/efi/EFI/Linux/tamper-test.efi bs=1 seek=100000 count=16 conv=notrunc. Hold Space, pick tamper-test |
Security violation. Delete the file afterwards. (The name avoids the debian-* default) |
| 4 | SSD in another computer | lsblk -f shows crypto_LUKS. The ESP holds only signed public files. Nothing unlocks without the passphrase or recovery key |
| 5 | Turn Secure Boot off in the BIOS, boot, then turn it back on | With it off you get the passphrase prompt, not the PIN prompt (PCR 7 changed). With it back on, the PIN works again |
| 6 | Boot recovery |
Boots with Secure Boot on. Never asks for a PIN. cryptsetup open /dev/nvme0n1p3 x asks for a passphrase |
| 7 | cat /sys/kernel/security/lockdown |
[integrity] selected |
| 8 | sudo build-uki --all, reboot |
PIN prompt still works. This proves the signed policy survives a rebuild |
| 9 | journalctl -b | grep -i pcr |
Lines showing PCR 11 being extended at the initrd phases |
| 10 | sudo build-uki --verify |
ok on every file |
Test 5 is the most informative one: it shows that changing the firmware state really does stop the TPM from releasing the key.
Almost every failure ends the same way: unlock with the recovery key, fix the cause, re-enroll the TPM if needed. Only losing both the passphrase and the recovery key is unrecoverable.
| Symptom | Likely cause | Fix |
|---|---|---|
| Passphrase prompt instead of the PIN | PCR 7 changed (BIOS update, Secure Boot toggled, keys reset), or the UKI has no .pcrsig |
Check ukify inspect shows .pcrsig. If it does, boot with the recovery key and re-enroll as in Step 5 |
| Passphrase prompt on the new kernel only | The UKI was signed with a PCR key other than the enrolled one | Confirm /etc/sb-keys/pcr.pub is the key you enrolled, run build-uki --all, or re-enroll |
| “Security Violation”, or straight back to the firmware | A file is unsigned or signed with the wrong key (often an overwritten systemd-boot) | Boot recovery from the F7 menu. Failing that, switch Secure Boot off, boot, and run build-uki --all |
| Boot loops back to the firmware after the logo | Something failed in the initramfs and rd.emergency=reboot rebooted |
Boot recovery, mount the system as in Step 6, temporarily remove rd.shell=0 rd.emergency=reboot from /etc/kernel/cmdline, run build-uki --all, read the error, then put them back |
| No keyboard at the PIN prompt | A driver missing from the initramfs | Use an external USB keyboard, then add the driver with add_drivers+= in /etc/dracut.conf.d/ and rebuild |
| No menu and no recovery entry | The ESP was damaged, or the NVRAM entries were lost | Use the direct Debian Recovery firmware entry, or the signed BOOTX64.EFI fallback, then build-uki --all |
| Black screen after enrolling your keys | Firmware needed a Microsoft-signed component | Reset to factory keys in the BIOS. To keep your keys and add the factory ones, re-enter Setup Mode, build a combined list with cat db.esl factory-db.esl > db-combined.esl, sign it with the offline KEK, and enroll it |
| PIN rejected many times | The TPM’s anti-hammering lockout | Use the recovery key. The lockout expires after a while |
| Forgot the PIN | The PIN is only in your head | Recovery key, then re-enroll with a new PIN |
| Lost passphrase and recovery key | None | The data cannot be recovered. A header backup does not help without a valid secret |
| ESP full | Too many UKIs | df -h /efi, remove old kernels with apt autoremove |
| No Wi-Fi | The MT7922 needs MediaTek firmware | apt install firmware-linux, reboot, check dmesg | grep -i mt79 |
Check these early, because the plan depends on them
pcrextend or pcrphase units into the initramfs. Step 3 tests it. I did not install the stack myself.What this design does not cover
Sources. Pages marked “opened” were fetched and read. The rest I saw as search excerpts only, and the hardware and BIOS statements rest on them.