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
+68
View File
@@ -0,0 +1,68 @@
#
# .gitea/workflows/dnsmasq-image.yaml
#
# Builds the serving-stack dnsmasq image (serving/dnsmasq) and pushes it to the
# registry, so serving hosts `docker compose pull` it instead of building it at
# deploy time (matching the SPDK image). The image is GENERIC: it carries no
# Secure Boot keys and no iPXE binary — the signed snponly.efi rides the separate
# fleet-ipxe image (ipxe-image.yaml), mounted into dnsmasq at /ipxe.
#
# Runs only when the dnsmasq image inputs change (path filter) or on demand.
#
# Required Gitea repo secrets: PACKAGE_USER, PACKAGE_TOKEN (registry creds).
#
name: Build dnsmasq image
run-name: ${{ gitea.actor }} builds the serving dnsmasq image
on:
push:
branches:
- "main"
paths:
- "serving/dnsmasq/**"
- ".gitea/workflows/dnsmasq-image.yaml"
workflow_dispatch:
env:
DNSMASQ_IMAGE: "git.bitasylum.org/audiobuzz/fleet-dnsmasq"
jobs:
dnsmasq-build:
name: Build & push dnsmasq image
runs-on: ubuntu-latest
container:
image: git.bitasylum.org/audiobuzz/ubuntu-act-runner-qemu:main
steps:
-
name: Checkout
uses: actions/checkout@v6
-
name: Install buildah
run: |
sudo apt-get update
sudo apt-get install -y --no-install-recommends buildah
-
name: Registry login
run: |
buildah login \
--username "${{ secrets.PACKAGE_USER }}" \
--password "${{ secrets.PACKAGE_TOKEN }}" \
git.bitasylum.org
-
name: Build & push
# vfs storage: overlay-on-overlayfs is unsupported in-container.
env:
STORAGE_DRIVER: vfs
BUILDAH_ISOLATION: chroot
run: |
SHA="${{ gitea.sha }}"; SHA="${SHA:0:7}"
echo "Building ${DNSMASQ_IMAGE}:${SHA} (+ :latest)"
buildah bud \
-t "${DNSMASQ_IMAGE}:${SHA}" \
-t "${DNSMASQ_IMAGE}:latest" \
serving/dnsmasq
buildah push "${DNSMASQ_IMAGE}:${SHA}"
buildah push "${DNSMASQ_IMAGE}:latest"
+124
View File
@@ -0,0 +1,124 @@
#
# .gitea/workflows/ipxe-image.yaml
#
# Builds the fleet-ipxe image: the iPXE network bootloader (snponly.efi) for the
# PXE-only chainload path (firmware without UEFI HTTP boot, e.g. EliteBook 820
# G4), SIGNED with the fleet `db` key and packaged as a tiny FROM-scratch OCI
# image. The dnsmasq serving container mounts it at /ipxe and copies snponly.efi
# into its TFTP root — so the serving host never carries a loose, unsigned .efi
# and the bootloader is digest-pinned + signed exactly like the UKI.
#
# Kept separate from the (generic) dnsmasq image so the daemon image stays
# key-agnostic and this one is the only per-Secure-Boot-environment artifact.
# See scripts/build-ipxe-oci.sh, docs/secure-boot-keys.md, CLAUDE.md invariant #9.
#
# Runs only when the iPXE inputs change (path filter) or on demand.
#
# Required Gitea repo secrets: PACKAGE_USER, PACKAGE_TOKEN (registry creds).
# Optional: SB_KEY, SB_CERT (db PRIVATE key + cert, PEM). Unset => UNSIGNED lab
# image (loud warning; Secure-Boot-enforcing hosts will reject it). PK/KEK
# never come near CI — only `db` signs (docs/secure-boot-keys.md).
#
name: Build fleet-ipxe image
run-name: ${{ gitea.actor }} builds the signed fleet-ipxe bootloader image
on:
push:
branches:
- "main"
paths:
- "scripts/build-ipxe-oci.sh"
- ".gitea/workflows/ipxe-image.yaml"
workflow_dispatch:
inputs:
ipxe_version:
description: "Override the image tag (default: sanitized ipxe package version)"
required: false
env:
IPXE_IMAGE: "git.bitasylum.org/audiobuzz/fleet-ipxe"
jobs:
ipxe-build:
name: Build & push fleet-ipxe image
runs-on: ubuntu-latest
container:
image: git.bitasylum.org/audiobuzz/ubuntu-act-runner-qemu:main
steps:
-
name: Checkout
uses: actions/checkout@v6
-
name: Install tooling
# buildah (OCI build), sbsigntool (sbsign/sbverify — Debian singular), and
# the ipxe package which ships the prebuilt snponly.efi.
run: |
sudo apt-get update
sudo apt-get install -y --no-install-recommends buildah sbsigntool ipxe
-
name: Locate snponly.efi + derive tag
id: src
run: |
# snponly.efi: UEFI x64 iPXE that rides the firmware's NIC driver (SNP),
# so it binds where iPXE's native driver won't (e.g. I219-LM). Ubuntu's
# ipxe package installs it under /usr/lib/ipxe.
SNP=$(find /usr/lib/ipxe /usr/share/ipxe -name 'snponly.efi' 2>/dev/null | head -1)
[ -n "$SNP" ] || { echo "snponly.efi not found in the ipxe package" >&2; exit 1; }
echo "snponly=$SNP" >> "$GITHUB_OUTPUT"
# Tag: dispatch override, else the ipxe package version sanitized to the
# OCI tag charset ([A-Za-z0-9_.-]).
RAW=$(dpkg-query -W -f='${Version}' ipxe 2>/dev/null || echo unknown)
VER="${{ inputs.ipxe_version }}"
[ -n "$VER" ] || VER=$(printf '%s' "$RAW" | sed 's/[^A-Za-z0-9_.-]/-/g')
echo "version=$VER" >> "$GITHUB_OUTPUT"
echo "Using snponly.efi=$SNP tag=$VER (ipxe pkg $RAW)"
-
name: Materialize Secure Boot signing material
# Same db key that signs the UKI; written to a runner-temp file and passed
# to build-ipxe-oci.sh as a path. If SB_KEY is unset this no-ops and the
# build produces an UNSIGNED (lab) image with its loud warning.
if: ${{ secrets.SB_KEY != '' }}
env:
SB_KEY: ${{ secrets.SB_KEY }}
SB_CERT: ${{ secrets.SB_CERT }}
run: |
umask 077
d="$RUNNER_TEMP/sb"; mkdir -p "$d"
printf '%s\n' "$SB_KEY" > "$d/db.key"
printf '%s\n' "$SB_CERT" > "$d/db.crt"
echo "SB_KEY_FILE=$d/db.key" >> "$GITHUB_ENV"
echo "SB_CERT_FILE=$d/db.crt" >> "$GITHUB_ENV"
echo "Secure Boot signing material staged (signed fleet-ipxe build)."
-
name: Registry login
run: |
buildah login \
--username "${{ secrets.PACKAGE_USER }}" \
--password "${{ secrets.PACKAGE_TOKEN }}" \
git.bitasylum.org
-
name: Build & push
env:
STORAGE_DRIVER: vfs
run: |
VER="${{ steps.src.outputs.version }}"
bash scripts/build-ipxe-oci.sh \
"${{ steps.src.outputs.snponly }}" \
"${IPXE_IMAGE}" \
"$VER" \
"${SB_KEY_FILE:-}" \
"${SB_CERT_FILE:-}" \
true
# also publish :latest for lab convenience (pin by digest in production)
buildah tag "${IPXE_IMAGE}:${VER}" "${IPXE_IMAGE}:latest"
buildah push "${IPXE_IMAGE}:latest"
-
name: Shred Secure Boot signing key
if: always()
run: shred -u "$RUNNER_TEMP/sb/db.key" 2>/dev/null || rm -f "$RUNNER_TEMP/sb/db.key" 2>/dev/null || true
+1 -1
View File
@@ -12,5 +12,5 @@ keys
# iPXE network bootloader binaries for the PXE fallback (serving/tftp). Binary,
# and the signed one is db-derived/environment-specific — supply per environment.
/serving/tftp/*.efi
#/serving/tftp/*.efi
/serving/tftp/*.kpxe
+10 -2
View File
@@ -33,7 +33,10 @@ files/plymouth/themes/fleet/ Custom boot splash (scripted security
files/kiosk/ weston+Chromium kiosk + chromium lockdown policy
scripts/extract-artifacts.sh qcow2 -> kernel/initramfs/erofs+verity/cmdline
scripts/build-oci.sh ukify + sbsign + roothash gate + OCI image
scripts/build-ipxe-oci.sh sbsign snponly.efi -> FROM-scratch fleet-ipxe image
serving/ Lab serving stack: dnsmasq + nginx + SPDK
(dnsmasq + fleet-ipxe + SPDK images are
CI-built + pulled; see .gitea/workflows/)
serving/kiosk-app/ Svelte+Tailwind spaceship-terminal SPA
(kiosk endpoint; nginx reverse-proxies /kiosk/)
.gitea/workflows/ Gitea CI: lint -> packer build -> push OCI
@@ -128,8 +131,13 @@ instead of working around it.
SHA256SUMS}`. `rootfs.erofs` (erofs data) and `rootfs.hash` (dm-verity hash
tree) are **separate** namespaces because the as-built Rocky 9 systemd does
not support the in-file `hash-offset` verity layout. No shell, no tools, no
base image. Serving software (SPDK, nginx) lives in separate, independently
versioned images — never merge them into the artifact image.
base image. Serving software (SPDK, nginx, dnsmasq) lives in separate,
independently versioned images — never merge them into the artifact image.
The signed iPXE bootloader (`snponly.efi`) is its own FROM-scratch
**`fleet-ipxe`** image (`scripts/build-ipxe-oci.sh`, signed with `db`), mounted
into dnsmasq at `/ipxe` — kept out of *both* the artifact image and the
(generic, key-agnostic) dnsmasq image because its signature is
per-Secure-Boot-environment. Don't bake `snponly.efi` into the dnsmasq image.
1. **Deployments are pinned by digest.** One commit per fleet release in
the deployment manifest; that manifest is the single source coupling
UKI ↔ namespace. Rollback = revert the commit.
+11 -4
View File
@@ -639,12 +639,19 @@ smoke test.
the MS cert even in Setup Mode — looked like a non-empty-db rejection but was
the NUL bug all along; it failed identically on a clean `pre-enrolled-keys=0`
disk.)
- **iPXE signing WIRED (2026-06-21):** `snponly.efi` is now built + signed
(`db`) + pushed by CI as a separate FROM-scratch **`fleet-ipxe`** image
(`scripts/build-ipxe-oci.sh`, `.gitea/workflows/ipxe-image.yaml`), mounted into
dnsmasq at `/ipxe`. dnsmasq itself moved to a generic CI-built/pulled image
(`.gitea/workflows/dnsmasq-image.yaml`) — no longer built at deploy time, and
carries no keys/binary. (Decision: separate `fleet-ipxe` image, not baked into
dnsmasq, so the daemon image stays key-agnostic across SB environments.)
- **REMAINING for production:** (a) **secure path** — re-verify signed boot with
`disable_dot1x=false` (802.1X/MACsec), not just the test path; (b) **820 / PXE
path** — sign `snponly.efi` with the same `db` key so the iPXE front-end also
passes Secure Boot (the VM's HTTP path has no iPXE stage, so this is still
unverified on real PXE hardware); (c) confirm signing is reproducible (verity
hash stable across two signed builds).
path** — BENCH-VERIFY the signed `fleet-ipxe` chain on the 820 (the VM's HTTP
path has no iPXE stage, so the signed iPXE binary is still unverified on real
PXE hardware); (c) confirm signing is reproducible (verity hash stable across
two signed builds).
- **PXE-only iPXE fallback — BENCH-VERIFIED on the EliteBook 820 G4 (2026-06-20).**
Firmware without UEFI HTTP boot chainloads `serving/tftp/snponly.efi` → renders
`autoexec.ipxe` (ifopen + DHCP-retry + `imgargs <real cmdline>`) → UKI over HTTP
+22 -9
View File
@@ -202,10 +202,19 @@ Caveats:
## PXE-only hosts: iPXE chainload and Secure Boot
Firmware without UEFI HTTP boot (e.g. HP EliteBook 820 G4) falls back to PXE; the
serving stack hands those hosts **iPXE** over TFTP (`serving/tftp/ipxe.efi`),
which then chainloads the UKI over HTTP. dnsmasq selects per client automatically
serving stack hands those hosts **iPXE** over TFTP (`snponly.efi`), which then
chainloads the UKI over HTTP. dnsmasq selects per client automatically
(HTTP-capable → UKI direct; PXE-only → iPXE → UKI), so no per-host config.
**Packaging:** the signed `snponly.efi` is built + signed + pushed by CI as a
small FROM-scratch **`fleet-ipxe` image** (`.gitea/workflows/ipxe-image.yaml` →
`scripts/build-ipxe-oci.sh`), mounted into the dnsmasq container at `/ipxe`
(serving/docker-compose.yml). It is deliberately a **separate, per-Secure-Boot-
environment image** (its signature is `db`-key-specific) so the dnsmasq image
stays generic/key-agnostic — same reasoning as invariant #9 keeping serving
software out of the release-artifact image. For offline/lab use, drop an
`snponly.efi` in `serving/tftp/` and the entrypoint falls back to that bind mount.
Secure Boot across that chain needs **only the `db` key — the one already
enrolled for the UKI. No extra keys, no Microsoft CA, no shim** (the fleet owns
its keys):
@@ -215,9 +224,10 @@ firmware --(verify snponly.efi vs db)--> iPXE --(chain -> firmware LoadImage
verifies UKI vs db)--> UKI
```
- The firmware verifies the TFTP-downloaded NBP (`ipxe.efi`) against `db` →
**the iPXE binary must be signed by `db`** (`sbsign`, same key/step as the UKI).
Stock/unsigned iPXE is rejected under Secure Boot.
- The firmware verifies the TFTP-downloaded NBP (`snponly.efi`) against `db` →
**the iPXE binary must be signed by `db`** (`sbsign`; CI does this in
`build-ipxe-oci.sh` when the `SB_KEY`/`SB_CERT` secrets are set, same key as the
UKI). Stock/unsigned iPXE is rejected under Secure Boot.
- iPXE must load the UKI as an **EFI image** (`imgfetch` + `boot`, or `chain`) so
the firmware's `LoadImage()` re-verifies it against `db` — do **not** use iPXE's
`kernel`/`boot` on a raw vmlinuz, which bypasses SB.
@@ -232,10 +242,13 @@ firmware --(verify snponly.efi vs db)--> iPXE --(chain -> firmware LoadImage
Boot ON is immune** — the stub ignores any loader-supplied cmdline — so this is
purely a lab/SB-off concern.
**Lab posture (now): iPXE signing OFF → run the iPXE path with Secure Boot OFF.**
`serving/tftp/ipxe.efi` is unsigned. **Production upgrade:** sign it with `db`
and Secure Boot stays enforced end-to-end (tracked in TODO #8). The HTTP-boot path
(firmware → UKI) is unaffected and remains SB-clean regardless.
**Posture:** the `fleet-ipxe` image is signed with `db` **whenever CI has the
`SB_KEY`/`SB_CERT` secrets** (same condition as the UKI), so the PXE path can stay
Secure-Boot-enforced end-to-end. An **unsigned** `fleet-ipxe` build (no secrets)
keeps its loud warning and requires Secure Boot OFF on the PXE target. The
HTTP-boot path (firmware → UKI) is unaffected and SB-clean regardless. Still
**BENCH-VERIFY** the signed PXE chain on the 820 (the VM uses HTTP boot, so the
signed iPXE stage is unverified on real hardware — TODO #8).
## Key hygiene & lifecycle
+106
View File
@@ -0,0 +1,106 @@
#!/usr/bin/env bash
# build-ipxe-oci.sh <snponly.efi> <image-ref> <version> [sb-key] [sb-cert] [push]
#
# Packages the iPXE network bootloader (snponly.efi) as a tiny FROM-scratch OCI
# image for the PXE-only chainload path (firmware without UEFI HTTP boot, e.g.
# HP EliteBook 820 G4). The dnsmasq serving container mounts this image and
# copies the binary into its TFTP root — so the serving host never carries a
# loose, unsigned .efi and the bootloader is digest-pinned + signed like the UKI.
#
# This is DELIBERATELY a separate, independently versioned image (NOT merged into
# the FROM-scratch release-artifact image built by build-oci.sh): snponly.efi is
# near-static (it changes only when iPXE is rebuilt/re-signed), whereas the
# UKI/rootfs change every release. Keeping it separate matches the serving-stack
# pattern (SPDK, nginx) and the artifact-image invariant (only the 5 release
# files live there). See CLAUDE.md invariant #9 and docs/secure-boot-keys.md.
#
# Steps:
# 1. sbsign snponly.efi with the `db` key/cert if supplied (same key that signs
# the UKI — CI/HSM only, never in this repo or the build VM). Unsigned builds
# are lab-only and keep a loud warning: a Secure-Boot-enforcing host rejects
# an unsigned snponly.efi before iPXE ever runs.
# 2. build a FROM-scratch OCI image containing ONLY:
# /ipxe/snponly.efi (signed iPXE bootloader)
# /ipxe/SHA256SUMS
# No shell, no tools, no base image. Pin deployments by digest.
#
# Requires on the build host: sbsigntools (if signing) and buildah (preferred)
# or docker. sbverify (optional) is used to confirm the signature when present.
set -euo pipefail
SRC=${1:?usage: build-ipxe-oci.sh <snponly.efi> <image-ref> <version> [sb-key] [sb-cert] [push]}
IMAGE=${2:?image ref required, e.g. registry.example.org/fleet-ipxe}
VERSION=${3:?version required (the iPXE binary version/tag)}
SB_KEY=${4:-}
SB_CERT=${5:-}
PUSH=${6:-false}
[ -f "$SRC" ] || { echo "ERROR: snponly.efi not found at $SRC" >&2; exit 1; }
STAGE=$(mktemp -d)
trap 'rm -rf "$STAGE"' EXIT
mkdir "$STAGE/ipxe"
OUT="$STAGE/ipxe/snponly.efi"
# --- 1. Sign (or pass through with a loud warning) ----------------------------
SIGNED=false
if [ -n "$SB_KEY" ] && [ -n "$SB_CERT" ]; then
command -v sbsign >/dev/null || { echo "ERROR: sbsigntools required for signing" >&2; exit 1; }
sbsign --key "$SB_KEY" --cert "$SB_CERT" --output "$OUT" "$SRC"
SIGNED=true
if command -v sbverify >/dev/null; then
sbverify --cert "$SB_CERT" "$OUT" >/dev/null \
|| { echo "FATAL: signed snponly.efi fails sbverify against the db cert." >&2; exit 1; }
echo "==> snponly.efi signed and verified against the db cert"
else
echo "==> snponly.efi signed (sbverify not present to confirm)"
fi
else
cp "$SRC" "$OUT"
echo "WARNING: building UNSIGNED snponly.efi image (no sb-key/sb-cert supplied)." >&2
echo " Fine for the lab (Secure Boot OFF); a Secure-Boot-enforcing host" >&2
echo " rejects an unsigned iPXE binary. Production must sign in CI/HSM." >&2
fi
( cd "$STAGE/ipxe" && sha256sum -- * > SHA256SUMS )
# --- 2. Build the OCI image ---------------------------------------------------
REF="${IMAGE}:${VERSION}"
LABELS=(
"org.opencontainers.image.title=fleet-ipxe"
"org.opencontainers.image.version=${VERSION}"
"org.fleet.ipxe.signed=${SIGNED}"
)
if command -v buildah >/dev/null; then
# vfs storage: overlay-on-overlayfs is unsupported in-container (CI runner);
# this image is tiny so vfs is fine. Override via STORAGE_DRIVER on a host
# with native overlay support. (Same rationale as build-oci.sh.)
export STORAGE_DRIVER="${STORAGE_DRIVER:-vfs}"
ctr=$(buildah from scratch)
buildah copy "$ctr" "$STAGE/ipxe" /ipxe
for l in "${LABELS[@]}"; do buildah config --label "$l" "$ctr"; done
buildah commit --format oci --rm "$ctr" "$REF"
DIGEST=$(buildah inspect --type image --format '{{.FromImageDigest}}' "$REF" || true)
[ "$PUSH" = "true" ] && buildah push "$REF"
elif command -v docker >/dev/null; then
{ printf 'FROM scratch\nCOPY ipxe /ipxe\n'
for l in "${LABELS[@]}"; do printf 'LABEL "%s"\n' "$l"; done
} > "$STAGE/Dockerfile"
docker build -t "$REF" "$STAGE"
DIGEST=$(docker inspect --format '{{index .RepoDigests 0}}' "$REF" 2>/dev/null || true)
[ "$PUSH" = "true" ] && docker push "$REF"
else
echo "ERROR: need buildah or docker to build the OCI image" >&2; exit 1
fi
cat <<EOF
==> fleet-ipxe image built: $REF
signed: $SIGNED
digest: ${DIGEST:-"(available after push)"}
Deploy: set IPXE_IMAGE/IPXE_VERSION in serving/.env to this ref, then
\`docker compose up -d dnsmasq\` (the entrypoint copies /ipxe/snponly.efi
into the TFTP root). Pin by digest in production.
EOF
+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.