Skip to content
Stribog

Bare Metal

All writing

Measured Boot on Metal You Own: TPM and Secure Boot Keys

Measured boot proves what actually ran: enrol your own UEFI Secure Boot keys, record the chain into TPM 2.0 PCRs, verify a quote, and name the trust left.

Stribog13 min read

Ask an infrastructure team whether Secure Boot is enabled and you usually get a yes. Ask what booted on node seventeen last Tuesday and the room goes quiet. Different questions, different mechanisms — and the gap between them is where boot integrity lives.

Secure Boot refuses, measured boot records

UEFI Secure Boot enforces. At each stage the firmware checks a signature against the allow list in db, having first checked the deny list in dbx, and refuses control to anything that fails; the Key Exchange Key authorises updates to those stores and the Platform Key authorises the KEK. The result is one boolean per stage, decided locally and discarded: nothing durable written, nothing signed, nothing leaving the machine. On stock keys, a successful boot shows only that the code carried someone else's authorised signature.

TPM measured boot does the opposite: it refuses nothing. Each stage hashes the next and extends that hash into a Platform Configuration Register before executing it — the register becomes the hash of its old value concatenated with the new measurement: one-way, order-dependent and, short of a reboot, unwindable. But a register is a digest, not a narrative. The record is the firmware event log, and the PCR exists to prove that log has not been edited.

PCR measurements most commonly serve two distinct purposes: to implement access policy on TPM sealed objects […] a forward-looking use […] To permit reasoning about the boot process and runtime so far, for example for the purpose of remote attestation […] a backward-looking use.
UAPI Group, Linux TPM PCR Registry

Hold on to that split: almost every practical mistake here is a forward-looking mechanism asked a backward-looking question. It is a different layer from confidential computing and TEE attestation, which protects data in use at runtime. Measured boot answers what ran before the kernel scheduled a process, and needs no special silicon.

Same code path, two outputs. The left rail decides; the right rail testifies. Neither, alone, tells a third party anything.

Taking the Platform Key: enrolling your own Secure Boot chain

Secure Boot on the factory key set is a policy written by the OEM and the authorities it chose — not a bad policy, simply not yours to amend or revoke from. Enrolling your own PK, KEK and db moves that decision in-house. The open-source path is sbctl: create-keys, enroll-keys and sign, with sbctl status reporting whether firmware is in Setup Mode.

bash
# Setup Mode first: the PK slot must be empty before enrolling.
sbctl status

# Your own PK, KEK and db. Back this up offline now — losing it is a
# fleet-wide availability incident.
sbctl create-keys

# Enrol. Choose ONE, on purpose. Some devices validate signed hardware
# firmware against the factory db; failing that check can brick them.
sbctl enroll-keys --microsoft    # keep the Microsoft certs alongside yours
# sbctl enroll-keys --tpm-eventlog  # event-log checksums (experimental)
# sbctl enroll-keys --yes-this-might-brick-my-machine  # ignores that error

# Sign everything firmware executes; --save lets sbctl re-check it.
# Verify the ESP BEFORE rebooting.
sbctl sign --save /efi/EFI/BOOT/BOOTX64.EFI
sbctl sign --save /efi/EFI/Linux/linux.efi
sbctl verify
Three enrolment paths, one of which can brick a server. Two stay commented on purpose.

That brick warning is sbctl's own, and it is not about an unsigned bootloader — it is about option ROMs on discrete GPUs, NICs and HBAs validating against the factory db. --microsoft owns the PK without taking that risk. The remaining cost is narrower: whatever the retained Microsoft certificates miss is yours to sign — the UKI and fallback loader above — while Microsoft-signed loaders and option ROMs keep verifying, and media trusted only by the OEM's factory db may not. Enrolment also changes PCR 7, so any secret already sealed against it becomes unreachable. Do this at provisioning, folded into bare-metal provisioning with Tinkerbell, Metal3 and Sidero Omni, not as a retrofit.

Which PCR holds what, before you select anything

