34. iPXE Netboot Assets and Root Integrity¶
Date: 2026-10-17
Status¶
Accepted. Implements issue #5.
Context¶
Bare-metal hosts should boot a flavor over the network without a local disk image. The previous guidance in docs/platforms/baremetal.md section 4 told operators to serve their own kernel and initrd with root=/dev/ram0. No initrd in the image understands that argument as a network root, so the snippet could not boot.
The facts below were measured on 2026-10-17. The raw notes live in the local evidence file imago-5-netroot-facts.md.
- The netboot mechanism is dracut, not casper or live-boot. The Ubuntu 26.04 (resolute) cloud image manifest lists
dracut110-11ubuntu0.1,dracut-coreanddracut-network. It lists onlyinitramfs-tools-core, not theinitramfs-toolsgenerator. Neithercaspernorlive-bootis installed. casper26.04.2 netboots only throughurl=*.isooriso-url=*.iso, which download a whole ISO.live-boot1:20250815 (universe) is an initramfs-tools hook. Itsverify-checksumsoption is a media check that reboots afterwards, so it does not gate the boot.- Neither package applies to a dracut-built initrd.
- dracut 110 boots a root image over HTTP.
dracut.cmdline(7)documentsroot=live:<url>for http, https, ftp, torrent and tftp. It needs thelivenetmodule (fromdracut-network) and thedmsquash-livemodule (fromdracut-core). - The
livenetmodule'scheck()returns 255, so dracut includes it only when it is named explicitly. livenetroot.shfetches the image withcurland verifies no checksum.rd.live.ram=1andrd.live.overlay.size=control the copy to RAM and the size of the writable overlay.- iPXE cannot compare digests. At iPXE master
c51ca76a6e56(2026-10-15),md5sumandsha1sumonly print a digest, andimgverifychecks a CMS signature. iPXE does provide the${cwduri}setting, the directory of the current script, and it resolves relative URIs against the script's own URI. - Size. The rootfs of the daily resolute cloud image (sha256
8800651811af9a85…) unpacks to 3,153,971,370 B.mksquashfs4.7.5 with zstd level 19 packs it into 634,195,968 B (about 605 MiB). The forge's per-asset limit is 2 GiB (ASSET_LIMIT_BYTESintools/ci/release_assets.py). Flavors with GPU stacks are larger than the base image and are measured by the matrix itself. /bootis a separate partition. Read on 2026-10-18 from the same image: partition 13 (1023 MiB, ext4, labelBOOT) holds the kernels and initrds, and/bootincloudimg-rootfsis an empty mount point. Its/etc/fstabmountsLABEL=BOOTat/bootandLABEL=UEFIat/boot/efi, neither withnofail. The first CI dispatch (run 39073997870) failed because the extract read/bootfrom the root partition alone.
Decision¶
- Assets per flavor. Each release flavor in
.github/workflows/release-matrix.ymlships four extra assets: <flavor>-netboot.vmlinuz: the image's own kernel;<flavor>-netboot.initrd: a netboot initrd built by dracut inside the image;<flavor>-root.squashfs: the image's root filesystem, packed with zstd;<flavor>.ipxe: the boot script. All four go intochecksums-<flavor>.sha256, and so into the cosign-signedSHA256SUMS. That signature is what makes the iPXE script a signed asset; iPXE itself does not check it (Context 3).- The netboot initrd. A Packer provisioner builds it in the guest with
dracut --add "livenet dmsquash-live imago-netroot". The kernel the image boots from disk keeps its own initrd unchanged. - Root integrity check. A new dracut module
imago-netrootcloses the gap in Context 2. It readsimago.root-sha256=<64 hex>from the kernel command line and compares it with the sha256 of the fetched squashfs. It runs beforedmsquash-livemounts the image. - A mismatch, a missing value or a malformed value stops the boot with a console message.
- It never falls back to an unchecked root, and no option skips the check.
- Hook point:
livenetroot.sh(dracut 110,modules.d/70livenet/livenetroot.sh:83) ends its bounded fetch loop withexec /sbin/dmsquash-live-root "$root", which70dmsquash-live/module-setup.sh:32installs. The module installs after70dmsquash-live(dracut installs modules inmodules.dglob order), renames that program todmsquash-live-root.origand takes its place, so every fetched image, including one fetched after a retry, passes the check before the original runs. - dracut ignores a failing module
install(), so the provisioner reads the built initrd back withlsinitrdand fails the build when the module or the renamed original is missing. - The iPXE script. The script fetches the kernel and initrd by URIs relative to itself. It passes
root=live:${cwduri}<flavor>-root.squashfs,ip=dhcp,rd.neednet=1,imago.root-sha256=<digest>andfstab=no(Consequences). An operator therefore serves the four assets from one directory and chains the script. The digest is written into the script at build time from the same file that goes intoSHA256SUMS. - Kernel and initrd integrity. iPXE cannot verify these (Context 3). The operator verifies
SHA256SUMSwithcosign verify-bloband checks the four assets against it before serving them.docs/platforms/baremetal.mdstates this step. The squashfs digest is checked again at boot (item 3). - Size limit. The matrix's
release_assets.py sizesstep covers the new assets. An asset at or above 2 GiB fails the flavor build, and a planted oversize asset in the tests proves that. - CI proof. A test boots
base-genericin QEMU through iPXE over a local HTTP server: the iPXE script loads the kernel and initrd, and the initrd fetches the squashfs and reaches a multi-user marker on the serial console. The same run with a wrongimago.root-sha256must stop before the root is mounted. - Documentation.
docs/platforms/baremetal.mdsection 4 replaces theroot=/dev/ram0snippet with the shipped script and the verification step.
Consequences¶
- Each release flavor grows by about one compressed root image (about 605 MiB for the base image) plus a kernel and an initrd.
- A netbooted host runs from RAM with a writable overlay. Its changes are lost at reboot unless the operator adds persistent storage.
- The image's
/etc/fstabmounts theBOOTandUEFIpartitions withoutnofail(Context 5). A netbooted host has neither, so their mount units would faillocal-fs.target. The script therefore passesfstab=no, which makessystemd-fstab-generatorignore every mount and swap entry in/etc/fstabin the booted system (systemd-fstab-generator(8), systemd 262). The root is mounted by dracut fromroot=live:and does not depend on/etc/fstab. The CI boot in item 7 confirms that the host reaches the marker with these entries skipped. - The extract mounts the
BOOTpartition read-only at/bootof the mounted root (tools/ci/release_assets.py attach), andmksquashfs -one-file-systempacks the root without it: the kernel and the initrd ship as their own assets, and/bootstays an empty directory in the squashfs. - A change to the dracut
livenetordmsquash-livecommand line is caught by the CI boot, not by a unit test.
References¶
- Issue #5.
dracut.cmdline(7), dracut 110;modules.d/70livenet(livenetroot.sh,parse-livenet.sh),modules.d/70dmsquash-live(module-setup.sh) anddracut.shat that version.- iPXE
src/hci/commands/digest_cmd.c,src/hci/commands/image_trust_cmd.c,src/core/settings.catc51ca76a6e56. - Ubuntu resolute cloud image manifest;
casper(7)26.04.2; live-boot 1:20250815. - ADR-0033 for the 2 GiB asset limit as applied to Aegis.