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).
Flow for a PXE-only host:
firmware PXE (PXEClient) --TFTP--> snponly.efi (iPXE)
iPXE re-DHCPs (user-class "iPXE") --> handed boot.ipxe (TFTP)
boot.ipxe: imgfetch UKI over HTTP -> imgargs <real cmdline> -> boot
boot.ipxe is rendered at container start by the dnsmasq entrypoint; only the
iPXE binary (snponly.efi) is supplied here. The script exists because of a sharp
edge:
Why not chainload the UKI URL directly? When iPXE executes the UKI it sets the EFI load options to the image's command line — or, if that's empty, to the image name. With Secure Boot OFF, systemd-stub uses the load options as the kernel cmdline, replacing the UKI's embedded cmdline (
roothash/rd.nvmf.discover/rd.dot1x=0/systemd.volatile=overlay). So booting the URL directly — or clearing the args — makes the kernel come up with justfleet-<ver>.efias its cmdline → dracut emergency shell.The fix: feed iPXE the real cmdline. On the serving host that cmdline only lives inside the UKI (it's not in the OCI image — invariant #10 — nor shipped as a file), so the entrypoint extracts the UKI's
.cmdlinePE section withobjcopy(the artifact image is mounted read-only at/artifacts) and bakes it intoboot.ipxeasimgargs <uki> <cmdline>. Needsbinutilsin the image (added) and the artifact mount.Secure Boot ON makes all of this moot — the stub ignores any loader-supplied cmdline and uses the embedded one — so this is purely a lab/SB-OFF concern; the rendered script is harmless once SB is on.
What to drop here
A UEFI x86-64 iPXE binary named snponly.efi (the name the dnsmasq config
serves). snponly rides the firmware's NIC driver via the UEFI SNP protocol
rather than iPXE's own, so it works on virtio VMs and real hardware where the
native driver won't bind.
# Ubuntu/Debian package (simplest — confirmed present):
sudo apt-get install -y ipxe # then copy whatever snponly it ships, e.g.:
# dpkg -L ipxe | grep -i snponly
cp /usr/lib/ipxe/snponly.efi serving/tftp/snponly.efi
# Fedora/EL: dnf install ipxe-bootimgs -> /usr/share/ipxe/ipxe-snponly-x86_64.efi
# or build: git clone https://github.com/ipxe/ipxe && cd ipxe/src
# make bin-x86_64-efi/snponly.efi
Why
snponly, not the fullipxe.efi? iPXE's own NIC drivers don't bind on some hardware — the EliteBook 820 G4 (Intel I219-LM) failsipxe.efiwith "net0: No configurable methods".snponlyinherits the firmware's working driver (the one that just loaded iPXE over PXE), so it's the robust default.boot.ipxe.orgonly publishes the fullipxe.efi, notsnponly— getsnponlyfrom a distro package or build it.
Updating the binary requires a dnsmasq restart — the entrypoint copies it from this (read-only) mount into the container's TFTP root at startup:
docker compose restart dnsmasq.
(BIOS/legacy hosts would need undionly.kpxe + client-arch matching; this fleet
is UEFI x64.)
Secure Boot
Lab (now): Secure Boot OFF for the iPXE path. snponly.efi here is
unsigned, so a Secure-Boot-enforcing machine would reject it.
Production upgrade: sign snponly.efi with the fleet db key (the same key
that signs the UKI), then Secure Boot stays enforced end-to-end
(firmware → verifies iPXE vs db → iPXE chains the UKI → firmware LoadImage
verifies UKI vs db). Only db needs enrolling — no Microsoft keys / shim, since
the fleet owns its keys. See ../../docs/secure-boot-keys.md.
# production (where db.key lives — CI/HSM):
sbsign --key db.key --cert db.crt --output snponly.efi snponly.efi
Bench notes / gotchas (EliteBook 820 G4 / Intel I219-LM, 2026-06-20)
Hard-won findings from bringing up the first PXE-only physical host:
- Use
snponly.efi, not the fullipxe.efi. The 820 G4's NIC is an Intel I219-LM (PCI00:1f.6). iPXE's nativeinteldriver in the prebuilt full build doesn't bind it (net0: No configurable methods/ nonet0).snponlyrides the firmware's driver via NII/SNP, sonet0appears. Ubuntu'sipxepackage ships a usablesnponly.efi(no build needed). - PortFast/edge on the access port is REQUIRED (this was the real blocker).
iPXE's
ifopenresets the PHY → the link bounces → a managed switch restarts its STP forwarding delay (~30 s) and silently drops packets → iPXE's DHCP never reaches the server (ifstatshowslink:down/Link status: Unknown; symptom isNo configurable methodsor a dracut shell). The firmware's own PXE works only because it keeps the link up continuously from POST so STP already converged. Fix: enable PortFast/edge (or disable STP) on the workstation port — VERIFIED to fix it 2026-06-20. (Ciscospanning-tree portfast; Aruba/HPadmin-edge-port; UniFi "Fast"; or a dumb unmanaged switch.) Theautoexec.ipxeretry loop (below) is a fallback that outlasts STP but adds ~30 s to every boot — PortFast removes the delay entirely. - iPXE aborts the script on any unguarded command failure (→ iPXE exits → the
firmware restarts the whole PXE process, looking like "it doesn't retry"). Every
fallible step in
boot.ipxe/autoexec.ipxeis therefore||-guarded. autoexec.ipxe(rendered by the dnsmasq entrypoint) is fetched + run at iPXE startup, replacing the default autoboot; that's where theifopen+ DHCP-retry +imgargs <real cmdline>chain lives.
Note
*.efi here is gitignored (binary, and the signed one is environment-specific
/ produced from db). Keep this README + .gitkeep.