VMware UEFI / Casper boot fix:
- Linux cmdline for Debian/Ubuntu/Mint/Pop!_OS/elementary now uses the
canonical Casper `iso-url=` option and `ds=nocloud`, matching the
fix Bootimus shipped in v0.1.67. The previous
`boot=casper netboot=url url=… ip=dhcp ---` form booted fine on
bare-metal UEFI but hung at "cloud-init running" on VMware guests
because subiquity / cloud-init can't reach a metadata datasource
through PXE.
Static binary (matches Bootimus v0.1.70):
- Dockerfile build stage now compiles against
x86_64-unknown-linux-musl. The resulting /openpxe has no glibc
dependency at all; the runtime stage still ships Debian slim for the
samba/wimtools/nfs-common shellouts, but a future scratch/distroless
variant is now a one-line swap. Cuts a class of "GLIBC_2.39 not
found" surprises on older RHEL/Rocky hosts.
Forms auth (Sonarr/Radarr-style):
- New AdminStore in openpxe-core: single admin record persisted to
<work_dir>/auth.json, bcrypt-hashed credentials, rotation requires
current password.
- New SessionStore in openpxe-http-api: in-memory UUID-keyed sessions
with 24h sliding TTL, openpxe_session HttpOnly cookie.
- Endpoints: POST /api/setup (first-run), POST /api/login, POST
/api/logout, GET /api/me, PUT /api/me/credentials (rotates and
revokes every other session).
- Auth middleware gates /api/* once the admin is configured;
passes through entirely until then (tests + fresh installs ride this
path). Allowlists PXE-essential paths (/boot.ipxe, /iso/*, /ipxe/*,
/api/queue/join, /api/queue/poll/*) so iPXE clients still work
without a cookie they can't send.
- WebUI: first-run setup card, login card, logout chip in the sidebar
footer, Account card in Settings for rotating creds. Auth screen is
fully styled (centered narrow card, matches Sonarr layout).
SSO config (FleetDM-shaped, storage-only):
- New SsoStore in openpxe-core: { enabled, idp_name, metadata,
metadata_url } persisted to <work_dir>/sso.json with size caps and
URL-scheme validation.
- Endpoints: GET /api/sso, PUT /api/sso. Validation: enabling SSO
without either metadata or metadata_url returns 400.
- WebUI: SSO card in Settings with a URL-vs-XML mode switch and an
inert "Sign in with X" button on the login screen while runtime
flow is pending. Per the brief: no Entity ID field (defaults to the
advertised public_base_url internally when SAML wiring lands).
Quality:
- 132 tests passing (was 106 in v0.4.4): +5 auth unit tests, +5 SSO
unit tests, +7 auth integration tests, +1 SSO integration test, +1
regression guard pinning the new Casper cmdline.
- cargo clippy --workspace --all-targets clean.
Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
125 lines
5.9 KiB
Docker
125 lines
5.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 ##########
|
|
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 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/
|
|
COPY --from=fetch /src/assets/ipxe /src/assets/ipxe
|
|
|
|
# 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 nfs-common \
|
|
&& 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 four 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.
|
|
# nfs-common - `mount.nfs` / `mount.nfs4` for the Storage tab's NFS
|
|
# share manager. Mount requires CAP_SYS_ADMIN; without it
|
|
# mount(2) returns EPERM and the manager surfaces a clear
|
|
# error in the UI.
|
|
# 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).
|
|
# A future "openpxe-static" variant could drop everything except the
|
|
# binary onto distroless once we move the Windows + NFS 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"]
|