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

8.0 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.

Manual enrollment on a fleet workstation (over SSH)

The build bakes the EFI management tools into the image (efitools + efivar/mokutil/efibootmgr, installed in the main package provisioner), so you can enrol keys manually over SSH on a booted workstation that is in Setup Mode — handy for provisioning before the automated flow (TODO #10) exists. The tools write to efivarfs (/sys/firmware/efi/efivars), not the read-only root, so they work at runtime; they're RPM-installed so fapolicyd already trusts them.

# on the workstation (as the sudo admin):
mokutil --sb-state                      # check Secure Boot / Setup Mode
efi-readvar                             # dump current PK/KEK/db/dbx
# copy your PK.auth/KEK.auth/db.auth (or .esl) onto the host, then in Setup Mode:
sudo efi-updatevar -f db.auth  db
sudo efi-updatevar -f KEK.auth KEK
sudo efi-updatevar -f PK.auth  PK       # last — leaves Setup Mode
efi-readvar                             # read back + confirm fingerprints match

Only the public certs/.esl/.auth need to 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).