Skip to content

Blog ·

From a DHCP answer to a finished install

Netboot part two. What happens after the offer lands: the boot script, the answer file, the callback nobody writes, and which vendors publish a checksum.

By Provibr

Getting a machine to accept a netboot offer is the part everyone writes about. We wrote about it too, after losing a week to the destination address of a proxy-DHCP reply.

The part after that gets almost no coverage, and it is where the actual failures live. A machine that boots and then sits on a question forever is worse than one that never booted, because it looks like progress. This is the second half: what has to be true between the offer landing and a login prompt appearing, and which of those things fail silently.

The five things that get served

Once the client has an offer and a boot file, an unattended install is five artefacts. Not one of them is optional, and the last one is the one people leave out.

#WhatServed byFails as
1Boot scriptHTTPMachine drops to a shell
2KernelHTTPBoot aborts
3InitrdHTTPBoot aborts
4Answer fileHTTPInstaller asks a question and waits
5Completion callbackThe installed systemEverything hangs until a deadline

The interesting column is the last one. Items 1 to 3 fail loudly, on screen, in a way somebody will notice. Items 4 and 5 fail as an install that appears to be running and never finishes.

Boot scripts and the placeholder problem

The boot script has to name absolute URLs, and it cannot know them when the profile is written: the address of whatever is serving the files depends on the segment the machine came up on.

So the profile contains a placeholder that is substituted at serve time. Ours is @@PROVIBR_BASE@@, and the shape of it is a deliberate choice rather than a style preference.

The obvious form would be ${BASE}. That is exactly wrong, because iPXE expands ${...} itself. A substitution that fails to happen becomes an empty string rather than an error, and you end up staring at

text
chain http:///vmlinuz

wondering what is wrong with your web server. A delimiter the boot loader does not recognise turns a silent substitution failure into a visible one.

The three kernel command lines that matter, once you strip the branding:

text
# Ubuntu (subiquity)
kernel <kernel> initrd=<initrd> ip=dhcp autoinstall ds=nocloud;s=<dir>/ --- || goto failed

# Debian (preseed)
kernel <kernel> initrd=<initrd> auto=true priority=critical ip=dhcp url=<file> DEBIAN_FRONTEND=text --- quiet || goto failed

# RHEL family (Anaconda)
kernel <kernel> initrd=<initrd> ip=dhcp inst.repo=<tree> inst.ks=<file> inst.text || goto failed

Three installers, three completely different vocabularies for the same idea, and the argument that carries the answer file is spelled differently in each. ds= takes a directory; url= and inst.ks= take a file. Mixing that up gives you an installer that boots and then asks the first question.

The four autoinstall keys that decide whether it finishes

Ubuntu's autoinstall deserves its own paragraph, because on an isolated segment four keys separate an install that completes from one that does not, and none of them is about disks or users:

  • interactive-sections: []: without it, subiquity still stops to ask something.
  • refresh-installer.update: false: without it, it goes looking for a newer installer first.
  • apt.fallback: offline-install: without it, it waits on the archive before giving up.
  • drivers.install: false: same class of problem, same outcome.

The failure in every case is the same: a screen that looks busy. There is no error, because from the installer's point of view nothing has gone wrong.

There is a matching flag for the medium. Ubuntu's netboot initrd unpacks an install medium into a tmpfs and installs from it; if you do not tell it where that medium is, it goes to the internet to find one. On a segment with no uplink, that is the difference between working and four minutes of nothing followed by a busybox prompt.

The callback nobody writes

Item 5 on the list is a single line in a post-install script that fetches a URL, and it is the most commonly missing piece in a home-grown setup.

Without it there is no event that means finished. The installer reboots, the machine comes up, and the system that started the install has no idea, so it waits for whatever deadline it has and then reports a timeout on a machine that installed perfectly.

Two details make this less annoying than it sounds. First, ship the hook in the template rather than documenting it, so somebody writing their own post-install script does not have to know it exists. Second, have a failure hook as well as a success hook: an install that fails at 04:00 and says so is a different support experience from one that goes quiet.

