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*.keyfile 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
dbalongside 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 onlyefivar/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:
- Put the public
db.crt(andKEK.crt/PK.crt, or the.esl/.auth) on a FAT USB stick (or the ESP). - 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. - 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
dbbelongs in CI. PK and KEK never leave the offline/HSM store. SB_CERTis 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_TEMPwipe + the shred step. Gitea masks secret values in logs, andbuild-oci.shpasses the path (not content) to ukify, so the key won't leak into logs. - Stronger (production target): keep
db.keyin 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.keyin an HSM/PKCS#11 token and have sbsign/ukify sign through it rather than reading a file from disk. - Validity:
-days 3650is 10 years; pick a rotation policy. Rotatingdb= sign newdb.eslwith KEK, push thedbvariable update, re-sign UKIs. - dbx (the revocation list) is a fourth, optional variable — manage it to
blacklist a compromised
dbcert. - 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.
Related
- 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).