PCRs are scarce: a standards-compliant TPM has twenty-four. Registers 0 through 7 are owned by platform firmware, and per the TCG PC Client platform firmware profile the OS may use 8 through 15. Copying a selection from a tutorial is how teams seal against a register that changes on every kernel update.

PCR 7 is the one to learn first. It changes when Secure Boot mode is toggled and when the PK, KEK, db or dbx certificates are updated; shim also measures most of its non-MOK certificates and its SBAT data there. So PCR 7 encodes the Secure Boot policy in force — exactly what you want to attest, and exactly what you do not want a disk key bound to if you ever rotate.

On the OS side, systemd measures to PCRs 5 (boot-loader-config), 11 (kernel-boot), 12 (kernel-config), 13 (sysexts) and 15 (system-identity). That is the named-purpose set, not every register systemd touches — the same page also covers PCR 1 for SMBIOS data and PCR 9 for NvPCR init.

Two details matter most. systemd-stub measures every PE section of a unified kernel image defined by the UAPI.5 specification into PCR 11, in canonical order, writing a paired record for the section name then the section data — PCR 11 is a transcript of the UKI, not one opaque hash. The kernel command line goes into PCR 12, where the measured hash covers the literal command line in UTF-16: add one debug argument and PCR 12 moves.

There is a gate underneath all of it: systemd's userspace measurements are by default only made if the system booted with systemd-stub. If the UEFI-mode measurements did not happen, the userspace ones do not either — on a conventional kernel-plus-initrd boot, stub-owned 11 and 13 stay empty, as does userspace-only 15, and 12 stays empty unless systemd-boot measured a command line. PCR 14 is the exception: shim owns it for MOK material, so any shim-backed node fills it.

bash
# Which banks exist? A legacy SHA-1 bank should not carry your policy.
tpm2_pcrread sha256

# securityfs is not always mounted.
mountpoint -q /sys/kernel/security || \
  mount -t securityfs securityfs /sys/kernel/security

# Some server firmware hands Linux no event log at all — without this
# file there is nothing to replay. Read it before trusting a parser.
ls -l /sys/kernel/security/tpm0/binary_bios_measurements
tpm2_eventlog /sys/kernel/security/tpm0/binary_bios_measurements | head -n 40
Confirm the machine is actually producing evidence before building anything on top of it.

One consequence of reading a real log: golden values do not copy across a fleet. PCR 1 can encode machine-specific data — the MAC address of the NIC a node netbooted from — so two identically provisioned servers disagree legitimately.

Sealing is not attesting

The most common TPM deployment in a self-hosted estate is a LUKS key sealed to PCR values so the root disk unlocks without a passphrase. A good control: a stolen drive is useless, and a node booting into something unexpected fails to unlock. It is also invisible for compliance: the TPM evaluated a policy and released a key to the machine it lives in. No signature, no nonce, no log off the box.

That is the forward-looking use: a policy requiring PCRs in a specific literal state, or any state for which a signature by a specific key pair can be provided. Attestation is the backward-looking use, needing a signed quote and a replayable log sent to somebody who is not the node. A team with TPM-unlocked disks that thinks it has boot attestation has confused a lock for a witness.

The signed-policy variant matters even if you never attest, because it is what makes sealing survive an update. Talos Linux seals the disk encryption key with the TPM under an unlock policy configured to trust the expected policy signed by a PCR signing key, rather than literal register values. Re-sign for the new UKI and nodes keep unlocking; bind to literal values and a firmware update strands the fleet.

yaml
# ... rest of machine config
---
apiVersion: v1alpha1
kind: VolumeConfig
name: STATE          # EPHEMERAL takes the same shape
encryption:
  provider: luks2
  keys:
    - slot: 0
      # options.pcrs is the EXTRA literal bind (unset = PCR 7, [] = none).
      # Signed PCR 11 policy always applies; not selected here.
      tpm:
        options:
          pcrs: []
# ... rest of machine config
Excerpt of a Talos machine config. The signed PCR 11 policy is not configured through this list.

Upstream, systemd-measure exists to pre-calculate and sign the expected TPM2 PCR 11 values for booted unified kernel images. Its manual page still warns the command is experimental and may change in behaviour and interface. The broader case for an immutable, signed node OS is the security case for Talos Linux.

