From b2ad0210bfa7c7f98d4c2ab6693169b78f53fa8e Mon Sep 17 00:00:00 2001 From: Ilker Manap Date: Fri, 30 Oct 2026 12:51:22 +0100 Subject: [PATCH] GRUB keeps the firmware's text console; phone-home back to HTTP Lab, Secure Boot VM: the deployed Debian booted under Secure Boot through the chainload, but its console was garbled after the network GRUB had switched the display mode; GRUB menus now use the firmware's text console (terminal_output console), which also suits serial/SOL. phone-home over HTTPS failed once on that first boot (TLS: bad record MAC on the region's side; the same request works from the machine since), and cloud-init spends its 10 tries within milliseconds. Its content is not secret and its token only works from the machine's address, so it goes over HTTP again. Co-Authored-By: Claude Opus 5.5 --- CHANGELOG.md | 8 ++++++-- cmd/rollcall/deploy.go | 9 ++++----- cmd/rollcall/grub.go | 5 +++++ 3 files changed, 15 insertions(+), 7 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index b652461..b5cae5c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -91,8 +91,12 @@ region and `deploy/install-rack.sh` on rack hosts (new dnsmasq layout, signed ra the agents' endpoints and images on 443. Agents accept the CA by the fingerprint on the PXE command line and do not fall back to HTTP. Old agents keep working over HTTP. The commissioning image has to be rebuilt to get the new agents. - - The stateless overlay client works the same way (rebuild stateless images), and a deployed OS - calls home over HTTPS. + - The stateless overlay client works the same way (rebuild stateless images). +- **UEFI and Secure Boot.** UEFI machines PXE-boot through Debian's signed shim and GRUB, so machines + with Secure Boot on can boot at all; before, the firmware refused the unsigned `syslinux.efi`. + - GRUB menus are made from the pxelinux ones. + - The local disk is booted by chainloading the installed OS's signed loader. + - Secure Boot verifies the kernel; the initrd and squashfs are not covered. - **BMC certificates.** HTTPS BMCs (Redfish, Proxmox) are pinned on first use. The certificate is read in a handshake that carries no credentials, and a changed certificate gets none (`tls_fingerprint`; cleared on an address change or a BMC firmware update by Rollcall). diff --git a/cmd/rollcall/deploy.go b/cmd/rollcall/deploy.go index 4dfdf4e..3f39654 100644 --- a/cmd/rollcall/deploy.go +++ b/cmd/rollcall/deploy.go @@ -297,11 +297,10 @@ func (a *App) renderSeed(j *Job, sp DeploySpec, token string) []seedFile { if len(sp.Packages) > 0 { fmt.Fprintf(&b, "packages: %s\n", yamlList(sp.Packages)) } - home := base - if t := a.agentTLS(base); t != "" && a.ca != nil { - home = t // the deployed OS trusts the Rollcall CA (bootcmd below), so it calls home over HTTPS - } - fmt.Fprintf(&b, "phone_home:\n url: %s/api/v1/phone-home/%s\n post: [instance_id, hostname, fqdn, pub_key_ed25519]\n tries: 10\n", home, token) + // plain HTTP on purpose: nothing in it is secret (the token only works from this + // machine's address), and cloud-init spends its 10 tries within milliseconds, so a + // TLS hiccup on first boot (seen once on Debian: bad record MAC) loses "booted" + fmt.Fprintf(&b, "phone_home:\n url: %s/api/v1/phone-home/%s\n post: [instance_id, hostname, fqdn, pub_key_ed25519]\n tries: 10\n", base, token) b.WriteString("final_message: \"rollcall: deployed, up after $UPTIME s\"\n") // only $UPTIME/$TIMESTAMP/$DATASOURCE/$VERSION are expanded ud = b.String() } diff --git a/cmd/rollcall/grub.go b/cmd/rollcall/grub.go index e432222..b513961 100644 --- a/cmd/rollcall/grub.go +++ b/cmd/rollcall/grub.go @@ -24,6 +24,7 @@ import ( ) const grubDispatch = `# generated by rollcall: the machine's own menu, else the one for unknown machines +terminal_output console set timeout=8 configfile $prefix/grub.cfg-$net_default_mac configfile $prefix/grub.cfg-default @@ -90,6 +91,10 @@ func pxelinuxToGrub(body string) string { } var b strings.Builder b.WriteString("# generated by rollcall from the pxelinux menu\n") + // the firmware's text console: GRUB does not switch the display mode, so the OS + // it starts gets the firmware's own (OVMF showed a garbled console after a switch), + // and serial/SOL consoles see the menu + b.WriteString("terminal_output console\n") fmt.Fprintf(&b, "set default=%d\nset timeout=%d\n", def, timeout) for _, e := range entries { fmt.Fprintf(&b, "menuentry %s {\n", grubQuote(e.label))