diff --git a/.gitea/workflows/gitea-main-ci.yaml b/.gitea/workflows/gitea-main-ci.yaml index c2f972d..85f8632 100644 --- a/.gitea/workflows/gitea-main-ci.yaml +++ b/.gitea/workflows/gitea-main-ci.yaml @@ -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); diff --git a/.gitignore b/.gitignore index ae81594..18f209c 100644 --- a/.gitignore +++ b/.gitignore @@ -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 diff --git a/diskless.pkr.hcl b/diskless.pkr.hcl index d557996..6149d90 100644 --- a/diskless.pkr.hcl +++ b/diskless.pkr.hcl @@ -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", ] } diff --git a/docs/TODO.md b/docs/TODO.md index eb8eff4..471a1ba 100644 --- a/docs/TODO.md +++ b/docs/TODO.md @@ -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 diff --git a/docs/secure-boot-keys.md b/docs/secure-boot-keys.md index 1a739ea..c68595d 100644 --- a/docs/secure-boot-keys.md +++ b/docs/secure-boot-keys.md @@ -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 diff --git a/files/secureboot/fleet-sb-enroll b/files/secureboot/fleet-sb-enroll new file mode 100644 index 0000000..c8440af --- /dev/null +++ b/files/secureboot/fleet-sb-enroll @@ -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"