Initial Secure boot setup
This commit is contained in:
@@ -624,6 +624,8 @@ smoke test.
|
||||
by the serving dnsmasq — drop the hardcoded `192.168.50.10`.
|
||||
- **Signing:** wire `sb_key`/`sb_cert` (CI/HSM secrets) for a signed UKI; enable
|
||||
Secure Boot on targets. Until then the loud unsigned warning must stay.
|
||||
**Key generation + enrolment procedure: [secure-boot-keys.md](secure-boot-keys.md)**
|
||||
(the PK/KEK/db trio; `db` = `sb_key`/`sb_cert`, PK/KEK stay offline).
|
||||
- **Bench-verify on the secure path** (swtpm + hostapd + FreeRADIUS,
|
||||
`disable_dot1x=false`) — the items deferred this session:
|
||||
- NM port secure path: `en*` UNMANAGED + `macsec0` MANAGED/renewing, MKA
|
||||
@@ -687,3 +689,58 @@ Tips the balance toward "do it" only if: we start managing the **serving hosts**
|
||||
roles to amortise — currently they're docker-compose), provisioning grows much
|
||||
more branchy, or we want molecule-tested hardening roles validated without a
|
||||
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).
|
||||
|
||||
**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
|
||||
*unauthenticated* writes to PK/KEK/db, so public certs (ESL) are sufficient. It's
|
||||
fleet-generic (no per-host state, invariant #1) and deterministic (reproducible
|
||||
verity hash).
|
||||
|
||||
**The real risk — provisioning-time TOFU (trust-on-first-use):** in Setup Mode
|
||||
Secure Boot isn't enforcing, so the enroller runs as *unverified* code, and the
|
||||
fleet's UEFI **HTTP boot is unauthenticated** in that state. An on-path attacker
|
||||
during provisioning can serve a malicious image that enrolls **their** PK/KEK/db —
|
||||
the box then "has Secure Boot" but trusts the attacker. Auto-enroll is only as
|
||||
trustworthy as the provisioning channel, and it removes the physical-presence
|
||||
approval gate. So treat it as a controlled-environment provisioning step, not an
|
||||
always-on behaviour.
|
||||
|
||||
**Optional separate provisioning UKI (preferred shape):** build a dedicated
|
||||
**provisioning UKI** that carries the certs + the enroll logic, instead of arming
|
||||
the enroller in the production image. Keeps the standing footgun out of the normal
|
||||
image (a factory reset + tampered image can't silently re-root trust), and lets
|
||||
the enroller be a one-shot, attended/isolated step. The production UKI never
|
||||
enrolls.
|
||||
- Build it as a separate Packer target / flag (e.g. `provisioning=true`) →
|
||||
its own small UKI; signed or unsigned (it runs in Setup Mode anyway, but sign
|
||||
it so it *also* boots once keys are enrolled / for re-runs).
|
||||
- Or a tiny standalone EFI tool (KeyTool-style) served alongside, not a full OS.
|
||||
|
||||
**Mitigations (do these regardless of UKI vs separate UKI):**
|
||||
- Run provisioning on a **trusted, isolated VLAN** (or a physically controlled
|
||||
bench) — the integrity Setup Mode can't give must come from the environment.
|
||||
- **Gate** enrollment behind an explicit provisioning mode / one-shot token;
|
||||
never arm it on every boot of the production image.
|
||||
- **Read-back verify before declaring success:** `efi-readvar` the enrolled
|
||||
PK/KEK/db and compare fingerprints to the values baked in; mismatch = fail
|
||||
loudly (turns silent TOFU into tamper-evidence).
|
||||
- **Audit** the enrollment event + read-back fingerprints to the collector.
|
||||
- **Keep the Microsoft UEFI CA in `db`** (or enroll your db alongside it) unless
|
||||
every NIC/GPU option ROM is covered by your keys — clearing MS db can break
|
||||
signed **option ROMs** (NIC PXE/HTTP-boot, GPU init), bricking the boot path.
|
||||
- **PCR 7 check:** enrollment changes the Secure Boot measurement in PCR 7;
|
||||
confirm nothing the fleet seals to the TPM (the EAP-TLS key at `0x81010001`,
|
||||
NV cert) is PCR-7-policy-bound before relying on this.
|
||||
|
||||
**Decision before building:** auto-enroll trades assurance for convenience and is
|
||||
reasonable for a controlled fleet; the highest-assurance path is attended/firmware
|
||||
or manufacturer pre-provisioning. Default recommendation: **separate provisioning
|
||||
UKI + isolated VLAN + read-back verify.**
|
||||
|
||||
@@ -0,0 +1,170 @@
|
||||
# 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](../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
|
||||
|
||||
```bash
|
||||
# Fedora / RHEL: dnf install openssl efitools sbsigntools
|
||||
# Debian / Ubuntu: apt install openssl efitools sbsigntool
|
||||
```
|
||||
|
||||
## 1. Generate the three key/cert pairs
|
||||
|
||||
```bash
|
||||
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`)
|
||||
|
||||
```bash
|
||||
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:
|
||||
|
||||
```bash
|
||||
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.
|
||||
|
||||
```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
|
||||
```
|
||||
|
||||
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
|
||||
|
||||
```bash
|
||||
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](../.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`](https://github.com/Foxboron/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](../CLAUDE.md).
|
||||
- Signing is the production-posture step in [TODO.md](TODO.md) #8.
|
||||
- FIPS is currently off and is to be revisited alongside signing/Secure Boot
|
||||
([stig-deviations.md](stig-deviations.md) #5).
|
||||
Reference in New Issue
Block a user