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 <noreply@anthropic.com>
150 lines
4.7 KiB
Go
150 lines
4.7 KiB
Go
package main
|
|
|
|
// UEFI machines boot through shim and GRUB, both signed (Debian's
|
|
// shim-signed and grub-efi-amd64-signed), so a machine with Secure Boot on
|
|
// PXE-boots at all (syslinux.efi is unsigned: the firmware refuses it) and
|
|
// GRUB loads only kernels that shim verifies. BIOS machines keep pxelinux.
|
|
//
|
|
// TFTP root:
|
|
//
|
|
// bootx64.efi shim (shimx64.efi.signed)
|
|
// grubx64.efi GRUB for network boot (grubnetx64.efi.signed)
|
|
// grub/grub.cfg picks grub.cfg-<mac>, else grub.cfg-default
|
|
// grub/grub.cfg-<mac> per machine, made from its pxelinux file
|
|
//
|
|
// The rack controller writes the GRUB files from the pxelinux ones it gets,
|
|
// so both menus always say the same. Secure Boot checks the kernel; the
|
|
// initrd and the squashfs it fetches are not covered by it.
|
|
|
|
import (
|
|
"fmt"
|
|
"regexp"
|
|
"strconv"
|
|
"strings"
|
|
)
|
|
|
|
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
|
|
`
|
|
|
|
var grubNameRe = regexp.MustCompile(`^grub\.cfg-([0-9a-f]{2}:){5}[0-9a-f]{2}$`)
|
|
|
|
// grubName is the GRUB file for a pxelinux one: 01-aa-bb-cc-dd-ee-ff -> grub.cfg-aa:bb:cc:dd:ee:ff.
|
|
func grubName(pxe string) string {
|
|
if !strings.HasPrefix(pxe, "01-") {
|
|
return ""
|
|
}
|
|
n := "grub.cfg-" + strings.ReplaceAll(strings.TrimPrefix(pxe, "01-"), "-", ":")
|
|
if !grubNameRe.MatchString(n) {
|
|
return ""
|
|
}
|
|
return n
|
|
}
|
|
|
|
type pxeEntry struct {
|
|
label, kernel, initrd, append string
|
|
local, poweroff bool
|
|
}
|
|
|
|
// pxelinuxToGrub turns one of rollcall's pxelinux menus into the same GRUB menu.
|
|
func pxelinuxToGrub(body string) string {
|
|
var entries []*pxeEntry
|
|
def, timeout := 0, 5
|
|
var cur *pxeEntry
|
|
for _, raw := range strings.Split(body, "\n") {
|
|
line := strings.TrimSpace(raw)
|
|
f := strings.Fields(line)
|
|
if len(f) == 0 || strings.HasPrefix(line, "#") {
|
|
continue
|
|
}
|
|
key := strings.ToUpper(f[0])
|
|
rest := strings.TrimSpace(line[len(f[0]):])
|
|
switch {
|
|
case key == "TIMEOUT":
|
|
if n, err := strconv.Atoi(rest); err == nil { // tenths of a second
|
|
timeout = (n + 9) / 10
|
|
}
|
|
case key == "LABEL":
|
|
cur = &pxeEntry{label: rest}
|
|
entries = append(entries, cur)
|
|
case cur == nil:
|
|
case key == "MENU" && len(f) > 1 && strings.EqualFold(f[1], "LABEL"):
|
|
cur.label = strings.TrimSpace(rest[len(f[1]):])
|
|
case key == "MENU" && len(f) > 1 && strings.EqualFold(f[1], "DEFAULT"):
|
|
def = len(entries) - 1
|
|
case key == "KERNEL":
|
|
if strings.HasSuffix(rest, "poweroff.c32") {
|
|
cur.poweroff = true
|
|
} else {
|
|
cur.kernel = rest
|
|
}
|
|
case key == "INITRD":
|
|
cur.initrd = rest
|
|
case key == "APPEND":
|
|
cur.append = rest
|
|
case key == "LOCALBOOT":
|
|
cur.local = true
|
|
}
|
|
}
|
|
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))
|
|
switch {
|
|
case e.poweroff:
|
|
b.WriteString(" halt\n")
|
|
case e.local:
|
|
b.WriteString(grubLocalBoot)
|
|
case e.kernel != "":
|
|
fmt.Fprintf(&b, " linux %s", grubQuote("/"+strings.TrimPrefix(e.kernel, "/")))
|
|
for _, w := range strings.Fields(e.append) {
|
|
b.WriteString(" " + grubQuote(w))
|
|
}
|
|
b.WriteString("\n")
|
|
if e.initrd != "" {
|
|
fmt.Fprintf(&b, " initrd %s\n", grubQuote("/"+strings.TrimPrefix(e.initrd, "/")))
|
|
}
|
|
default:
|
|
b.WriteString(" exit 1\n")
|
|
}
|
|
b.WriteString("}\n")
|
|
}
|
|
return b.String()
|
|
}
|
|
|
|
// grubLocalBoot starts the installed OS by chainloading its signed loader
|
|
// from the disk's EFI partition (shim, which Secure Boot accepts). Handing
|
|
// back to the firmware with "exit" is not enough: firmware then tries its
|
|
// other network boot options and ends in its own menu (OVMF does).
|
|
const grubLocalBoot = ` insmod part_gpt
|
|
insmod fat
|
|
insmod chain
|
|
for f in /EFI/debian/shimx64.efi /EFI/ubuntu/shimx64.efi /EFI/rocky/shimx64.efi /EFI/almalinux/shimx64.efi /EFI/redhat/shimx64.efi /EFI/centos/shimx64.efi /EFI/opensuse/shim.efi /EFI/sles/shim.efi /EFI/BOOT/BOOTX64.EFI; do
|
|
if search --no-floppy --set=root --file $f; then
|
|
chainloader $f
|
|
boot
|
|
fi
|
|
done
|
|
echo "no EFI loader found on the disks"
|
|
sleep 10
|
|
exit 1
|
|
`
|
|
|
|
var grubPlain = regexp.MustCompile(`^[A-Za-z0-9_./:=,+@%-]+$`)
|
|
|
|
// grubQuote makes a word safe in GRUB's script language (no $ expansion, no ;).
|
|
func grubQuote(s string) string {
|
|
if grubPlain.MatchString(s) {
|
|
return s
|
|
}
|
|
return "'" + strings.ReplaceAll(s, "'", `'\''`) + "'"
|
|
}
|