BIOS/UEFI Deep Dive: Boot Phases, Secure Boot, Measured Boot, and TPM
Most of us meet UEFI only when something has gone wrong. The machine won’t boot a new disk, Secure Boot is rejecting a freshly-compiled kernel module, or a Windows update has overwritten the EFI entries and Linux is invisible. You spelunk into the BIOS setup screen, poke around, fix the symptom, and close the book. UEFI is a 2000-page specification, a small operating system running before your operating system, and an open attack surface that modern defenders care about a great deal. Understanding it pays back whenever firmware is involved — bootloaders, hardware provisioning, measured boot, remote attestation, full-disk encryption that unlocks automatically, Secure Boot custom key enrollment, or merely debugging why grub stopped working.
This post walks the real architecture: what firmware does between power-on and OS handoff, how the boot phases (SEC, PEI, DXE, BDS) compose, what Secure Boot actually verifies and how the key hierarchy (PK, KEK, db, dbx) works, what the TPM records and why “measured boot” is a different thing from “secure boot,” how PCRs are used by systemd-cryptenroll and sbctl, and how to actually debug the pile with sbctl, mokutil, efibootmgr, tpm2-tools, and friends.
Legacy BIOS is finally dead
The Basic Input/Output System dates to 1981. 16-bit real mode, MBR partition tables, 440-byte boot code in the first sector, a set of interrupt-driven services the OS was supposed to use but Linux and Windows long ago abandoned. BIOS didn’t scale — it didn’t understand GPT disks larger than 2 TB, didn’t know about modern networking, couldn’t be extended, and had no security model.
UEFI (Unified Extensible Firmware Interface) replaced it. The spec lives at uefi.org. The reference implementation is EDK II — Intel’s open-source codebase that nearly every firmware vendor forks and customizes. Coreboot and its payload Tianocore are the “libre” branches. OEMs like AMI (American Megatrends), Insyde, and Phoenix ship customized EDK II. Your motherboard’s firmware is, in all likelihood, a heavily-modified EDK II build.
As of roughly 2020, buying legacy-BIOS-only hardware is essentially impossible, though many machines still offer a CSM (Compatibility Support Module) that emulates legacy BIOS on top of UEFI for old OSes. Intel announced CSM deprecation years ago and most boards are CSM-free now. When people say “BIOS” today they usually mean the firmware setup screen, not the 1980s interrupt table.
The boot phases
From power-on to your OS loader, UEFI walks through a defined sequence:
SEC — Security Phase
The earliest code. Runs from the CPU’s reset vector. Initializes just enough CPU state to proceed. Sets up a temporary execution environment (often using CPU cache as RAM, because DRAM isn’t initialized yet). Performs very early platform initialization. The “Security” name is historical; it’s mostly about establishing trust in subsequent code.
On Intel, SEC may include microcode updates loaded by the CPU itself before anything else runs. On modern Intel platforms, SEC is extensively integrated with Boot Guard — the CPU verifies that the firmware’s early code is signed by a key fused into the chip, providing a hardware root of trust.
PEI — Pre-EFI Initialization
Runs in a minimally-initialized environment — CPU is up, cache is available, DRAM is being initialized. PEI modules (PEIMs) handle:
- Memory controller configuration. Training DDR, SPD reads, ECC setup. This is the part that takes 10-20 seconds on server boards with lots of DIMMs — “memory training” is real work.
- Chipset init. PCH setup on Intel, FCH on AMD.
- CPU microcode updates.
- Early error logging via HOBs (Hand-Off Blocks) — structured blobs of data handed to the next phase.
PEI ends when DRAM is initialized and the firmware has enough working memory to hoist the rest of itself into RAM and run DXE from there.
DXE — Driver Execution Environment
The bulk of UEFI. A mini-kernel with its own memory management, event loop, and driver model. DXE loads drivers for:
- PCI enumeration and resource allocation
- USB, SATA, NVMe, network
- Filesystem drivers (FAT on the ESP, sometimes NTFS or ext4 read-only)
- Graphics (GOP — Graphics Output Protocol)
- Crypto, variable services, TCP/IP, HTTP
This is where most of the UEFI API that an OS loader (grub, systemd-boot, Windows Boot Manager) sees gets exposed. UEFI variables, the Boot Services table, the Runtime Services table — all set up during DXE.
BDS — Boot Device Selection
Reads the UEFI BootOrder variable, iterates the BootXXXX variables it points to, and tries each. Each BootXXXX variable names a device path and typically an executable on the EFI System Partition (ESP). The first one that loads successfully and returns control (or transitions to the OS) wins.
If a BootXXXX entry’s executable fails to load (file missing, Secure Boot rejection, bad hardware), BDS moves to the next. If everything fails, it drops to the UEFI shell or the setup screen.
OS handoff
The OS loader is loaded into memory, ExitBootServices() is called (which tears down most of UEFI’s DXE drivers — only Runtime Services survive), and control transfers to the kernel. From here, the OS owns everything except the Runtime Services (for variable access, time, reset) and any code still living in SMM (System Management Mode).
The EFI System Partition
UEFI needs a filesystem it can read from. That’s the ESP: a small FAT32 partition (usually 100-500 MB), flagged with GPT partition type c12a7328-f81f-11d2-ba4b-00a0c93ec93b. Mounted at /boot/efi on Linux (sometimes /efi).
Inside the ESP:
/boot/efi/
├── EFI/
│ ├── BOOT/
│ │ └── BOOTX64.EFI # fallback, used when BootOrder is empty or invalid
│ ├── ubuntu/
│ │ ├── shimx64.efi
│ │ ├── grubx64.efi
│ │ ├── mmx64.efi # MOK manager
│ │ └── grub.cfg
│ ├── Microsoft/
│ │ └── Boot/
│ │ └── bootmgfw.efi
│ └── systemd/
│ └── systemd-bootx64.efi
Each OS keeps its loader in its own subdirectory. /EFI/BOOT/BOOTX64.EFI is a conventional fallback — if the UEFI firmware can’t find any configured boot entry, it tries the fallback path. Many USB installers put their loader there so they boot out of the box on any UEFI machine.
The firmware stores boot entries in NVRAM as variables named Boot0000, Boot0001, etc., each pointing to a file path on some disk. efibootmgr on Linux manages them:
|
|
Create a new entry:
|
|
Reorder:
|
|
Delete:
|
|
The NVRAM entries are what actually drive the boot selection. grub-install and bootctl install create them; distros’ post-install scripts maintain them. If you’ve ever reinstalled Windows and lost your Linux boot entry, that’s because Windows replaced the BootOrder — the grub files are still on the ESP, you just need efibootmgr to put the entry back.
Secure Boot: cryptographic gatekeeper
Secure Boot is the mechanism by which UEFI refuses to run executables that aren’t signed by a trusted key. Its goal is to prevent persistent pre-OS malware (bootkits) from loading. Whether it achieves that is a long argument; what it definitely does is complicate the boot process.
The key hierarchy
Four types of variables live in secure NVRAM, protected by the firmware:
- PK (Platform Key) — exactly one. Authorizes changes to KEK. Normally owned by the OEM.
- KEK (Key Exchange Keys) — one or more certificates. Authorize changes to db and dbx.
- db (Signature Database) — the list of allowed signers. If a binary is signed by a key whose cert is in db (or whose hash is in db), it runs.
- dbx (Forbidden Signatures Database) — a revocation list. Binaries whose hash or signer matches dbx are rejected, even if they’d otherwise be allowed.
The chain of trust: PK authorizes changes to KEK → KEK authorizes changes to db/dbx → db validates loaded binaries.
At the factory, a typical OEM layout is:
- PK: owned by the OEM (Dell, HP, etc.)
- KEK: Microsoft’s KEK + the OEM’s KEK
- db: “Microsoft Windows Production CA 2011” + “Microsoft Corporation UEFI CA 2011” + some OEM certs
- dbx: Microsoft-curated list of revoked bootloaders
Microsoft’s “UEFI CA 2011” is the cert that signs the shim bootloaders used by Linux distributions. That’s how Ubuntu, Fedora, Debian, etc. boot on Secure Boot machines they don’t own the PK for — they get Microsoft to sign a tiny bootloader (shim) which then verifies the distro’s kernel using its own embedded certs.
The shim dance
Every Secure Boot-enabled Linux distribution has roughly this chain:
- Firmware verifies
shim.efiagainst db. Shim was signed by Microsoft’s UEFI CA, which is in db on almost every OEM machine. ✓ - Shim embeds the distro’s CA certificate. Shim loads GRUB (or systemd-boot, etc.), verifying its signature against the embedded distro cert. ✓
- GRUB loads the kernel, verifying its signature against the same distro cert (kernel has embedded signatures using the distro’s key). ✓
This works for stock distro kernels. It breaks when you want to run:
- A kernel you built yourself
- An out-of-tree module (NVIDIA proprietary, VirtualBox, VMware Workstation, ZFS)
- A bootloader you’ve modified
For those, you need to either:
- Add your own key to the firmware’s db (via
sbctl enroll-keysor manual PK/KEK/db flashing) - Use MOK (Machine Owner Key) — a shim-managed extension that lets you enroll a key without touching firmware keys. Shim loads binaries signed by MOK without requiring db entry.
- Disable Secure Boot — the “nuclear option” that many engineers reach for too early.
MOK — Machine Owner Key
The shim-in-the-middle design includes MOK. Secrets here:
mokutil— command-line tool to manage MOK.mokutil --import key.der— queues a key for enrollment. Requires a password.- Reboot: shim detects the pending import, prompts for the password via the MokManager UI, you confirm, key is stored in
MokListRTUEFI variable. - From then on, shim treats binaries signed by that key as trusted.
The NVIDIA driver install on a Secure Boot system walks you through this: it generates a keypair, signs the built module, asks you to enroll the public key via mokutil, and on next boot you type the password into MokManager. After that, modprobe loads the signed module without complaints.
sbctl: the friendly tool
sbctl from Foxboron is the modern Linux tool for Secure Boot management. It:
- Generates your own PK, KEK, and db keys
- Enrolls them into the firmware (replacing the OEM’s keys, or alongside)
- Signs your kernel, initramfs (via UKI — Unified Kernel Image), and bootloaders
- Verifies what’s currently signed/unsigned on your system
Typical workflow for “own my own keys”:
|
|
After enrollment, you own the platform. Only binaries you’ve signed (or were signed by keys you’ve enrolled, like Microsoft’s for OROM) will run. This is a stronger position than the default “Microsoft signed shim → distro signed kernel” chain because you control the entire trust anchor.
The --microsoft flag is important if you have OPROMs (GPU firmware, NIC firmware) that are Microsoft-signed and get executed during boot. Many GPUs won’t produce video output without their OPROM running, and the OPROM is signed by Microsoft’s UEFI CA. Drop Microsoft’s KEK and you break your GPU.
dbx updates and you
Every few months, Microsoft publishes a new dbx containing hashes of recently-discovered insecure bootloaders (BootHole, Black Lotus, etc.). fwupd on Linux can pull and apply these updates:
|
|
If you own your own keys, dbx updates still matter — they revoke known-bad binaries regardless of who signed them. Keep up with them.
Measured Boot and the TPM
Secure Boot is enforcement: it prevents untrusted code from running. Measured Boot is attestation: it records what ran, so you can decide later whether the boot was legitimate.
The Trusted Platform Module is a small discrete or firmware-emulated (fTPM on Intel, PSP fTPM on AMD) chip with:
- Asymmetric keys that never leave the chip
- A set of Platform Configuration Registers (PCRs), each 20 or 32 bytes, initialized to zero
- Crypto primitives: RSA, ECDSA, AES, HMAC
- Non-volatile storage (small, expensive — not a general-purpose secret store)
PCRs cannot be written, only extended. Extending a PCR:
PCR = hash(current_PCR || new_data)
This means the final PCR value represents a cryptographic history of everything measured into it. You can’t fake a PCR value without breaking the hash function.
What gets measured
Standard PCRs on a UEFI+TPM system:
| PCR | Measurement |
|---|---|
| 0 | Core system firmware executable code |
| 1 | Firmware configuration (settings like Secure Boot state, boot order) |
| 2 | Extended or pluggable firmware (OPROMs) |
| 3 | Firmware config for OPROMs |
| 4 | MBR / Boot loader code |
| 5 | GPT / Partition table |
| 6 | State transitions (resume events, etc.) |
| 7 | Secure Boot state — PK, KEK, db, dbx, and binaries’ signing keys |
| 8–9 | (Used by bootloaders — GRUB kernel cmdline, initrd, etc.) |
| 10 | Linux IMA (Integrity Measurement Architecture) |
| 11 | Used by systemd-boot for unified kernel image components |
| 14 | MOK variables |
After boot, read them:
|
|
On an unchanged system these values are stable across boots. Upgrade the kernel → PCR 4 changes. Change the boot order → PCR 1 changes. Update a firmware setting → PCR 0 or 1 changes.
Disk unlock tied to PCRs
The most common use: LUKS disk unlock that requires the TPM to prove the machine is in a “known-good” state. systemd-cryptenroll:
|
|
The PCR values are read and sealed into a TPM policy. On boot:
- Kernel + initrd request the TPM to unseal the LUKS keyslot.
- TPM compares current PCRs against the sealed policy.
- If they match, the TPM reveals the key, LUKS unlocks, boot proceeds without prompt.
- If they don’t match (firmware updated, bootloader tampered with, Secure Boot state changed), TPM refuses, LUKS falls back to password.
Which PCRs to pick is the engineering decision:
- PCR 7 — captures Secure Boot state and signing keys. If someone disables Secure Boot or enrolls a new key, PCR 7 changes. Essential for a “was this machine tampered with?” check.
- PCR 0 — firmware code. Updates when you flash firmware. Nice for catching firmware tampering, annoying because every firmware update breaks your unlock and you re-enroll.
- PCR 2 — OPROMs. Changes if someone adds or modifies a GPU / NIC firmware blob.
- PCR 4 — bootloader code. Updates with every GRUB upgrade.
- PCR 11 — UKI components. Best tied with the kernel/initrd itself in UKI-based boot.
- PCR 12 — kernel cmdline (when measured by UKI or systemd-stub). Protects against “booted the legit kernel with a malicious cmdline.”
A reasonable starting set for a workstation is 0+2+7+12. Laptops benefit from 7+12 at minimum — mostly catches evil-maid attacks.
Remote attestation
Beyond local unseal, PCR values can be signed by a key in the TPM (the Attestation Identity Key, AIK) and sent to a remote party for verification. This is how:
- Kubernetes node attestation (Keylime, SPIFFE) verifies that a node’s firmware and boot chain match an expected baseline before it can join.
- Corporate device compliance checks boot state before letting a laptop onto a network.
- Confidential computing services check that a VM launched in the expected configuration.
The tool chain is tpm2-tools (tpm2_quote, tpm2_verifyquote) plus higher-level frameworks like Keylime or fapi-based libraries. For most engineers, remote attestation becomes relevant only inside zero-trust infrastructure projects, but the primitive is available on any TPM 2.0 machine.
Unified Kernel Images (UKI)
A UKI is a single PE executable that bundles the kernel, initramfs, kernel command line, and splash image into one signable blob:
UKI = [.text section: kernel] + [.linux section: vmlinuz] +
[.initrd section: initramfs] + [.cmdline section: cmdline] +
[.osrel, .uname, .dtb sections: metadata]
The benefit for Secure Boot: the entire unit is one file, signed once. You can’t swap in a malicious initramfs without breaking the signature. You can’t alter the cmdline. PCR 11 measures the UKI content, so measured boot captures all of it together.
Build one with ukify:
|
|
systemd-boot auto-discovers UKIs in /boot/EFI/Linux/ and shows them as boot entries. This is the cleanest Secure Boot / measured boot path today; it replaces the “shim + grub + kernel + initrd” chain with a single signed artifact.
Debugging the stack
Nothing boots after I added a disk
Most often: firmware decided to re-probe boot devices and reset BootOrder. Rescue boot from USB, efibootmgr -v to see what’s there, add missing entries. Some firmware also refuses to keep an EFI entry pointing to a disk it can’t currently see — adding a disk in a slot the firmware doesn’t recognize can evict the entry.
“Secure Boot Violation”
The firmware refused to load a binary. Temporarily disable Secure Boot in the setup screen, boot, investigate:
sbctl verify— shows which installed binaries are signed or unsignedmokutil --list-enrolled— what keys shim trustsmokutil --sb-state— is Secure Boot actually enabled?sbverify --list /path/to/binary— what signatures does a binary have?
Re-sign missing binaries, re-enroll the relevant MOK / key, re-enable Secure Boot.
Linux boots but Windows is gone (or vice versa)
Look at BootOrder. efibootmgr -v from a Linux live USB, or from the Windows recovery shell bcdedit. Reorder or recreate the missing entry. If both OSes’ bootloaders are on the same ESP, both are still there — you just need an NVRAM entry pointing to each.
LUKS unlock with TPM suddenly prompts for password
Something that was measured changed. Look at tpm2_pcrread and compare to what you sealed against. Common culprits:
- Firmware update (PCR 0)
- Adding or removing hardware with OPROM (PCR 2)
- Installing a new version of shim / grub (PCR 4, PCR 7)
- Enabling/disabling a Secure Boot related setting (PCR 7)
systemd-cryptenroll --tpm2-device=auto --wipe-slot=tpm2 /dev/... followed by re-enrolling against the new PCR values is the recovery path, after you’ve typed the fallback password.
efivar: error creating variable
The firmware’s NVRAM write is being rejected. Often because NVRAM is full (especially on servers with lots of boot entries built up over time). efibootmgr -b XXXX -B to delete stale entries.
“Can’t boot into BIOS setup”
Modern firmware has a setup shortcut from inside the OS:
|
|
Reboots into the firmware setup screen without needing to mash the F2 key.
Coreboot and alternative firmware
Not every system runs vendor EDK II. Coreboot is a libre firmware replacing proprietary BIOS/UEFI with minimal, auditable code that hands off to a “payload” — typically Tianocore (EDK II in payload form), SeaBIOS (legacy BIOS emulation), or a Linux kernel directly. Used by:
- Chromebooks — coreboot with a depthcharge payload
- System76 Open Firmware, Star Labs, MNT Reform — Linux laptops with coreboot
- Dasharo — modern coreboot port targeting enterprise workstations
- 9elements Secureboot OSF — servers
If firmware supply-chain matters to you (no unaudited blobs between power-on and OS), coreboot is the serious answer. The downside: supported-hardware list is limited and firmware updates are DIY.
What I wish I’d known five years ago
- Always keep a live USB. When UEFI state gets weird, you need to boot something to fix it. A USB with a tools distro (Grml, SystemRescue) lives in my laptop bag.
- Back up NVRAM variables.
efivar-dumpormokutil --exportbefore major changes. Re-importing a known-good set can recover from a bricked boot config. - Don’t trust the firmware update UI over the command line.
fwupdmgron Linux handles most vendors’ firmware updates via LVFS (Linux Vendor Firmware Service) and logs what it does. OEM Windows utilities tend to combine firmware updates with unrelated bloatware. - Never disable Secure Boot permanently if you can avoid it. The bar for running your own signed kernel is low (sbctl) and leaving Secure Boot off gives up a real protection against pre-boot malware that doesn’t cost you anything once set up.
- Measured boot is more useful than people give it credit for. Even on a laptop, sealing your LUKS key to PCR 7 means a thief can’t boot a different kernel to dump your unlocked data — they’d have to break the TPM itself.
- UEFI shell is there when you need it. Most firmware ships an EFI Shell you can drop into (or you can
cp Shell.efito the ESP yourself). From there,map,bcfg, filesystem access, network boot — enough to recover from a mangled install. - TPM 2.0 vs 1.2 matters. 1.2 is SHA-1 only, dying, and useless for modern attestation. 2.0 is SHA-256/384, flexible, and the floor for anything you build today. Every CPU from roughly 2016 onward has fTPM 2.0 even without a discrete chip.
Closing
UEFI is the cost of having a PC that does more than 1981’s PC did. It’s not elegant — the spec is enormous, implementations vary wildly, and OEMs ship bugs that take years to fix. But the primitives are real: cryptographic boot verification, hardware-rooted trust, measured attestation, and enough structure that modern security tooling (disk encryption, remote attestation, key management) has something to build on. Ignoring the layer means every interaction with firmware turns into trial-and-error. Learning it means you can make firmware do what you want — enroll your own keys, automate bare-metal provisioning, tie disk unlock to machine identity, and debug the boot chain when things go sideways.
Everyone who owns a UEFI machine already owns the hardware for all of this. The only thing between you and using it is learning what the pieces are and what the tools do. You now have that.
Comments