Files
diskless-workstation/docs/secure-boot-keys.md
T
2026-06-20 06:04:04 +00:00

9.2 KiB

Secure Boot key generation & enrollment — diskless fleet

The fleet boots a signed UKI directly via systemd-stub (no shim, no GRUB), so UEFI Secure Boot verifies the UKI against keys you enrol in firmware. This doc generates the three Secure Boot keys (the "trio": PK, KEK, db), enrols them, and wires the db key into the build.

Invariant #7: signing keys never enter the build VM or this repo. The commands below produce key material that lives offline / in an HSM. Only the db key signs UKIs routinely, and the build receives it as a path (sb_key/sb_cert) on the CI/HSM host — never committed, never echoed in logs. Treat every *.key file here as secret; never check one into git.

The trio — roles and where each lives

Key Role Used by Where it lives
PK (Platform Key) Root of trust; controls who may update the KEK. Setting it locks Secure Boot into User Mode. Firmware enrolment only Offline / HSM, used once (and for KEK rotation)
KEK (Key Exchange Key) Signs updates to db/dbx. Firmware enrolment only Offline / HSM
db (Signature Database) Signs the bootable UKI. The fleet build — sb_key/sb_cert (../scripts/build-oci.sh → ukify/sbsign) HSM/CI signing host

Trust chain at enrolment: PK signs KEK → KEK signs db → db signs the UKI.

