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
@@ -0,0 +1,156 @@
|
||||
//! GRUB menu rendering for the Secure Boot chain (v0.7.0).
|
||||
//!
|
||||
//! Secure-Boot-enabled firmware refuses our unsigned iPXE, so those
|
||||
//! clients are automatically escalated (see `openpxe_dhcp_proxy::
|
||||
//! escalation`) to the Microsoft-signed Fedora `shim` → signed `grub`
|
||||
//! chain. GRUB then fetches `grub.cfg` from this server (TFTP `$prefix`
|
||||
//! resolution, or HTTP when the whole chain came over HTTP Boot) — and
|
||||
//! this module renders that config from the same boot-entry model that
|
||||
//! renders `boot.ipxe`.
|
||||
//!
|
||||
//! Scope: **Linux kernel entries only.** A signed GRUB will only execute
|
||||
//! kernels that pass shim verification — i.e. distro-signed kernels —
|
||||
//! which is exactly what `LinuxKernel` boot entries point at. `sanboot`
|
||||
//! ISO emulation and `wimboot` are iPXE mechanisms with no signed
|
||||
//! equivalent; those entries are omitted here, and the menu says so.
|
||||
//! (Windows deployment under Secure Boot has no legitimate unsigned
|
||||
//! path — per project policy we never ship test-signed binaries or touch
|
||||
//! client trust stores.)
|
||||
//!
|
||||
//! The kernel/initrd lines use GRUB's `(http,host:port)` device syntax;
|
||||
//! Fedora's signed netboot GRUB carries the `http`, `tftp` and `efinet`
|
||||
//! modules built in, so no unsigned module loading is required.
|
||||
|
||||
use openpxe_iso_store::{BootKind, IsoMeta};
|
||||
use std::fmt::Write as _;
|
||||
|
||||
/// Render the full `grub.cfg` for the signed-GRUB menu.
|
||||
///
|
||||
/// `base_url` is the public HTTP base (`http://10.0.0.5` or
|
||||
/// `http://10.0.0.5:8080`) — converted to GRUB's `(http,host:port)`
|
||||
/// device prefix for kernel/initrd fetches.
|
||||
#[must_use]
|
||||
pub fn render_grub_menu(isos: &[IsoMeta], base_url: &str) -> String {
|
||||
let base = base_url.trim_end_matches('/');
|
||||
let dev = grub_http_device(base);
|
||||
let mut s = String::new();
|
||||
let _ = writeln!(s, "# OpenPXE — Secure Boot menu (signed shim+GRUB chain)");
|
||||
let _ = writeln!(s, "set timeout=30");
|
||||
let _ = writeln!(s, "set default=0");
|
||||
let _ = writeln!(s);
|
||||
|
||||
let mut entries = 0usize;
|
||||
for iso in isos {
|
||||
for entry in &iso.boot_entries {
|
||||
let BootKind::LinuxKernel {
|
||||
kernel_url,
|
||||
initrd_urls,
|
||||
args,
|
||||
} = &entry.kind
|
||||
else {
|
||||
continue;
|
||||
};
|
||||
// GRUB menu titles: keep quotes out of the label.
|
||||
let title = entry.title.replace('"', "'");
|
||||
let cmdline = args.cmdline.replace("${base-url}", base);
|
||||
let _ = writeln!(s, "menuentry \"{} — {title}\" {{", iso.filename);
|
||||
let _ = writeln!(s, " linux {dev}/{kernel_url} {cmdline}");
|
||||
if !initrd_urls.is_empty() {
|
||||
let _ = write!(s, " initrd");
|
||||
for u in initrd_urls {
|
||||
let _ = write!(s, " {dev}/{u}");
|
||||
}
|
||||
let _ = writeln!(s);
|
||||
}
|
||||
let _ = writeln!(s, "}}");
|
||||
let _ = writeln!(s);
|
||||
entries += 1;
|
||||
}
|
||||
}
|
||||
|
||||
if entries == 0 {
|
||||
let _ = writeln!(
|
||||
s,
|
||||
"menuentry \"No Secure-Boot-bootable images on this server yet\" {{ true }}"
|
||||
);
|
||||
let _ = writeln!(s);
|
||||
}
|
||||
// Always give the operator a way off this screen.
|
||||
let _ = writeln!(s, "menuentry \"Boot from local disk\" {{");
|
||||
let _ = writeln!(s, " exit");
|
||||
let _ = writeln!(s, "}}");
|
||||
s
|
||||
}
|
||||
|
||||
/// `http://10.0.0.5:8080` → `(http,10.0.0.5:8080)`. GRUB wants the
|
||||
/// scheme as the device type and host[:port] as the device address.
|
||||
fn grub_http_device(base: &str) -> String {
|
||||
let host = base
|
||||
.trim_start_matches("http://")
|
||||
.trim_start_matches("https://");
|
||||
format!("(http,{host})")
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use openpxe_iso_store::{BootEntry, IsoSource, KernelArgs};
|
||||
|
||||
fn linux_iso() -> IsoMeta {
|
||||
IsoMeta {
|
||||
id: "alp".into(),
|
||||
filename: "alpine.iso".into(),
|
||||
size_bytes: 1,
|
||||
sha256_hex: None,
|
||||
uploaded_at: time::OffsetDateTime::UNIX_EPOCH,
|
||||
source: IsoSource::Local,
|
||||
introspection: openpxe_iso_store::IntrospectionReport::default(),
|
||||
boot_entries: vec![BootEntry {
|
||||
id: "alp-linux".into(),
|
||||
title: "Linux installer".into(),
|
||||
kind: BootKind::LinuxKernel {
|
||||
kernel_url: "iso/alp/boot/vmlinuz".into(),
|
||||
initrd_urls: vec!["iso/alp/boot/initrd".into()],
|
||||
args: KernelArgs {
|
||||
cmdline: "quiet repo=${base-url}/iso/alp.iso".into(),
|
||||
},
|
||||
},
|
||||
}],
|
||||
category: openpxe_iso_store::IsoCategory::default(),
|
||||
password_hash: None,
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn renders_linux_entries_with_http_device_urls() {
|
||||
let cfg = render_grub_menu(&[linux_iso()], "http://10.0.0.5:8080/");
|
||||
assert!(
|
||||
cfg.contains("menuentry \"alpine.iso — Linux installer\""),
|
||||
"{cfg}"
|
||||
);
|
||||
assert!(
|
||||
cfg.contains("linux (http,10.0.0.5:8080)/iso/alp/boot/vmlinuz quiet repo=http://10.0.0.5:8080/iso/alp.iso"),
|
||||
"{cfg}"
|
||||
);
|
||||
assert!(
|
||||
cfg.contains("initrd (http,10.0.0.5:8080)/iso/alp/boot/initrd"),
|
||||
"{cfg}"
|
||||
);
|
||||
assert!(cfg.contains("Boot from local disk"), "{cfg}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn sanboot_and_wimboot_entries_are_omitted() {
|
||||
let mut iso = linux_iso();
|
||||
iso.boot_entries = vec![BootEntry {
|
||||
id: "win".into(),
|
||||
title: "Windows".into(),
|
||||
kind: BootKind::SanBootIso {
|
||||
iso_url: "iso/win.iso".into(),
|
||||
},
|
||||
}];
|
||||
let cfg = render_grub_menu(&[iso], "http://10.0.0.5");
|
||||
assert!(!cfg.contains("Windows"), "{cfg}");
|
||||
assert!(cfg.contains("No Secure-Boot-bootable images"), "{cfg}");
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user