Producing and checking a quote on real hardware

A quote is a TPM-signed structure over a digest of a chosen PCR selection plus caller-supplied qualifying data — where a verifier's nonce goes. tpm2_quote provides quote and signature for a given list of PCRs in given algorithms and banks, writing the message, the signature and the PCR values to separate files.

bash
# --- on the node ---------------------------------------------
# A template EK and the AK that signs quotes. Neither command reads
# or verifies an endorsement certificate.
tpm2_createek --ek-context ek.ctx --key-algorithm rsa --public ek.pub
tpm2_createak --ek-context ek.ctx --ak-context ak.ctx \
              --key-algorithm rsa --hash-algorithm sha256 \
              --signing-algorithm rsassa --public ak.tpm

# The verifier needs a PEM.
tpm2_readpublic --object-context ak.ctx --format pem --output akpub.pem

# Read the selection first: 0-7 firmware and Secure Boot state,
# 11-13 UKI, command line, sysexts. Keep it for the verifier.
tpm2_pcrread sha256:0,1,2,3,4,5,6,7,11,12,13 > pcrs.yaml

# --qualification takes HEX_STRING_OR_PATH; an empty nonce still
# yields a verifiable quote, so fail closed.
: "${VERIFIER_NONCE:?verifier-issued hex nonce required}"

tpm2_quote --key-context ak.ctx \
           --pcr-list sha256:0,1,2,3,4,5,6,7,11,12,13 \
           --qualification "$VERIFIER_NONCE" \
           --message quote.msg --signature quote.sig --pcr quote.pcrs

# Two logs, not one. binary_bios_measurements ends at ExitBootServices;
# systemd's userspace extensions are recorded separately.
cp /sys/kernel/security/tpm0/binary_bios_measurements eventlog.bin
BUNDLE="quote.msg quote.sig quote.pcrs akpub.pem pcrs.yaml eventlog.bin"

# systemd-stub only; a shim/grub node ships the firmware log alone.
if cp /run/log/systemd/tpm2-measure.log userspace.log 2>/dev/null; then
  BUNDLE="$BUNDLE userspace.log"
fi

tar czf attestation.tgz $BUNDLE

# --- on the verifier, off the box ------------------------------
tpm2_checkquote -u akpub.pem -m quote.msg -s quote.sig -f quote.pcrs \
                -g sha256 -q "$VERIFIER_NONCE"

# quote.pcrs is checkquote's binary input, not a digest list you can
# diff. pcrs.yaml is the node's readable copy — a convenience, not
# evidence. Replay the firmware log, compare its pcrs: block, and
# expect only firmware-closed registers to match from this log alone.
tpm2_eventlog eventlog.bin
The full loop. The nonce comes from the verifier; generating it on the node proves nothing about freshness.

Two logs, because there are two measurers. binary_bios_measurements is the firmware event log and it ends at ExitBootServices. Everything systemd extends after that lands in /run/log/systemd/tpm2-measure.log: the phase strings enter-initrd, leave-initrd, sysinit, ready, shutdown, final that systemd-pcrextend writes into PCR 11, and — where systemd-pcrosseparator.service runs — os-separator into PCRs 0-7, 9 and 12-14, sealing the firmware registers off from OS measurements. Replay the firmware log alone against a quote covering 11, 12 or 13 and the digests cannot reconcile. Not a broken TPM; a verifier reading half the evidence.

Label that last command accurately. tpm2_checkquote validates the quote signature against the public key you supply and, if provided, checks that the qualifying data and PCR values match those in the quote. It never touches an endorsement certificate, so it says nothing about whether a genuine TPM produced the quote.

Closing that hole is separate work: bind the AK to the EK with a credential-activation challenge, then chain the endorsement certificate to a manufacturer CA bundle you trust. Until then a verifier accepts quotes from anything holding the right private key, software included. Sign and version the reference values too — SBOM, Sigstore and admission control applies to a golden-PCR file as much as a container image.

Gating cluster admission on the attestation result

