Files
OpenPXE/deploy/docker/Dockerfile
T
Miles WardandClaude Opus 4.8 1eb41288c3 v0.4.69: PNG boot-menu background (iPXE built from source), NFS AUTH_SYS, FleetDM logo
Three things, headlined by the long-blocked graphical PXE menu.

## 1. Graphical PXE boot background — the iVentoy feature, finally

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 now build our own iPXE from upstream with that thin config delta
(deploy/ipxe/local/{general,console}.h).

The 8-release blocker was cc1 segfaulting when an amd64 gcc ran under
QEMU emulation on the arm64 build host. Fix: a new `ipxe-build`
Dockerfile stage pinned to $BUILDPLATFORM (native arch — no emulation)
that cross-compiles x86_64 iPXE with CROSS_COMPILE=x86_64-linux-gnu-.
The compiler runs native and emits x86_64. Validated end-to-end:
png.o + fbcon.o + pixbuf.o all compile and link (confirmed via the
linked-ELF symbol table, not just strings), ~112s, no segfault. Host
tools needed libc6-dev (dropped by --no-install-recommends; without
it the native host compile falls through to iPXE's freestanding
headers and dies on bits/stdint.h — fixed).

Server side:
- pxe_logo.rs is now a full-screen background compositor: a dark field
  (matching the WebUI theme) with the operator's uploaded logo across
  the top, or — with no upload — a default OpenPXE rainbow disc drawn
  with pure pixel math (no font/SVG deps). Always 1024x768 (iPXE
  doesn't scale; this is the universal mode). WebP/JPEG/GIF/PNG in,
  PNG out (iPXE only eats PNG).
- /branding/pxe-logo always returns a PNG now (default when no logo,
  default when SVG) so the menu always has a background.
- render_menu uses `console --picture … --top 290 || console`: paints
  the background and reserves the logo band on PNG-capable binaries
  (x86_64 UEFI), cleanly falls back to text on the others. The ASCII
  wordmark is GONE.

Only x86_64 UEFI is built from source (host-arch-agnostic cross build);
BIOS/i386/arm64 keep upstream-fetched no-PNG binaries + text fallback.
Modern clients are overwhelmingly x86_64 UEFI.

## 2. NFS AUTH_SYS credential — fixes NFS3ERR_ACCES

v0.4.68's privileged-port fix got past MNT3ERR_ACCES (mount); operators
then hit NFS3ERR_ACCES on READDIR because nfs3_client defaults to
AUTH_NONE and virtually every server exports sec=sys. We now present an
AUTH_UNIX credential (uid 0 / gid 0): no_root_squash servers treat us
as root, root_squash servers map us to anon which reads any
world-readable ISO share. Kept fixed (no UI knob) to stay dead-simple.
Hint updated: a remaining NFS3ERR_ACCES is now a server-side
permission/squash issue, not IP/auth-flavor.

## 3. FleetDM-style full-width logo (top-left)

When a custom logo is uploaded the sidebar header drops the bundled
mark + "OpenPXE" wordmark and lets the logo span the header
(left-aligned, capped 200x50, contain). Rendered server-side via a
brand-class in index_html (has_custom_logo) so there's no flash of the
default. The bundled-default case is unchanged.

Tests: 164 passing. clippy -D warnings clean. iPXE build stage
validated in isolation before the full image build.

Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
2026-05-29 03:11:35 -04:00

179 lines
8.9 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
RUN apt-get update && apt-get install -y --no-install-recommends curl ca-certificates \
&& rm -rf /var/lib/apt/lists/*
WORKDIR /src
COPY scripts/fetch-ipxe.sh scripts/fetch-ipxe.sh
RUN mkdir -p assets/ipxe && bash scripts/fetch-ipxe.sh
########## 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 ##########
FROM rust:${RUST_VERSION}-bookworm AS build
WORKDIR /src
# v0.4.5: build a fully static musl binary (matches Bootimus v0.1.70's
# move). The resulting `/openpxe` has no glibc dependency at all, which:
# - Lets the runtime stage be any Linux distro (we still ship Debian
# slim for the `samba` / `wimtools` / `nfs-common` shellouts, but a
# scratch/distroless variant becomes a one-line swap).
# - Cuts a class of "GLIBC_2.39 not found" surprises when running on
# older RHEL/Rocky hosts that don't match Debian 12's libc version.
# - Sidesteps cross-compilation snags (the binary is its own world).
#
# x86_64-unknown-linux-musl is fully static by default (no extra
# RUSTFLAGS needed). musl-tools provides the linker.
RUN apt-get update \
&& apt-get install -y --no-install-recommends musl-tools \
&& rm -rf /var/lib/apt/lists/* \
&& rustup target add x86_64-unknown-linux-musl
# Copy the whole workspace in one go. We used to do a two-pass "cache-prime
# with stubs, then real build" dance for dep-compile reuse; that turned out
# to silently serve stale stub binaries when cargo's fingerprint didn't
# notice the source swap. A single build is ~1.5 min longer on cold cache
# but guarantees the binary reflects the sources we copied.
# 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
# `cargo 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. The mtime touch is
# belt-and-suspenders: cargo occasionally misses mtime-only changes on
# networked FS; this forces a fingerprint check.
RUN --mount=type=cache,target=/usr/local/cargo/registry \
--mount=type=cache,target=/src/target,sharing=locked \
find crates -name '*.rs' -exec touch {} + && \
cargo build --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"]