Debian 13 on the GEEKOM X14 Pro: TPM-sealed Btrfs, Secure Boot, signed recovery

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.

What this protects against

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.

Your laptop: what is known and what to check first

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

  1. Update the BIOS and firmware from GEEKOM PC Manager. Note the version.
  2. Open the advanced BIOS (Esc+Ctrl+H from a full power-off) and look for a Secure Boot key-management page. You need one of: erase or delete the Platform Key, “Custom” mode, or “Reset to Setup Mode”. I found no source documenting this on the X14 Pro, so this is the one real go/no-go check.
  3. Find the TPM setting. If it says Intel PTT, the TPM is firmware-based and has no bus to sniff. In Windows, tpm.msc shows manufacturer INTC for PTT.
  4. Confirm the BIOS has an administrator or supervisor password option and a way to disable USB boot.
  5. Leave Windows alone otherwise. Its licence is tied to the firmware, so you can reinstall later from Microsoft’s ISO.

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.

Design

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

  1. The firmware, with Secure Boot on and only your db key enrolled, verifies systemd-boot.
  2. systemd-boot lists the UKIs it finds in /efi/EFI/Linux and on the RECOVERY partition. Its editor is off, so the command line cannot be changed.
  3. The firmware verifies the chosen UKI. systemd-stub measures the UKI’s sections into PCR 11. The kernel command line is inside the UKI, so it is signed and measured too.
  4. The dracut initramfs runs systemd, which asks the TPM to unseal the disk key. The TPM checks PCR 7 (Secure Boot state and keys) and a policy signed by your PCR key for PCR 11 in the enter-initrd phase. You type the PIN.
  5. LUKS opens, Btrfs mounts, and systemd extends PCR 11 with 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

Step 0: live USB, factory backup, Setup Mode

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.

Step 1: partition, encrypt, Btrfs, base system

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.

Step 2: configure the system inside the chroot

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

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.

Step 3: keys, UKI build script, bootloader, kernel

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.

Step 4: enroll your keys and boot for the first time

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.

Step 5: recovery key and TPM+PIN

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

Step 6: the recovery partition

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

Step 7: lock down the firmware

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.

Daily operation

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:

  1. If the machine boots and asks for the PIN, nothing is needed.
  2. If it asks for the passphrase or recovery key, boot, and re-enroll the TPM as in Step 5.
  3. If the firmware keys were reset and nothing boots, turn Secure Boot off, boot, re-enter Setup Mode in the BIOS, run the three 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.

Test plan

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.

When things go wrong

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

Residual risks, unverified items, sources

Check these early, because the plan depends on them

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.