No stock Kubernetes mechanism gates admission on a boot attestation result. Kubelet TLS bootstrap proves possession of a token; nothing in the control plane asks a node what it booted. Whatever you build here is glue you write and maintain.

The dashed box is the scope of the claim, not a caveat. Attestation relocates vendor trust into four anchors you can name — it does not remove it.

The first approach is hardware-rooted node identity at enrolment. SPIRE's tpm_devid node attestor issues two challenges: proof-of-possession, verifying the node holds the private key matching its DevID certificate, and proof-of-residency, proving that key pair was generated in and resides in a TPM. The server additionally verifies the TPM is authentic by checking its endorsement certificate is rooted to a trusted set of manufacturer CAs.

hcl
# ... plugins block continues above
NodeAttestor "tpm_devid" {
    plugin_data {
        # The DevID chain you issued during provisioning.
        devid_ca_path       = "/opt/spire/conf/server/devid-ca.pem"

        # The TPM manufacturer CA bundle.
        endorsement_ca_path = "/opt/spire/conf/server/endorsement-ca.pem"
    }
}
# ... plugins block continues below
Excerpt of a SPIRE server config. The second path is where your trust chain leaves your organisation.

Be precise about what that buys: a workload identity anchored in specific, genuine silicon — the hardware-rooted floor under SPIFFE and SPIRE on Kubernetes. It does not replay a firmware event log or evaluate PCR values against a policy. A node with a valid DevID, compromised below the kernel, still attests successfully.

The second approach is a continuous verifier. Keylime — a CNCF-hosted project providing remote boot attestation and runtime integrity measurement rooted in hardware — does what SPIRE does not: with a measured-boot policy supplied, PCRs 0-9 and 11-14 are included in the agent's quote, and the agent ships /sys/kernel/security/tpm0/binary_bios_measurements for the verifier to replay.

Then the join. Keylime raises a revocation event; something has to turn that into a cordon and drain, without cordoning a whole rack when a firmware rollout legitimately moves PCR 0. That controller is yours to write, test and run.

Where the chain still trusts a vendor

An honest attestation story ends with an inventory of borrowed trust — four anchors, none removed by owning the metal.

  • The endorsement certificate. Proving a quote came from real hardware means checking it against a manufacturer CA — trust delegated to issuance practices you cannot inspect.
  • The platform firmware. It performs the first measurement, including of itself. A compromised core root of trust for measurement reports a clean chain, and everything downstream inherits it.
  • Signed option ROMs. GPU, NIC and HBA firmware validated against keys you did not issue, executing before your kernel and logged as measurements you cannot reproduce.
  • The TPM implementation. A discrete part running a vendor's firmware stack, with its own errata.

That last one is not theoretical. CERT/CC VU#782720 recorded two buffer overflow vulnerabilities in the TPM 2.0 reference library specification at Level 00, Revision 01.59: an attacker with access to a TPM-command interface can send crafted commands to trigger them, gaining read-only access to sensitive data or overwriting protected module data. Exposure is implementation-specific: on that same advisory Microchip analysed both issues, performed code review and device tests, and confirmed its devices unaffected. Inventory and vendor advisories, not fatalism.

June 2026 showed publicly what running on someone else's key material costs. Microsoft Corporation KEK CA 2011 — the KEK-stored certificate signing DB and DBX updates — expired on June 24, 2026, replaced by Microsoft Corporation KEK 2K CA 2023. Microsoft UEFI CA 2011, the DB certificate signing third-party loaders, expired on June 27, 2026, replaced by Microsoft UEFI CA 2023 for boot loaders and Microsoft Option ROM UEFI CA 2023 for option ROMs.

Read that structurally, not dramatically. Linux servers did not stop booting, and a machine running its own Platform Key is not exposed the same way. The lesson: key material in your db has an expiry date, a rotation plan and an owner — and if that owner is not you, neither is the schedule.

Exit ramps and the long game

