Add secure boot helper

This commit is contained in:
Audiobuzz
2026-06-20 06:04:04 +00:00
parent 417f36d1b7
commit e3978bb210
6 changed files with 216 additions and 38 deletions
+20
View File
@@ -203,6 +203,26 @@ jobs:
echo "SB_CERT_FILE=$d/db.crt" >> "$GITHUB_ENV"
echo "Secure Boot signing material staged (signed UKI build)."
-
name: Materialize Secure Boot enrollment certs
# PUBLIC PK/KEK/db .auth files baked into the image for fleet-sb-enroll.
# They're binary, so stored base64 in repo secrets; decode into
# files/secureboot/keys/ (gitignored) before the build picks them up. If
# the secrets are unset this no-ops and the helper ships without keys
# (reports "no baked keys" until supplied). NOT signing keys — see
# docs/secure-boot-keys.md; PK/KEK private keys never come near CI.
if: ${{ secrets.SB_DB_AUTH_B64 != '' }}
env:
SB_PK_AUTH_B64: ${{ secrets.SB_PK_AUTH_B64 }}
SB_KEK_AUTH_B64: ${{ secrets.SB_KEK_AUTH_B64 }}
SB_DB_AUTH_B64: ${{ secrets.SB_DB_AUTH_B64 }}
run: |
d=files/secureboot/keys; mkdir -p "$d"
printf '%s' "$SB_PK_AUTH_B64" | base64 -d > "$d/PK.auth"
printf '%s' "$SB_KEK_AUTH_B64" | base64 -d > "$d/KEK.auth"
printf '%s' "$SB_DB_AUTH_B64" | base64 -d > "$d/db.auth"
echo "Staged PK/KEK/db .auth for in-image enrollment:"; ls -l "$d"/*.auth
-
name: packer build (UKI + OCI release image)
# Signed when SB_KEY/SB_CERT secrets are set (paths exported above);
+7
View File
@@ -2,3 +2,10 @@ reports
keys
# Throwaway USBGuard mass-storage test image (see the USBGuard verify steps)
/usbguard-test.img
# Secure Boot enrollment material baked into the image. The *.auth are PUBLIC but
# environment-specific, so keep the repo neutral: supply via CI secrets
# (SB_*_AUTH_B64) or `git add -f` if you choose to commit them. Keep README/.gitkeep.
/files/secureboot/keys/*.auth
/files/secureboot/keys/*.esl
/files/secureboot/keys/*.crt
+38 -17
View File
@@ -65,8 +65,8 @@ variable "artifact_dir" {
variable "nic_drivers" {
type = string
description = "Kernel modules for the fleet's real NICs (build VM only has virtio!)"
default = "igb igc ixgbe i40e ice mlx5_core"
description = "Kernel modules for the fleet's real NICs (build VM only has virtio!). e1000e covers Intel I21x onboard NICs (e.g. EliteBook 820 G4 = I219-LM) for laptop/desktop bench hosts; igb/igc/ixgbe/i40e/ice/mlx5_core cover server NICs. Firmware uses its own driver to PXE/HTTP boot, but the initramfs needs the matching module here to bring the link up in-OS for 802.1X/DHCP/NVMe."
default = "e1000e igb igc ixgbe i40e ice mlx5_core"
}
variable "build_password" {
@@ -240,6 +240,16 @@ build {
destination = "/tmp/"
}
# Secure Boot enrollment helper + PUBLIC PK/KEK/db .auth files. Staged to
# /tmp/secureboot; installed in the package step. The helper (fleet-sb-enroll)
# enrolls the keys into firmware via efivarfs over SSH (EL9 has no efitools).
# keys/ may be empty (the *.auth are gitignored / CI-materialized); the helper
# then reports "no baked keys" until they're supplied. See docs/secure-boot-keys.md.
provisioner "file" {
source = "files/secureboot"
destination = "/tmp/"
}
# --- 2. Packages, configs, diskless adaptations --------------------------
provisioner "shell" {
inline = [
@@ -256,19 +266,30 @@ build {
# dracut warns and silently falls back to gzip (a larger initramfs).
"dnf -y install wpa_supplicant tpm2-tools tpm2-tss nvme-cli cryptsetup dracut-network NetworkManager zstd openssl",
# EFI / Secure Boot management tools, for MANUAL key enrollment over SSH on
# a workstation in Setup Mode: efitools (efi-updatevar, efi-readvar,
# cert-to-efi-sig-list, sign-efi-sig-list, KeyTool) + efivar/mokutil/
# efibootmgr companions. See docs/secure-boot-keys.md ("Manual enrollment");
# auto-enroll is TODO #10. They write to efivarfs
# (/sys/firmware/efi/efivars), NOT the read-only root, so they work at
# runtime; RPM-installed => already fapolicyd-trusted (no STIG trust edit).
# efitools is EPEL-only on el9: enable EPEL just to pull it, then drop the
# repo so a non-kiosk image's repo set is unchanged (kiosk builds re-add it).
"dnf -y install efivar mokutil efibootmgr",
"rpm -q efitools >/dev/null 2>&1 || { dnf -y install epel-release && dnf -y install efitools && EFITOOLS_EPEL=1; } || true",
"rpm -q efitools >/dev/null 2>&1 && echo '==> EFI Secure Boot tools installed (efitools + efivar/mokutil/efibootmgr)' || echo 'WARNING: efitools not installed (EPEL unavailable?) — manual SB enrollment tooling missing' >&2",
"[ \"$${EFITOOLS_EPEL:-0}\" = 1 ] && dnf -y remove epel-release >/dev/null 2>&1 || true",
# EFI / Secure Boot management tools (efivar, mokutil, efibootmgr — all base
# repos). Used over SSH on a workstation: `mokutil --sb-state/--pk/--kek/--db`
# reads Secure Boot state + the enrolled PK/KEK/db; efibootmgr manages boot
# entries. They act on efivarfs (/sys/firmware/efi/efivars), NOT the
# read-only root, so they work at runtime; RPM-installed => fapolicyd-trusted.
# NOTE: efitools (efi-updatevar/efi-readvar/KeyTool) is NOT packaged for EL9
# (confirmed 2026-06-20), so OS-side *enrollment* of custom PK/KEK/db isn't
# available on the image — enroll via the firmware setup UI (load-from-file)
# instead. See docs/secure-boot-keys.md; the auto-enroll path (TODO #10)
# can't rely on KeyTool here and would need it built/sourced separately.
# e2fsprogs gives `chattr`, which fleet-sb-enroll uses to clear the immutable
# flag on existing EFI vars before overwriting them via efivarfs.
"dnf -y install efivar mokutil efibootmgr e2fsprogs",
# Secure Boot enrollment helper + PUBLIC PK/KEK/db .auth files. The helper
# (fleet-sb-enroll) enrolls the keys into firmware via efivarfs over SSH on a
# Setup-Mode workstation (EL9-native, no efitools). Non-RPM exec => add to
# fapolicyd trust in 2c-STIG-postfix (like kiosk-session). keys/ may have no
# .auth (gitignored / CI-materialized) — then the helper reports no baked keys.
"install -D -m0755 /tmp/secureboot/fleet-sb-enroll /usr/local/bin/fleet-sb-enroll",
"install -d -m0755 /usr/share/fleet-secureboot",
"for f in /tmp/secureboot/keys/*.auth; do [ -e \"$f\" ] && install -m0644 \"$f\" /usr/share/fleet-secureboot/ || echo 'NOTE: no Secure Boot .auth baked (supply via files/secureboot/keys/ or CI secrets)'; done",
"restorecon -R /usr/local/bin/fleet-sb-enroll /usr/share/fleet-secureboot 2>/dev/null || true",
"rm -rf /tmp/secureboot",
# Plymouth boot splash. The custom scripted 'fleet' theme renders the
# security-handshake checklist driven by the dracut hooks
@@ -670,9 +691,9 @@ build {
# custom kiosk files: add them to fapolicyd's trust DB and rebuild it. weston
# /chromium are RPM-installed so already trusted. (See docs/stig-deviations.md.)
"if command -v fapolicyd-cli >/dev/null 2>&1; then",
" for f in /usr/local/bin/kiosk-session; do { [ -f \"$f\" ] && fapolicyd-cli --file add \"$f\"; } || true; done",
" for f in /usr/local/bin/kiosk-session /usr/local/bin/fleet-sb-enroll; do { [ -f \"$f\" ] && fapolicyd-cli --file add \"$f\"; } || true; done",
" fapolicyd-cli --update 2>/dev/null || true",
" echo '==> fapolicyd: trusted custom kiosk binaries + rebuilt trust db'",
" echo '==> fapolicyd: trusted custom binaries (kiosk-session, fleet-sb-enroll) + rebuilt trust db'",
"fi",
]
}
+15 -4
View File
@@ -693,10 +693,21 @@ full ~17-min Packer build.
## [ ] 10. (OPTIONAL) Secure Boot cert auto-enrollment for provisioning
Speed up bringing up new workstations: bake the **public** PK/KEK/db certs into an
image, detect Secure Boot **Setup Mode** at boot, and auto-enroll them with
efitools (`efi-updatevar`/`KeyTool`) so a fresh/reset box adopts the fleet trust
root without a manual firmware-setup visit. Pairs with #8 (signing) +
[secure-boot-keys.md](secure-boot-keys.md).
image, detect Secure Boot **Setup Mode** at boot, and auto-enroll them so a
fresh/reset box adopts the fleet trust root without a manual firmware-setup visit.
Pairs with #8 (signing) + [secure-boot-keys.md](secure-boot-keys.md).
**Tooling gap (found 2026-06-20): `efitools` is NOT packaged for EL9** — no
`efi-updatevar`/`efi-readvar`/`KeyTool`. Resolved for the OS-side path by writing
the signed `.auth` variables to **efivarfs directly** (de-immutable + single
write), which needs no extra package. **Done 2026-06-20:** baked
`/usr/local/bin/fleet-sb-enroll` (efivarfs method, Setup-Mode-gated, fapolicyd-
trusted) + the public `PK/KEK/db.auth` at `/usr/share/fleet-secureboot/` (supplied
via `files/secureboot/keys/` or `SB_*_AUTH_B64` CI secrets); `mokutil` covers
reading. So **manual** OS-side enrollment over SSH now works (`sudo
fleet-sb-enroll`). What's left for *auto*-enroll: drive `fleet-sb-enroll`
unattended on a **Setup-Mode detection** at boot, gated to a provisioning mode/UKI
(below), with read-back verify + audit — NOT armed in the production image.
**What's safe:** the certs are public (no key material — only the **db** private
key signs, and it stays in CI/HSM, invariant #7); Setup Mode accepts
+42 -17
View File
@@ -82,28 +82,53 @@ where this is scripted into provisioning.
> 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)
### On a fleet workstation: read state over SSH, enrol via firmware
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.
> **`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):
```bash
# 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
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
```
Only the **public** certs/`.esl`/`.auth` need to reach the workstation — never
copy `PK.key`/`KEK.key`/`db.key` onto a host.
**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**:
```bash
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
+94
View File
@@ -0,0 +1,94 @@
#!/usr/bin/env bash
# fleet-sb-enroll — enrol the fleet's custom Secure Boot keys (PK/KEK/db) from the
# baked PUBLIC .auth files into firmware, via efivarfs.
#
# EL9 ships no efitools (no efi-updatevar), so this is the OS-side equivalent: it
# prepends the EFI authenticated-variable attribute mask to each signed .auth
# payload and writes it to /sys/firmware/efi/efivars in a single write(), in the
# order db -> KEK -> PK (PK last leaves Setup Mode and turns Secure Boot on).
#
# Run as root over SSH on a workstation that is in Secure Boot SETUP MODE. Only
# PUBLIC material is used (the .auth files carry certs + signatures, never the
# PK/KEK/db private keys, which stay offline/HSM — see docs/secure-boot-keys.md).
#
# Usage:
# fleet-sb-enroll --check # print SB state + which keys are baked; do nothing
# fleet-sb-enroll # enrol db, KEK, PK (PK last) — requires Setup Mode
# fleet-sb-enroll --force # attempt even if not in Setup Mode (User-Mode
# # writes only succeed if each .auth is signed by
# # the in-place parent key)
#
# NOTE: this is a deliberate trust-bootstrap action. In Setup Mode the firmware
# accepts these writes unauthenticated, so only run it on a host you control, on a
# trusted/isolated network. See docs/TODO.md #10 for the auto-enrol risk model.
set -euo pipefail
KEYDIR=/usr/share/fleet-secureboot
EFIVARS=/sys/firmware/efi/efivars
GUID_GLOBAL=8be4df61-93ca-11d2-aa0d-00e098032b8c # PK, KEK, SetupMode
GUID_DB=d719b2cb-3d3a-4596-a3bc-dad00e67656f # db, dbx
# EFI var attributes: NON_VOLATILE|BOOTSERVICE_ACCESS|RUNTIME_ACCESS|
# TIME_BASED_AUTHENTICATED_WRITE_ACCESS = 0x27, little-endian 4 bytes.
ATTR=$'\x27\x00\x00\x00'
die() { echo "fleet-sb-enroll: $*" >&2; exit 1; }
[ "$(id -u)" = 0 ] || die "must run as root (use sudo)"
[ -d "$EFIVARS" ] || die "efivarfs not present ($EFIVARS) — not a UEFI system / not booted via UEFI?"
setup_mode() { # echoes yes|no|unknown
local f="$EFIVARS/SetupMode-$GUID_GLOBAL"
[ -e "$f" ] || { echo unknown; return; }
# 4 attribute bytes then 1 data byte: 1 = Setup Mode, 0 = User Mode.
if [ "$(od -An -tu1 -j4 -N1 "$f" | tr -d ' ')" = 1 ]; then echo yes; else echo no; fi
}
show_state() {
echo "Secure Boot: $(mokutil --sb-state 2>/dev/null | tr '\n' ' ' || echo '?')"
echo "Setup Mode: $(setup_mode)"
echo "Baked keys ($KEYDIR):"
local k
for k in db KEK PK; do
if [ -f "$KEYDIR/$k.auth" ]; then echo " $k.auth present"; else echo " $k.auth MISSING"; fi
done
}
enrol_one() { # $1=var name $2=guid $3=.auth path
local f="$EFIVARS/$1-$2" tmp
[ -f "$3" ] || die "missing $3"
tmp=$(mktemp)
printf '%s' "$ATTR" > "$tmp"
cat "$3" >> "$tmp"
# Existing EFI vars carry the immutable flag; clear it before overwriting.
[ -e "$f" ] && chattr -i "$f" 2>/dev/null || true
# efivarfs requires the whole variable in ONE write() — dd, single block.
dd if="$tmp" of="$f" bs="$(stat -c%s "$tmp")" count=1 status=none \
|| { rm -f "$tmp"; die "write to '$1' failed (in Setup Mode? .auth signed by the right parent key?)"; }
rm -f "$tmp"
echo " enrolled $1"
}
FORCE=0
case "${1:-}" in
--check) show_state; exit 0 ;;
--force) FORCE=1 ;;
"") FORCE=0 ;;
*) die "usage: fleet-sb-enroll [--check|--force]" ;;
esac
# Require all three baked keys.
for k in db KEK PK; do
[ -f "$KEYDIR/$k.auth" ] || die "no baked keys at $KEYDIR/$k.auth — supply public PK/KEK/db .auth via the build (files/secureboot/keys/)"
done
if [ "$(setup_mode)" != yes ] && [ "$FORCE" != 1 ]; then
show_state
die "not in Setup Mode — clear/reset Secure Boot keys in firmware first, or pass --force"
fi
echo "Enrolling fleet Secure Boot keys (order: db -> KEK -> PK)..."
enrol_one db "$GUID_DB" "$KEYDIR/db.auth"
enrol_one KEK "$GUID_GLOBAL" "$KEYDIR/KEK.auth"
enrol_one PK "$GUID_GLOBAL" "$KEYDIR/PK.auth" # PK last -> exits Setup Mode
echo "Done."
echo "Verify with: mokutil --db and mokutil --sb-state"