Refactor dnsmasq to build as a container. Add signed iPXE image build

This commit is contained in:
Audiobuzz
2026-06-21 09:11:23 +00:00
parent 535793228e
commit 3226a44d5a
13 changed files with 410 additions and 35 deletions
+12
View File
@@ -17,6 +17,18 @@ ARTIFACT_SRC=../artifacts/0.0.0-2eefd97
SPDK_IMAGE=git.bitasylum.org/audiobuzz/spdk-nvmf
SPDK_VERSION=v24.09
# dnsmasq image — prebuilt + pushed by CI (.gitea/workflows/dnsmasq-image.yaml);
# GENERIC (no Secure Boot keys / no iPXE binary). Pulled, not built at deploy.
DNSMASQ_IMAGE=git.bitasylum.org/audiobuzz/fleet-dnsmasq
DNSMASQ_VERSION=latest
# fleet-ipxe image — the SIGNED iPXE bootloader (snponly.efi) for PXE-only hosts,
# built + signed (fleet db key) + pushed by CI (.gitea/workflows/ipxe-image.yaml).
# Mounted into dnsmasq at /ipxe. Per-environment (signature is db-key-specific);
# pin by digest in production, e.g. fleet-ipxe@sha256:…
IPXE_IMAGE=git.bitasylum.org/audiobuzz/fleet-ipxe
IPXE_VERSION=latest
# --- This serving host on the boot LAN -------------------------------------
BOOT_IFACE=ens19 # interface facing the booting client / test bridge
SERVER_IP=192.168.50.10 # this host's address on BOOT_IFACE (DHCP/DNS/HTTP/NVMe)
+12 -2
View File
@@ -23,8 +23,18 @@
set -eu
mkdir -p /srv/tftp
# User-supplied iPXE binary (snponly.efi) from the read-only source mount.
cp -f /tftp-src/*.efi /srv/tftp/ 2>/dev/null || true
# iPXE binary (snponly.efi) into the TFTP root. Prefer the signed binary from the
# mounted fleet-ipxe image (/ipxe); fall back to the read-only bind mount
# (/tftp-src) for offline/lab use. The dnsmasq image carries no .efi itself.
if ls /ipxe/*.efi >/dev/null 2>&1; then
cp -f /ipxe/*.efi /srv/tftp/
echo "iPXE bootloader source: /ipxe (fleet-ipxe image)"
elif ls /tftp-src/*.efi >/dev/null 2>&1; then
cp -f /tftp-src/*.efi /srv/tftp/
echo "iPXE bootloader source: /tftp-src (bind mount, lab fallback)"
else
echo "WARN: no iPXE binary in /ipxe or /tftp-src — PXE-only clients can't chainload" >&2
fi
# autoexec.ipxe was an STP-era workaround (ifopen+retry before autoboot); it's no
# longer rendered — PortFast on the switch port is the real fix. Remove any stale
# copy left in a long-lived container's writable layer.
+24 -11
View File
@@ -37,7 +37,12 @@
services:
# --- DHCP + DNS ------------------------------------------------------------
dnsmasq:
build: ./dnsmasq
# Prebuilt by CI (.gitea/workflows/dnsmasq-image.yaml) and pulled here, like
# the SPDK image — the serving host no longer builds it at deploy time. The
# image is GENERIC (no Secure Boot keys, no iPXE binary): the signed iPXE
# bootloader rides the separate, per-environment fleet-ipxe image mounted at
# /ipxe below. Local-dev alternative: replace this line with `build: ./dnsmasq`.
image: ${DNSMASQ_IMAGE}:${DNSMASQ_VERSION}
network_mode: host
cap_add:
- NET_ADMIN # DHCP raw sockets
@@ -48,11 +53,19 @@ services:
# + DNS A/PTR. Auto-populated by fleet-namer (creative names per MAC); add
# manual lines to pin specific hosts. RW so fleet-namer can append.
- ./dnsmasq/dhcp-hosts:/etc/dnsmasq-hosts
# iPXE binary source (read-only) for the PXE-only chainload (firmware without
# UEFI HTTP boot, e.g. EliteBook 820 G4). Drop ipxe.efi here — see
# serving/tftp/README. The entrypoint copies it into /srv/tftp and renders
# boot.ipxe alongside it (the actual TFTP root is container-internal).
- ./tftp:/tftp-src:ro
# Signed iPXE bootloader for the PXE-only chainload (firmware without UEFI
# HTTP boot, e.g. EliteBook 820 G4). Mounted from the separate, signed,
# digest-pinnable fleet-ipxe image (.gitea/workflows/ipxe-image.yaml); the
# entrypoint copies /ipxe/snponly.efi into /srv/tftp. Kept OUT of the dnsmasq
# image so the daemon image stays generic/key-agnostic — see serving/tftp/README.
- type: image
source: ${IPXE_IMAGE}:${IPXE_VERSION}
target: /ipxe
image:
subpath: ipxe
# Fallback (Docker < 28 / offline / lab): drop snponly.efi in ./tftp and bind
# it instead (the entrypoint prefers /ipxe, then falls back to /tftp-src):
# - ./tftp:/tftp-src:ro
# The release artifacts (same image volume as the http service), read-only,
# so the entrypoint can objcopy the UKI's .cmdline section into boot.ipxe
# (Secure Boot OFF needs the real cmdline fed to iPXE — see entrypoint.sh).
@@ -137,11 +150,11 @@ services:
# snponly.efi = UEFI x64 iPXE that rides the FIRMWARE's NIC driver (UEFI SNP)
# instead of iPXE's own — works on virtio VMs AND real hardware where the
# native driver won't bind (e.g. EliteBook 820 G4 / Intel I219-LM, which
# fails ipxe.efi with "No configurable methods"). Ships in Ubuntu's `ipxe`
# package; see serving/tftp/README.md.
# NOTE (lab): the iPXE binary is UNSIGNED, so this path needs Secure Boot OFF.
# For production, sign it with the fleet db key (then SB stays enforced
# end-to-end) — see docs/secure-boot-keys.md.
# fails ipxe.efi with "No configurable methods"). Built + signed by CI into
# the fleet-ipxe image (mounted at /ipxe); see serving/tftp/README.md.
# The fleet-ipxe image is signed with the fleet db key when CI has the
# secrets, so this path stays Secure-Boot-enforced end-to-end; an unsigned
# (lab) fleet-ipxe build needs Secure Boot OFF — see docs/secure-boot-keys.md.
- --enable-tftp
- --tftp-root=/srv/tftp
- --dhcp-vendorclass=set:pxe,PXEClient
+20 -6
View File
@@ -1,11 +1,25 @@
# TFTP root — iPXE chainload for PXE-only firmware
This directory is mounted read-only at `/srv/tftp` in the dnsmasq container and
serves the iPXE network bootloader over TFTP to firmware that **lacks UEFI HTTP
boot** (e.g. HP EliteBook 820 G4). HTTP-boot-capable firmware never touches this
path — it gets the UKI straight over HTTP (see the `httpboot` tag in
`docker-compose.yml`). The fallback is automatic, driven by what each client
advertises (DHCP vendor-class / client-arch).
The iPXE network bootloader (`snponly.efi`) is served over TFTP to firmware that
**lacks UEFI HTTP boot** (e.g. HP EliteBook 820 G4). HTTP-boot-capable firmware
never touches this path — it gets the UKI straight over HTTP (see the `httpboot`
tag in `docker-compose.yml`). The fallback is automatic, driven by what each
client advertises (DHCP vendor-class / client-arch).
> **Where `snponly.efi` comes from (production): the `fleet-ipxe` OCI image.**
> CI builds it, **signs it with the fleet `db` key**, and pushes a tiny
> FROM-scratch image (`.gitea/workflows/ipxe-image.yaml` →
> `scripts/build-ipxe-oci.sh`). The dnsmasq container mounts that image at
> `/ipxe` and the entrypoint copies `/ipxe/snponly.efi` into `/srv/tftp`. It's a
> **separate, per-Secure-Boot-environment image** (its signature is `db`-specific)
> so the dnsmasq image itself stays generic — see docs/secure-boot-keys.md and
> CLAUDE.md invariant #9.
>
> **This directory is the offline/lab fallback only:** if you drop an
> `snponly.efi` here and enable the commented `./tftp:/tftp-src:ro` bind in
> `docker-compose.yml`, the entrypoint uses it when `/ipxe` is empty (it prefers
> `/ipxe`, then falls back to `/tftp-src`). Handy on Docker < 28 or when iterating
> without a registry round-trip.
Flow for a PXE-only host:
Binary file not shown.
Binary file not shown.