Boot integrity fails in production for operational reasons far more often than adversarial ones. A BIOS update changes PCR 0; a kernel argument added during an incident changes PCR 12. Each is a legitimate change a literal-value seal treats as an attack, and the outage teaches the team to disable the control. Design against that: sign a policy rather than pinning values, keep a documented break-glass passphrase, and stage firmware rollouts so reference values regenerate before reboot.

The custody model has to survive staff turnover, which is harder than the cryptography. Three bundles accumulate — PK, KEK and db private material, the UKI signing key, the PCR signing key — each needing an offline home, a named owner and a rehearsed rotation.

Keep the road back open. sbctl enroll-keys --microsoft enrols your keys alongside the Microsoft certificates rather than instead of them, preserving vendor-signed media; most server firmware also offers a restore-factory-keys action. Test that path on one node before rolling enrolment across a rack — an exit ramp whose only implementation is a BMC session per machine is a rumour.

What lasts is not the enrolment. It is the process that produces, signs and versions reference values for every hardware model in the estate, and the verifier checking them on a schedule nobody has to remember. That artefact outlives firmware generations and the engineer who built it, turning a questionnaire assertion into something you can demonstrate on demand. Owning the hardware was never the point. Proving what it runs is.

§FAQ/Common questions

Frequently asked

What is the difference between Secure Boot and measured boot?

Secure Boot enforces, measured boot records. UEFI Secure Boot checks each stage's signature against the db allow list (and the dbx deny list) and refuses to execute anything that fails — a local boolean that leaves no durable evidence. TPM measured boot executes everything and extends a hash of each stage into a Platform Configuration Register before running it, producing a tamper-evident chain plus a firmware event log. You need both: Secure Boot to stop unauthorised code, measured boot to prove to a third party what actually ran.

Does a TPM-unlocked LUKS volume mean I have boot attestation?

No. Sealing and attesting are different uses of the same registers. The UAPI Group's Linux TPM PCR Registry separates them: implementing access policy on sealed objects is a forward-looking use requiring pre-calculable values, while reasoning about the boot process so far for remote attestation is a backward-looking use requiring a measurement log. An unseal is a decision the TPM makes for the machine it lives in — no signature, no nonce, no log leaves the box, and nobody else learns anything.

Which PCRs should I quote or seal against?

Choose from your own event log rather than copying a list. A standards-compliant TPM has twenty-four PCRs; 0-7 are owned by platform firmware and the OS may use 8-15. PCR 7 carries Secure Boot state and changes when PK, KEK, db or dbx are updated. systemd measures to PCRs 5, 11, 12, 13 and 15 as its named-purpose set — not an exhaustive list, since the same documentation also covers PCR 1 and PCR 9. Note that PCR 11 holds the UKI PE-section transcript and PCR 12 the UTF-16 kernel command line, so both move on ordinary updates.

Is tpm2_checkquote enough to verify a node?

Not on its own. It uses the public portion of the key you supply to validate the quote signature and, if provided, to verify the qualifying data and PCR values match those in the quote. It never touches an endorsement certificate, so it proves someone holds the matching private key — not that a genuine TPM produced the quote. Complete the chain by binding the attestation key to the endorsement key and checking that certificate against a manufacturer CA bundle, the way SPIRE's tpm_devid attestor does.

Can Kubernetes gate node admission on an attestation result?

Not out of the box — the join is glue you write. Two open-source anchors exist. SPIRE's tpm_devid node attestor issues proof-of-possession and proof-of-residency challenges and verifies the endorsement certificate chains to trusted manufacturer CAs, giving hardware-rooted identity at enrolment without replaying an event log. Keylime, a CNCF-hosted project, does continuous verification: with a measured-boot policy supplied it quotes PCRs 0-9 and 11-14 and ships binary_bios_measurements for replay. Check measured_boot_policy_name in verifier.conf first — the default is accept-all.

measured bootuefi secure boot custom keys servertpm 2.0 pcr measurement linuxhardware root of trust self hostedsecure boot bare metal kubernetes nodestpm quote verification linux server

Executive Briefing

Thirty minutes to clarify your infrastructure risk

Walk us through your vendor footprint and regulatory constraints. We will tell you honestly where sovereignty creates leverage — and where it does not. No pitch deck. No obligation.