Three features, all zero-toggle and principle-clean (single static musl
binary, container-first, no test certs, no client trust-store changes).
Secure Boot via signed shim+GRUB (automatic):
- The v0.6.1 escalation ladder gains a third rung: Firmware -> Builtin
-> Shim. Secure-Boot firmware downloads our unsigned iPXE but refuses
to execute it — indistinguishable from a failed chainload — so after
two unconfirmed attempts the MAC is offered Fedora's Microsoft-signed
shimx64.efi, which loads the signed GRUB, which fetches a
server-rendered grub.cfg. Fully signed chain, SB stays on.
- scripts/fetch-shim.sh pulls shim-x64/grub2-efi-x64 (+aa64 best-effort)
from the official Fedora 43 packages and ships the EFI binaries
byte-for-byte unmodified; Dockerfile fetch stage gained rpm2cpio/cpio.
- New grub_script renderer (Linux kernel entries only — signed GRUB only
boots signed kernels; sanboot/wimboot have no signed equivalent and
are omitted with an explanatory menu line).
- TFTP server gains a DynamicAsset hook for server-rendered names
(grub.cfg); HTTP serves the same config under /ipxe/grub.cfg for
native UEFI HTTP Boot chains. Arch-aware fallback walks back down the
ladder where no shim exists (BIOS, IA32).
Boot rules + decision webhook (open 'Matrix Boot'):
- Ordered first-match-wins rules over MAC prefix + client arch (the DHCP
proxy now bakes arch into the boot.ipxe chain URL), generalizing
per-MAC pins. Persisted to boot_rules.json; GET/PUT /api/boot-rules;
rules editor + webhook field on the Hosts tab.
- Optional pixiecore-style webhook: unmatched boots GET
<url>?mac=&arch= and 200 {"target":"id"} chains to it. Fail-open
with a 2s budget — a dead endpoint can never block PXE.
- Decision order: exact pin -> rules -> webhook -> menu. Empty config
is byte-for-byte the previous behavior.
Tokenized answer files (the post-WDS/CVE-2026-0386 hardening):
- Every generated unattended URL (inst.ks / preseed url / autoinstall
seed) now carries a 4h boot-scoped token; /unattended/{id} and the
cloud-init seed routes require it (or an operator session) once an
admin exists. Stops answer-file credential harvesting by anything
else on the network. No toggle; setup-mode installs stay open.
Validation: clippy clean, fmt clean, 290 workspace tests green
(+18 new across boot_tokens, boot_rules, arch ladder, escalation,
grub renderer, and four new full-flow integration tests).
Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
191 lines
9.6 KiB
Docker
191 lines
9.6 KiB
Docker
# syntax=docker/dockerfile:1.7
|
|
#
|
|
# OpenPXE — multi-stage build.
|
|
#
|
|
# Design:
|
|
# - stage `fetch`: runs scripts/fetch-ipxe.sh to pull official iPXE binaries
|
|
# into assets/ipxe/ so the rust build can embed them via rust-embed.
|
|
# - stage `build`: compiles the workspace with cargo in release mode.
|
|
# - stage `runtime`: Debian slim image with setcap for NET_BIND_SERVICE,
|
|
# running as a non-root UID. No shell in PATH for the service user;
|
|
# attacker surface is just the openpxe binary + libc.
|
|
#
|
|
# Why not distroless? We want setcap support and easy debug (`oc rsh`).
|
|
# Debian slim at ~75 MB + binary ~25 MB is fine for a PXE server that
|
|
# spends most of its life idle.
|
|
|
|
ARG RUST_VERSION=1.95
|
|
|
|
########## fetch iPXE binaries + wimboot ##########
|
|
# Pulls the upstream boot.ipxe.org pre-builds (no PNG support) plus
|
|
# wimboot. These cover the arches we don't build from source here:
|
|
# BIOS undionly.kpxe and i386-efi (which need a 32-bit x86 toolchain),
|
|
# and serve as the baseline that the PNG-enabled x86_64/arm64 UEFI
|
|
# binaries from the `ipxe-build` stage overlay on top of.
|
|
FROM debian:12-slim AS fetch
|
|
# rpm2cpio + cpio: extract Fedora's Microsoft-signed shim/GRUB RPMs for
|
|
# the Secure Boot chain (v0.7.0, scripts/fetch-shim.sh).
|
|
RUN apt-get update && apt-get install -y --no-install-recommends \
|
|
curl ca-certificates rpm2cpio cpio \
|
|
&& rm -rf /var/lib/apt/lists/*
|
|
WORKDIR /src
|
|
COPY scripts/fetch-ipxe.sh scripts/fetch-ipxe.sh
|
|
COPY scripts/fetch-shim.sh scripts/fetch-shim.sh
|
|
RUN mkdir -p assets/ipxe && bash scripts/fetch-ipxe.sh
|
|
# Signed shim+GRUB (Secure Boot escalation rung). Redistributed
|
|
# unmodified from the official Fedora packages — see fetch-shim.sh for
|
|
# the trust model.
|
|
RUN bash scripts/fetch-shim.sh /src/assets/ipxe
|
|
|
|
########## build PNG-enabled iPXE from source ##########
|
|
# v0.4.69: THE graphical-boot-menu unlock. iVentoy paints a PNG
|
|
# background on the PXE screen using stock iPXE built with
|
|
# CONSOLE_FRAMEBUFFER + IMAGE_PNG + CONSOLE_CMD; the public iPXE
|
|
# binaries omit those, so `console --picture` is a no-op on them.
|
|
# We build our own from upstream with that thin config delta.
|
|
#
|
|
# The historical blocker was cc1 segfaulting when an amd64 gcc ran
|
|
# under QEMU emulation on an arm64 host. The fix: pin this stage to
|
|
# $BUILDPLATFORM (the NATIVE builder arch — arm64 on an Apple-Silicon
|
|
# Mac, amd64 in x86 CI) and cross-compile with a real cross toolchain
|
|
# (CROSS_COMPILE=x86_64-linux-gnu-). The compiler runs native and
|
|
# emits x86_64 — no emulation, no segfault. arm64-efi builds natively.
|
|
FROM --platform=$BUILDPLATFORM debian:12-slim AS ipxe-build
|
|
# libc6-dev is REQUIRED and easy to miss under --no-install-recommends:
|
|
# iPXE's host utilities (elf2efi, zbin) compile with the native gcc and
|
|
# pull <stdint.h>; without the native libc headers gcc's #include_next
|
|
# falls through to iPXE's freestanding headers and dies on bits/stdint.h.
|
|
# The target (iPXE firmware) code is -ffreestanding/-nostdinc, so the
|
|
# x86_64 cross toolchain needs NO cross libc headers.
|
|
RUN apt-get update && apt-get install -y --no-install-recommends \
|
|
git make perl gcc binutils libc6-dev \
|
|
gcc-x86-64-linux-gnu binutils-x86-64-linux-gnu \
|
|
ca-certificates \
|
|
&& rm -rf /var/lib/apt/lists/*
|
|
WORKDIR /src
|
|
COPY scripts/build-ipxe.sh scripts/build-ipxe.sh
|
|
COPY deploy/ipxe/local/ deploy/ipxe/local/
|
|
RUN mkdir -p assets/ipxe && bash scripts/build-ipxe.sh /src/assets/ipxe
|
|
|
|
########## build openpxe ##########
|
|
# v0.5.2: cross-compile the Rust binary NATIVELY — no QEMU.
|
|
#
|
|
# This stage is pinned to $BUILDPLATFORM (the builder's native arch — arm64
|
|
# on an Apple-Silicon Mac, amd64 in x86 CI), exactly like `ipxe-build`. The
|
|
# Rust compiler therefore runs at full native speed and emits an
|
|
# x86_64-unknown-linux-musl binary via `cargo-zigbuild`, which uses `zig cc`
|
|
# as the cross-linker (it bundles the musl sysroot for every target, so
|
|
# there's no fiddly cross-gcc toolchain to assemble).
|
|
#
|
|
# Why this replaced the old `FROM rust ... --platform=linux/amd64` build:
|
|
# that ran the *entire* compiler under QEMU x86_64 emulation on the arm64
|
|
# host. It was ~15x slower (a single crate took >20 min) and the emulated
|
|
# gcc/linker intermittently SIGSEGV'd or hung mid-link. Cross-compiling
|
|
# sidesteps emulation entirely — the build is minutes, not half an hour,
|
|
# and is deterministic.
|
|
#
|
|
# The output is still a fully static musl binary with no glibc dependency,
|
|
# so the runtime stage stays free to be any Linux distro.
|
|
FROM --platform=$BUILDPLATFORM rust:${RUST_VERSION}-bookworm AS build
|
|
WORKDIR /src
|
|
# zig (via the `ziglang` pip package — cargo-zigbuild auto-discovers it as
|
|
# `python3 -m ziglang`) supplies the x86_64 musl sysroot + linker.
|
|
# cargo-zigbuild is the thin cargo wrapper that wires zig in as the linker.
|
|
RUN apt-get update \
|
|
&& apt-get install -y --no-install-recommends python3 python3-pip \
|
|
&& rm -rf /var/lib/apt/lists/* \
|
|
&& rustup target add x86_64-unknown-linux-musl \
|
|
&& pip3 install --no-cache-dir --break-system-packages ziglang \
|
|
&& cargo install --locked cargo-zigbuild
|
|
|
|
# Do not copy rust-toolchain.toml into the image. The local workspace pins
|
|
# developer tooling, but inside Docker we intentionally use the Rust version
|
|
# selected by the base image. Copying rust-toolchain.toml with
|
|
# `channel = "stable"` makes rustup download a second full toolchain during
|
|
# the build, which is slow and can exhaust small Colima/CI disks.
|
|
COPY Cargo.toml Cargo.lock ./
|
|
COPY crates/ crates/
|
|
# Baseline binaries (BIOS / i386 / wimboot), then overlay the
|
|
# PNG-enabled x86_64 + arm64 UEFI binaries built from source. The
|
|
# overlay wins for snponly.efi / ipxe.efi / snponly-arm64.efi so the
|
|
# common modern clients get the graphical background; the rest keep the
|
|
# upstream no-PNG binaries and the menu's `|| console` text fallback.
|
|
COPY --from=fetch /src/assets/ipxe /src/assets/ipxe
|
|
COPY --from=ipxe-build /src/assets/ipxe/snponly.efi /src/assets/ipxe/snponly.efi
|
|
COPY --from=ipxe-build /src/assets/ipxe/ipxe.efi /src/assets/ipxe/ipxe.efi
|
|
|
|
# Cache cargo registry + target across builds. `cargo zigbuild` runs the
|
|
# native rustc (fast) and links for x86_64-musl with zig — no emulation.
|
|
RUN --mount=type=cache,target=/usr/local/cargo/registry \
|
|
--mount=type=cache,target=/src/target,sharing=locked \
|
|
cargo zigbuild --release --target x86_64-unknown-linux-musl --bin openpxe && \
|
|
cp target/x86_64-unknown-linux-musl/release/openpxe /openpxe && \
|
|
ls -l /openpxe
|
|
|
|
########## runtime ##########
|
|
FROM debian:12-slim AS runtime
|
|
RUN apt-get update \
|
|
&& apt-get install -y --no-install-recommends \
|
|
ca-certificates libcap2-bin tini gosu iproute2 \
|
|
wimtools samba smbclient \
|
|
&& rm -rf /var/lib/apt/lists/* \
|
|
&& useradd --system --uid 10001 --home-dir /var/lib/openpxe --shell /usr/sbin/nologin openpxe \
|
|
&& mkdir -p /var/lib/openpxe/isos /var/lib/openpxe/work /var/lib/openpxe/smb \
|
|
&& chown -R openpxe:openpxe /var/lib/openpxe
|
|
# v0.4.5: the openpxe binary itself is now built against musl and is
|
|
# fully static — no glibc dependency. The runtime stage still ships
|
|
# Debian slim because OpenPXE shells out to the packages below for
|
|
# functionality we deliberately don't reimplement in-process:
|
|
#
|
|
# wimtools - `wimlib-imagex`, used to inject startnet.cmd into boot.wim.
|
|
# samba - `smbd` serves extracted Windows install media on :445 so
|
|
# WinPE can `net use`. Guest read-only, scoped to
|
|
# /var/lib/openpxe/smb. This package provides the SERVER
|
|
# side only; the client CLI is a separate package below.
|
|
# smbclient - v0.4.66: Samba's `smbclient` userspace CLI, used by
|
|
# the Storage tab's SMB shares manager to list and stream
|
|
# ISOs from remote SMB servers without ever mounting them
|
|
# in the kernel. In Debian 12 `smbclient` is NOT pulled
|
|
# in by the `samba` package — they're siblings, not
|
|
# parent/child. v0.4.65 shipped without this line and
|
|
# every "Add share" attempt surfaced
|
|
# `could not exec smbclient: No such file or directory`
|
|
# until this landed.
|
|
# iproute2 - `ip addr` / `ip route` for the auto-detected Network
|
|
# tab fields (NIC name, subnet mask, default gateway).
|
|
# Tiny, always available; we don't pull in netlink crates
|
|
# for this one-shot startup probe.
|
|
# gosu - drops privileges cleanly from root after the entrypoint
|
|
# fixes bind-mount ownership (common OpenShift/Docker UX
|
|
# issue).
|
|
#
|
|
# v0.4.65 dropped `nfs-common` — kernel-mount NFS is gone. The SMB
|
|
# shares replacement uses userspace `smbclient` and needs no kernel
|
|
# helpers.
|
|
#
|
|
# A future "openpxe-static" variant could drop everything except the
|
|
# binary onto distroless once we move the Windows + SMB legs to
|
|
# in-process Rust crates.
|
|
|
|
COPY --from=build /openpxe /usr/local/bin/openpxe
|
|
COPY deploy/docker/entrypoint.sh /usr/local/bin/entrypoint.sh
|
|
RUN chmod +x /usr/local/bin/entrypoint.sh
|
|
|
|
# Grant the binary the ability to bind <1024 ports as a non-root user.
|
|
# This is the only capability OpenPXE needs for proxy-mode DHCP + TFTP + HTTP.
|
|
RUN setcap cap_net_bind_service=+ep /usr/local/bin/openpxe
|
|
|
|
# IMPORTANT: we do NOT `USER openpxe` here. The entrypoint runs as root,
|
|
# chowns the mounted data dirs, then execs the binary via gosu as openpxe.
|
|
# OpenShift ignores USER directives anyway (it injects its own uid), and
|
|
# there entrypoint.sh's non-root branch just execs directly.
|
|
WORKDIR /var/lib/openpxe
|
|
|
|
ENV OPENPXE_ISO_DIR=/var/lib/openpxe/isos \
|
|
OPENPXE_WORK_DIR=/var/lib/openpxe/work \
|
|
OPENPXE_LOG=info,openpxe=info
|
|
|
|
EXPOSE 67/udp 69/udp 4011/udp 80/tcp 445/tcp
|
|
|
|
ENTRYPOINT ["/usr/bin/tini", "--", "/usr/local/bin/entrypoint.sh"]
|