And keep two clocks, not one. The command that starts an install should complete as soon as the listeners are up and the session is accepted, not when the install finishes; ours has a 90 second deadline for that, against an install budget of an hour that is enforced on both sides. A single deadline for both means either a job that sits in the UI for an hour doing nothing, or an install killed for taking as long as installs take.

Which vendors publish a checksum, measured

If the machine fetches a kernel and an initrd over HTTP from a mirror, something should verify what came back. The way to do that is to pin a sha256 in the profile and check it after download, at the place doing the fetching.

That only works if the vendor publishes one. We assumed the answer was "mostly no", wrote that down, and it turned out to be wrong in three of four cases. What is actually published, measured on 2026-09-02:

VendorNetboot kernel + initrdWhere
DebianYesSHA256SUMS next to installer-amd64/current/images/
RHEL familyYes[checksums] in the .treeinfo beside the tree
Ubuntu (live server ISO)YesSHA256SUMS beside the release
Ubuntu (netboot kernel + initrd)NoNo checksum file in the netboot directory, and the tarball is not listed in the release sums either

The cost of that wrong assumption was not an outage; it was worse in a quiet way. Assets that could have been pinned sat marked as unverifiable, and profiles reported themselves as not installable while the vendor was publishing the answer in plain sight. Four of five artefacts across the first two profiles.

The behaviour when a checksum genuinely does not exist is worth designing on purpose. Ours leaves the pin empty, marks the asset as pending, and the profile reports itself as not installable until an administrator pastes a hash in. That is deliberately louder than downloading it anyway. An unverified kernel fetched over HTTP from a mirror is the single highest-value place to attack a provisioning system, and "we could not verify this so we did it anyway" is not a policy.

What "maintained" is allowed to mean

We ship ten base profiles: Ubuntu Server 24.04 and 26.04, Debian 12 and 13, Rocky Linux 9 and 10, AlmaLinux 9 and 10, CentOS Stream 10, Fedora Server 44.

The support status attached to each is computed, not typed. A profile may call itself maintained only when it has a verified run on both firmware paths, BIOS and UEFI. One of the two gives "preview". Neither gives "unmaintained".

Making that a rule rather than a field is worth the small amount of code. A hand-typed label is worth exactly as much as the care of whoever typed it, and this is a label a customer puts a machine on the strength of.

Which means being clear about where that leaves us today. Ubuntu Server 26.04 and Debian 13 have installed themselves fully unattended on an isolated segment, on a BIOS machine. The UEFI path was repaired in the same round, and repairing it is not the same as running it. No stock profile is therefore maintained at the time of writing, and the RHEL-family profiles have not been run on a real machine at all: they were written from vendor documentation and measured against real source files, which is a weaker claim than it looks like from the outside.

Separately, offering netboot as a way to create a new machine is off by default in our platform, pending exactly one thing: a real machine installing itself that way, on real firmware. The path that does exist and is used is reinstallation over an existing machine. A feature flag that is on because the code is written, rather than because the thing has been observed to work, produces an order button that strands halfway at the first real customer.

Small things that cost an afternoon each

  • Validate served file paths. No leading slash, no .., no backslashes, one character class. The thing serving files is inside somebody's network with a filesystem behind it.
  • Redact the audit row. Record which file was served and whether it carried a secret, never the contents. Anyone who needs the content can re-render the profile.
  • Do not let the answer file be a second source of truth about the medium. The kernel argument and the answer file both name where packages come from; deriving both from one setting is the only way they stay equal.
  • Assume nobody is watching the console. Every choice above is a choice about what happens when the install is at minute six of eight and there is no human in the room.

This is the second part of why UEFI netboot fails when BIOS works, and it came out of building network installation into Provibr. The details are in the netboot documentation.

Start with one host and one agent

Create an account, install the agent with a single command and connect your first Proxmox host. The trial runs for 14 days and asks for no payment details.