#!/usr/bin/env bash # build-ipxe-oci.sh [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 [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 < 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