Only db is online (it signs every release; the UKI cmdline is immutable once signed — invariant #6 — so re-signing is part of each build). PK and KEK are only needed to rotate keys later, so they stay offline.

Tooling

# Fedora / RHEL:   dnf install openssl efitools sbsigntools
# Debian / Ubuntu: apt install openssl efitools sbsigntool

1. Generate the three key/cert pairs

GUID=$(uuidgen); echo "$GUID" > GUID.txt    # owner GUID for the EFI variables

# Platform Key
openssl req -newkey rsa:2048 -nodes -keyout PK.key -new -x509 -sha256 -days 3650 \
  -subj "/CN=Fleet Platform Key/" -out PK.crt
# Key Exchange Key
openssl req -newkey rsa:2048 -nodes -keyout KEK.key -new -x509 -sha256 -days 3650 \
  -subj "/CN=Fleet KEK/" -out KEK.crt
# Signature DB key  <-- this pair becomes sb_key / sb_cert
openssl req -newkey rsa:2048 -nodes -keyout db.key -new -x509 -sha256 -days 3650 \
  -subj "/CN=Fleet db/" -out db.crt

rsa:2048 is the maximally firmware-compatible size for the enrolled variables; 4096 works on most modern UEFI but some implementations reject it for PK/KEK/db. The PEM db.key/db.crt are exactly what sbsign/ukify consume.

2. Convert to signed EFI variable updates (.auth)

for v in PK KEK db; do cert-to-efi-sig-list -g "$GUID" $v.crt $v.esl; done

sign-efi-sig-list -g "$GUID" -k PK.key  -c PK.crt  PK  PK.esl  PK.auth   # PK self-signs
sign-efi-sig-list -g "$GUID" -k PK.key  -c PK.crt  KEK KEK.esl KEK.auth  # PK signs KEK
sign-efi-sig-list -g "$GUID" -k KEK.key -c KEK.crt db  db.esl  db.auth   # KEK signs db

3. Enrol into firmware

Put the firmware in Setup Mode first (clear the existing keys in the UEFI setup screen). Then enrol db and KEK before PK — setting PK last is what flips the firmware out of Setup Mode and turns Secure Boot on:

efi-updatevar -f db.auth  db
efi-updatevar -f KEK.auth KEK
efi-updatevar -f PK.auth  PK      # do this LAST

Alternatively, enrol the .auth/.esl files via the firmware's own "enrol key from file" UI or KeyTool.efi — useful for headless fleet hardware where this is scripted into provisioning.

The fleet uses a UKI with no shim and no Microsoft keys, so no MOK is needed. If a host must also boot vendor-signed media, add your db alongside Microsoft's UEFI CA instead of clearing it.

On a fleet workstation: read state over SSH, enrol via firmware

efitools (efi-readvar/efi-updatevar/KeyTool) is NOT packaged for EL9 (confirmed on Rocky 9, 2026-06-20). The image therefore ships only efivar/mokutil/efibootmgr (base repos). That's enough to read Secure Boot state over SSH, but enrolment of custom PK/KEK/db is done in the firmware setup UI, not from the OS.

Read Secure Boot state over SSH (mokutil is the efi-readvar replacement; acts on efivarfs, works on the read-only root, RPM-installed so fapolicyd-trusted):

mokutil --sb-state     # enabled / setup mode
mokutil --pk           # Platform Key
mokutil --kek          # KEK
mokutil --db           # signature db — your enrolled fleet db cert appears here
mokutil --dbx          # revocations

Enrol — two paths:

(a) Baked helper over SSH (fleet-sb-enroll) — the OS-side, efitools-free path. The build bakes /usr/local/bin/fleet-sb-enroll plus the public PK/KEK/db.auth at /usr/share/fleet-secureboot/ (supplied via files/secureboot/keys/ or the SB_*_AUTH_B64 CI secrets). On a workstation in Setup Mode:

sudo fleet-sb-enroll --check     # show SB state + which keys are baked
sudo fleet-sb-enroll             # enrol db -> KEK -> PK (PK last; exits Setup Mode)
mokutil --db ; mokutil --sb-state   # verify

It writes the signed .auth updates to efivarfs directly (the EL9-native equivalent of efi-updatevar), refuses to run outside Setup Mode without --force, and is added to fapolicyd's trust DB at build (it's a non-RPM exec). ⚠ This is a deliberate trust-bootstrap action — run it only on a host you control, on a trusted/isolated network (see TODO #10).

(b) Firmware UI — no OS tooling, the most robust for one-off laptop bring-up:

  1. Put the public db.crt (and KEK.crt/PK.crt, or the .esl/.auth) on a FAT USB stick (or the ESP).
  2. Firmware setup → Secure Boot → Custom, Clear/Reset keys (Setup Mode), then Enrol / Load key from file for db, then KEK, then PK. On HP EliteBooks: F10 → Security → Secure Boot Configuration.
  3. Save & exit; re-check over SSH with mokutil --db.

Only the public certs/.esl/.auth reach the workstation — never copy PK.key/KEK.key/db.key onto a host.

4. Wire db into the build

packer build -var image_version=X.Y.Z \
  -var sb_key=/path/to/db.key \
  -var sb_cert=/path/to/db.crt \
  -var oci_image=<registry>/fleet-boot .

ukify signs fleet-<ver>.efi with db; the enrolled db cert lets firmware verify it. Without these vars the build stays unsigned (lab-only, with the loud warning). In CI, sb_key/sb_cert are secret paths on the HSM/signing host.

Signing in CI (Gitea Actions)

sb_key/sb_cert can be Gitea repo secrets, with one wrinkle: ukify consumes them as file paths, not values, and the signing post-processor runs shell-local on the runner (not in the build VM — consistent with invariant #7). So the job materializes the secrets to runner-temp files, passes the paths, and shreds the key afterward. This is wired in ../.gitea/workflows/gitea-main-ci.yaml (steps: Materialize Secure Boot signing material → packer build → Shred Secure Boot signing key). Set repo secrets SB_KEY (the db private key, PEM) and SB_CERT (the db cert, PEM); leave them unset for an unsigned lab build.

Caveats:

  • Only db belongs in CI. PK and KEK never leave the offline/HSM store.
  • SB_CERT is a public cert (not secret); kept as a secret only for convenience.
  • A CI secret puts the private key in plaintext on the runner FS for the duration of the build — only as safe as the runner. Use a trusted, ephemeral/self-hosted runner, ensure secrets are not exposed to fork/PR builds, and rely on RUNNER_TEMP wipe + the shred step. Gitea masks secret values in logs, and build-oci.sh passes the path (not content) to ukify, so the key won't leak into logs.
  • Stronger (production target): keep db.key in an HSM/PKCS#11 token or KMS and sign through it, so the raw key never touches the runner disk. The CI-secret flow is a reasonable interim; the HSM flow is the invariant-#7 ideal (TODO #8).

Key hygiene & lifecycle

  • PK.key and KEK.key go offline / into an HSM and stay there. They are only needed to rotate keys; routine signing uses db.key only.
  • Production ideal: keep db.key in an HSM/PKCS#11 token and have sbsign/ukify sign through it rather than reading a file from disk.
  • Validity: -days 3650 is 10 years; pick a rotation policy. Rotating db = sign new db.esl with KEK, push the db variable update, re-sign UKIs.
  • dbx (the revocation list) is a fourth, optional variable — manage it to blacklist a compromised db cert.
  • Lab shortcut: sbctl (sbctl create-keys → sbctl enroll-keys) generates and enrols the whole trio and tracks signed files — convenient for a dev box, but it keeps keys as local files, so for the fleet's HSM/offline-PK model use the manual flow above for PK/KEK.
  • Invariants #6 (UKI cmdline immutable once signed), #7 (keys never in repo/VM), #5 (roothash↔UKI hard gate) — ../CLAUDE.md.
  • Signing is the production-posture step in TODO.md #8.
  • FIPS is currently off and is to be revisited alongside signing/Secure Boot (stig-deviations.md #5).