Refactor dnsmasq to build as a container. Add signed iPXE image build
This commit is contained in:
@@ -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"
|
||||
@@ -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
@@ -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
|
||||
|
||||
@@ -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
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
Executable
+106
@@ -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
|
||||
@@ -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)
|
||||
|
||||
@@ -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
@@ -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
@@ -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.
Reference in New Issue
Block a user