v0.7.0: Secure Boot chain, boot rules + decision webhook, tokenized answer files
Three features, all zero-toggle and principle-clean (single static musl
binary, container-first, no test certs, no client trust-store changes).
Secure Boot via signed shim+GRUB (automatic):
- The v0.6.1 escalation ladder gains a third rung: Firmware -> Builtin
-> Shim. Secure-Boot firmware downloads our unsigned iPXE but refuses
to execute it — indistinguishable from a failed chainload — so after
two unconfirmed attempts the MAC is offered Fedora's Microsoft-signed
shimx64.efi, which loads the signed GRUB, which fetches a
server-rendered grub.cfg. Fully signed chain, SB stays on.
- scripts/fetch-shim.sh pulls shim-x64/grub2-efi-x64 (+aa64 best-effort)
from the official Fedora 43 packages and ships the EFI binaries
byte-for-byte unmodified; Dockerfile fetch stage gained rpm2cpio/cpio.
- New grub_script renderer (Linux kernel entries only — signed GRUB only
boots signed kernels; sanboot/wimboot have no signed equivalent and
are omitted with an explanatory menu line).
- TFTP server gains a DynamicAsset hook for server-rendered names
(grub.cfg); HTTP serves the same config under /ipxe/grub.cfg for
native UEFI HTTP Boot chains. Arch-aware fallback walks back down the
ladder where no shim exists (BIOS, IA32).
Boot rules + decision webhook (open 'Matrix Boot'):
- Ordered first-match-wins rules over MAC prefix + client arch (the DHCP
proxy now bakes arch into the boot.ipxe chain URL), generalizing
per-MAC pins. Persisted to boot_rules.json; GET/PUT /api/boot-rules;
rules editor + webhook field on the Hosts tab.
- Optional pixiecore-style webhook: unmatched boots GET
<url>?mac=&arch= and 200 {"target":"id"} chains to it. Fail-open
with a 2s budget — a dead endpoint can never block PXE.
- Decision order: exact pin -> rules -> webhook -> menu. Empty config
is byte-for-byte the previous behavior.
Tokenized answer files (the post-WDS/CVE-2026-0386 hardening):
- Every generated unattended URL (inst.ks / preseed url / autoinstall
seed) now carries a 4h boot-scoped token; /unattended/{id} and the
cloud-init seed routes require it (or an operator session) once an
admin exists. Stops answer-file credential harvesting by anything
else on the network. No toggle; setup-mode installs stay open.
Validation: clippy clean, fmt clean, 290 workspace tests green
(+18 new across boot_tokens, boot_rules, arch ladder, escalation,
grub renderer, and four new full-flow integration tests).
Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
This commit is contained in:
co-authored by
Claude Opus 4.8
parent
7f25bb681c
commit
3a32d65fb7
@@ -132,12 +132,17 @@ impl DriverEscalation {
|
||||
let recent = now.duration_since(entry.last_seen) < SAME_BOOT_DEBOUNCE;
|
||||
if primary && !recent {
|
||||
// A genuinely new boot. If the previous attempt was never
|
||||
// confirmed, the firmware-net build failed → escalate to
|
||||
// the all-drivers build. Builtin is the most capable build
|
||||
// we have, so it's the single escalation target (and a MAC
|
||||
// already on Builtin simply stays there).
|
||||
// confirmed, the build we served failed → climb one rung:
|
||||
// Firmware (firmware NIC stack) → Builtin (iPXE's own
|
||||
// drivers) → Shim (signed shim+GRUB, v0.7.0 — covers
|
||||
// Secure Boot firmware that downloads our unsigned iPXE
|
||||
// but refuses to execute it). Shim is terminal: a MAC
|
||||
// there stays until its entry TTLs out and resets.
|
||||
if entry.awaiting_confirm {
|
||||
entry.mode = DriverMode::Builtin;
|
||||
entry.mode = match entry.mode {
|
||||
DriverMode::Firmware => DriverMode::Builtin,
|
||||
DriverMode::Builtin | DriverMode::Shim => DriverMode::Shim,
|
||||
};
|
||||
}
|
||||
entry.awaiting_confirm = true;
|
||||
}
|
||||
@@ -221,6 +226,29 @@ mod tests {
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn third_unconfirmed_attempt_escalates_to_shim_and_stays() {
|
||||
// v0.7.0: a Secure-Boot client downloads-but-refuses both unsigned
|
||||
// iPXE builds; the third boot gets the signed shim chain, and the
|
||||
// MAC stays there for subsequent boots.
|
||||
let e = DriverEscalation::new();
|
||||
let t0 = Instant::now();
|
||||
assert_eq!(e.decide_at("ee", true, t0), DriverMode::Firmware);
|
||||
assert_eq!(
|
||||
e.decide_at("ee", true, t0 + Duration::from_mins(1)),
|
||||
DriverMode::Builtin
|
||||
);
|
||||
assert_eq!(
|
||||
e.decide_at("ee", true, t0 + Duration::from_mins(2)),
|
||||
DriverMode::Shim
|
||||
);
|
||||
// Shim is terminal — a fourth unconfirmed boot stays on Shim.
|
||||
assert_eq!(
|
||||
e.decide_at("ee", true, t0 + Duration::from_mins(3)),
|
||||
DriverMode::Shim
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn stale_entry_is_forgotten_and_resets_to_firmware() {
|
||||
let e = DriverEscalation::new();
|
||||
|
||||
@@ -49,10 +49,13 @@ pub fn decide(ctx: &ReplyContext<'_>) -> BootDirective {
|
||||
// Pass the client's MAC in the query string so the HTTP
|
||||
// layer can short-circuit to a per-MAC binding when one
|
||||
// exists. iPXE substitutes `${mac}` literally before issuing
|
||||
// the GET, so this stays static across firmwares.
|
||||
// the GET, so this stays static across firmwares. The arch is
|
||||
// known *here* from option 93, so it's baked in literally
|
||||
// (v0.7.0) — it lets boot rules select on architecture.
|
||||
url: format!(
|
||||
"{}/boot.ipxe?mac=${{mac}}",
|
||||
ctx.public_base_url.trim_end_matches('/')
|
||||
"{}/boot.ipxe?mac=${{mac}}&arch={}",
|
||||
ctx.public_base_url.trim_end_matches('/'),
|
||||
ctx.arch.as_str()
|
||||
),
|
||||
},
|
||||
FirmwareClass::HttpClient => {
|
||||
@@ -60,9 +63,12 @@ pub fn decide(ctx: &ReplyContext<'_>) -> BootDirective {
|
||||
// pointing at an EFI executable. We serve the iPXE EFI build for
|
||||
// the negotiated driver mode over HTTP; it'll then do the same
|
||||
// script-fetch the iPXE path does.
|
||||
// `bootfile_with_fallback` (v0.7.0) walks back down the
|
||||
// escalation ladder when the negotiated mode has no binary
|
||||
// for this arch (e.g. Shim on an arch with no signed chain).
|
||||
let name = ctx
|
||||
.arch
|
||||
.ipxe_bootfile_mode(ctx.driver_mode)
|
||||
.bootfile_with_fallback(ctx.driver_mode)
|
||||
.unwrap_or("snponly.efi");
|
||||
BootDirective::HttpScript {
|
||||
url: format!(
|
||||
@@ -72,7 +78,7 @@ pub fn decide(ctx: &ReplyContext<'_>) -> BootDirective {
|
||||
),
|
||||
}
|
||||
}
|
||||
FirmwareClass::PxeClient => match ctx.arch.ipxe_bootfile_mode(ctx.driver_mode) {
|
||||
FirmwareClass::PxeClient => match ctx.arch.bootfile_with_fallback(ctx.driver_mode) {
|
||||
Some(name) => BootDirective::TftpIpxe {
|
||||
filename: name.to_string(),
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user