Skip to content

Feature Request: PXE/netboot install support (standalone PXE artifacts + network DDI pull) #14

Description

@jeefy

Summary

Bluefin Server is currently installable only from physical/virtual media containing the full installer disk image (bluefin-server-installer-<ver>.raw.zst), where the OS DDI payload is embedded on the bluefin-installer-data partition. This makes it impossible to install over PXE/netboot, which is the standard provisioning path for homelabs and bare-metal fleets (the audience that a distroless, k3s-ready server image serves especially well).

This issue proposes two additive, backwards-compatible changes:

  1. Publish standalone PXE boot artifacts (kernel + installer initrd) as release assets, alongside the existing installer/DDI/UKI artifacts.
  2. Teach the installer to fetch the DDI over the network when directed to by kernel cmdline parameters, falling back to the existing embedded-partition behavior when absent.

With these in place, any PXE server (I'm building this against jeefy/booty, but it applies equally to netboot.xyz, Matchbox, dnsmasq+iPXE, etc.) can install Bluefin Server unattended over the LAN.

Motivation

  • Fleet/homelab provisioning: PXE is how bare-metal Kubernetes nodes get imaged. Bluefin Server + k3s sysext is a natural fit for exactly this workflow, but today each machine needs hand-prepared USB/virtual media.
  • No hardware media dance: Machines without easy USB access (rack servers via BMC, thin clients, mini-PC fleets) install straight from the network.
  • Everything needed already exists in the build — the kernel and initrd are built today, they're just only consumed by ukify and never exported. The DDI is already a standalone release asset. The gap is small.

Current State (as of bce0067d)

Boot artifacts are baked into the UKI only

In elements/oci/bluefin-server-installer.bst (~L252-257), the installer UKI is assembled from a kernel and cpio initrd that are never exported separately:

ukify build \
  --linux="/layer/boot/vmlinuz" \
  --initrd=/installer.cpio.gz \
  --cmdline="systemd.unit=system-install.target console=tty0 console=ttyS0,115200 rw unattended" \
  --output=/layer/boot/efi/EFI/BOOT/BOOTX64.EFI

PXE (via iPXE/GRUB) needs kernel + initrd as separate fetchable files with a caller-controlled cmdline. Chainloading the published .efi UKI directly is not sufficient because its cmdline is immutable — there is no way to inject a DDI URL (see below), and measured-boot-friendly UKIs are the point, so cmdline injection into the UKI would be the wrong direction anyway.

The installer cannot fetch the DDI over the network

The bluefin-sysinstall wrapper (installer.bst, ~L126-178) reads /proc/cmdline only to detect the unattended flag, and sources the DDI exclusively from the bluefin-installer-data GPT partition:

INSTALLER_PART="$(readlink -f /dev/disk/by-partlabel/bluefin-installer-data 2>/dev/null || true)"

Under PXE there is no installer media, therefore no bluefin-installer-data partition, and the install cannot proceed.

Proposal

Part 1: Publish standalone PXE artifacts

Export the two components that already exist at UKI-build time as release assets:

New asset Source in build Purpose
bluefin-server-pxe-vmlinuz-<ver> /layer/boot/vmlinuz PXE kernel
bluefin-server-pxe-initrd-<ver>.cpio.gz /installer.cpio.gz PXE initrd

Changes:

  • elements/oci/bluefin-server-installer.bst: copy both files into the export area alongside the UKI.
  • Justfile: new export-pxe recipe in the installer group.
  • .github/workflows/build.yml: upload both to the GitHub Release and include them in the (already GPG-signed) SHA256SUMS — consumers get artifact verification for free.

No new build work: these are byproducts of the existing installer build. The DDI (bluefin-server-ddi-<ver>.raw.zst) is already published, so no change is needed there.

Part 2: Network DDI pull in the installer

Extend bluefin-sysinstall (or better, the systemd-native machinery around it — see Design Discussion) to honor new kernel cmdline parameters:

Parameter Required Behavior
inst.ddi_url=<http(s) url> for netboot Download the (zstd-compressed) DDI instead of reading the embedded partition
inst.ddi_sha256=<hex> recommended Verify the downloaded DDI before install; abort on mismatch
inst.target_disk=/dev/... optional Explicit target disk, overriding the current first-writable-disk auto-detection

Behavior:

  1. If inst.ddi_url is absent → exactly today's behavior (embedded partition). Zero change for existing media users; offline-first design is preserved.
  2. If present → bring up networking (DHCP), fetch the DDI, verify SHA256 if provided, then feed it into the same systemd-sysinstall/systemd-repart flow the embedded path uses.
  3. Download should stream-decompress (zstd -d) rather than staging both compressed and decompressed copies in tmpfs, to keep RAM requirements sane. Minimum-RAM guidance should be documented either way.
  4. On any fetch/verification failure: fail loudly to console, do not touch the target disk.

The installer initrd would need networking + fetch tooling present in installer-stack.bst if it isn't already (systemd-networkd DHCP; fetch via importctl/systemd-pull or curl — see below).

Part 3: Documentation

  • New docs/skills/pxe-netboot.md: boot flow, cmdline parameter reference, example iPXE stanza, RAM requirements, troubleshooting (serial console, DHCP).
  • Update docs/skills/ddi-installer.md with a PXE section and cross-link.
  • Add PXE/netboot to the AGENTS.md skill routing table.

Design Discussion (maintainer input wanted)

1. Fetch mechanism: importctl / systemd-pull vs. curl.
AGENTS.md mandates the installer stay systemd-sysinstall-native with no custom scripting sprawl. systemd-importd's pull machinery (importctl pull-raw / systemd-pull) natively handles HTTP(S) download, zstd decompression, and SHA256SUMS+GPG verification — which matches the project's existing signed-manifest release format exactly. It seems like the most philosophically aligned option, at the cost of shipping importd in the installer initrd. Alternatively a small curl+zstd path in the existing bluefin-sysinstall wrapper is lighter but less "systemd-native." Which direction would you accept?

2. Cmdline namespace. inst.* follows Anaconda convention and is what PXE users will guess, but this project isn't Anaconda. Happy to use bluefin.install.* or sysinstall.* if preferred — bikeshed away.

3. UKI/measured-boot posture. Separate kernel+initrd PXE artifacts bypass the UKI's single-signed-object property. For a v1 targeting lab/fleet provisioning this seems acceptable (the installed system still boots its signed UKI), but if there's a desire to keep netboot within the UKI model long-term, UEFI HTTP boot of a netboot-variant UKI (with inst.ddi_url discovery via SMBIOS/DHCP instead of cmdline) could be a follow-up. I'd suggest not blocking v1 on it.

4. Per-host configuration (hostname, k3s join tokens, etc.) is explicitly out of scope here. A natural follow-up would be installer-fetched systemd credentials (inst.creds_url= or SMBIOS Type 11 OEM strings), which composes cleanly with this proposal.

Resulting PXE flow (for illustration)

#!ipxe
set base http://pxe-server:8080/data
kernel ${base}/bluefin-server-pxe-vmlinuz systemd.unit=system-install.target console=tty0 console=ttyS0,115200 rw unattended inst.ddi_url=${base}/bluefin-server-ddi.raw.zst inst.ddi_sha256=<sha256>
initrd ${base}/bluefin-server-pxe-initrd.cpio.gz
boot

The PXE server mirrors the three release assets (verified against the signed SHA256SUMS) and serves them over the LAN; the installer fetches the DDI locally, verifies, installs via the unchanged systemd-repart/bootctl path, and reboots into the installed OS.

Compatibility & Constraints

  • Fully backwards compatible: no cmdline params → identical behavior to today. Existing installer media unaffected.
  • Offline-first preserved: the embedded-DDI path remains the default; network pull is strictly opt-in.
  • UEFI-only: unchanged — PXE clients boot via ipxe.efi/UEFI netboot, consistent with the systemd-boot/UKI design.
  • Baseline x86_64: unchanged; artifacts are the same binaries already built.
  • Distroless target OS: untouched — all changes are installer-side.

Metadata

Metadata

Assignees

No one assigned

    Labels

    status/triageNeeds kind/ + area/ labels set. Maintainer: set them then comment /approve.

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions