Files
diskless-workstation/scripts/build-ipxe-oci.sh
T

107 lines
4.7 KiB
Bash
Executable File

#!/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