rescriptum · an answer written for this machine
EN
On this page

Serving boot media

An answer tells a machine how to install. It says nothing about where the installer comes from — and until now that was somebody else’s web server, holding images that nobody checked against the answers written for them.

With a media directory, the same server does both. A machine’s MAC selects its answer and the image that answer was written for, and the two cannot drift apart because one component decided both.

$ export RESCRIPTUM_MEDIA_DIR=/srv/media

Unset is the whole off switch. Nothing changes for an existing deployment until you set it.

Where the base images live

No installer image is in this project, and none is in a release. An ISO is somebody else’s artefact, it is one to four gigabytes, and it changes on its own schedule — three separate reasons it belongs on your disk rather than in ours. RESCRIPTUM_MEDIA_DIR is where you keep them, and that directory is the archive: what a vendor published, on disk, never modified afterwards.

That last part is a property rather than a promise. Nothing here rewrites an image — preparing one produces a sidecar and an injection applied on the wire (see Preparing a Proxmox image), so the bytes on disk stay exactly what the vendor published and their digest stays checkable against the vendor’s own SHA256SUMS. media list says which entries are the archive and which derive from it.

Getting an image in

Three ways, and the first is the one to reach for.

Pick one from a catalogue

$ rescriptum media sources
SOURCE       NAME              WHAT IT INSTALLS
proxmox-ve   Proxmox VE        the founding case — answers come from a file injected into the image
debian       Debian            netinst images; the answer is a preseed on the kernel command line
ubuntu       Ubuntu LTS        autoinstall, via a cloud-init datasource on the kernel command line
almalinux    AlmaLinux 9       kickstart, named on the kernel command line
rocky        Rocky Linux 9     kickstart, named on the kernel command line

$ rescriptum media sources proxmox-ve
reading https://enterprise.proxmox.com/iso/SHA256SUMS …
Proxmox VE — the founding case — answers come from a file injected into the image
  proxmox-ve_9.2-1.iso
  proxmox-ve_9.2-1-arm64.iso
  proxmox-ve_9.1-1.iso

$ rescriptum media add --from proxmox-ve proxmox-ve_9.2-1.iso

Nothing about a specific image is stored in this server. Each catalogue names the checksum index the vendor already publishes beside its own images, and the names and digests are read from it when you ask — so the list is whatever that vendor has today, and the digest is theirs. A table of URLs baked into a release would be offering last quarter’s images, some of them since deleted.

What that is worth, said plainly. Taking the digest from the same host that serves the image is not a signature check. Over HTTPS it authenticates the vendor’s domain and it catches a truncated download, a corrupt mirror and a file that changed underneath — most of what actually goes wrong — and nothing beyond that. If you want more, use the next section with a digest you obtained yourself.

Let the server fetch it

$ rescriptum media add https://enterprise.proxmox.com/iso/proxmox-ve_8.4-1.iso \
    --sha256 9f86d081884c7d65…
fetching https://enterprise.proxmox.com/iso/proxmox-ve_8.4-1.iso
  with curl, into /srv/media/proxmox-ve_8.4-1.iso.part
######################################################################## 100.0%
verifying 1.5G …
fetched 1.5G via curl, digest verified

It lands on a .part name and is renamed only once the digest matches, so a partial download never becomes a catalogue entry — the catalogue probes whatever it finds, and a truncated ISO probes as an unknown image a machine would then try to boot. An interrupted fetch leaves the .part in place and running the command again resumes it.

--sha256 is required here, because nothing else would check what arrived. Vendors publish a SHA256SUMS beside the image. If you genuinely mean to go without, say --unverified — the point is that skipping it is a deliberate act rather than the default, since this decides what every machine on the network installs.

--as NAME.iso picks the filename when the URL does not imply a usable one.

::: tip There is no TLS in this binary rustls plus a root store is forty-odd crates and over a megabyte on ARMv7, for a job every host already has a tool for. So this runs curl, or wget if that is what is installed, and says plainly when it finds neither — in which case the answer is the one below. :::

Or put it there yourself

Over SMB, over scp, from wherever the ISO already is — the native act on a NAS — and then register it:

$ rescriptum media add /srv/media/pve-8.4.iso --sha256 9f86d081884c7d65…
hashing /srv/media/pve-8.4.iso …
  10% (152.0M of 1.5G)

pve-8.4  9f86d081884c7d659a2feaa0c55ad015a3bf4f1b2b0b822cd15d6c15b0f00a08
  proxmox Proxmox Virtual Environment 8.4-1
  kernel /boot/linux26
  initrd /boot/initrd.img
  wrote /srv/media/pve-8.4.media

--sha256 is optional and worth giving: a mismatch is either a truncated download or the wrong file, and both would install the wrong thing on every machine that asks. Nothing is recorded when it does not match.

Nothing is copied and the image is never modified. What media add writes is the .media sidecar beside it, recording the digest and what the probe found. That is the whole point: hashing 1.5 GB takes the better part of a minute, and the server must never spend a minute inside a request.

An image with no sidecar still appears and is still served — it just has no digest to re-check, and it is probed on sight.

What it can tell about an image

$ rescriptum media list
ID                   FAMILY   ARCH       VERSION                          SIZE  PINNED
pve-8.4              proxmox  x86_64     Proxmox Virtual Environment…     1.5G  9f86d0818
ubuntu-24.04         ubuntu   x86_64     Ubuntu-Server 24.04.1 LTS        2.1G  —
gparted-1.6          unknown  —          GPARTED-LIVE                   420.0M  —

Six families are recognised — Proxmox, Debian, Ubuntu, RHEL and its rebuilds, SUSE and Fedora CoreOS — from a table of markers inside the image. Where a vendor left a version string it is used; the volume identifier is the fallback.

An image nothing recognises is still listed and still served. Not describable is not the same as not usable: it can be sanbooted, or written to a stick, or fetched whole by firmware. What it cannot do is produce a boot stanza, and the server says so rather than guessing.

The endpoints

The media listener is its own socket, on 0.0.0.0:8001 by default.

RouteWhat comes back
GET /the catalogue as text, or JSON with Accept: application/json
GET /<id>/isothe image
GET /<id>/kernelthe kernel, streamed from inside the image
GET /<id>/initrdthe initrd, likewise
GET /<id>/initrd+isothe initrd with the image appended, for old loaders
GET /<id>/file/<path>any file inside the image
GET /health200 OK

Nothing is extracted and nothing is unpacked. A file in an ISO9660 image is one contiguous run of bytes, so serving /pve-8.4/kernel is a seek and a length — the same few kilobytes of work whether the image is 400 MB or 4 GB.

Ranges, ETag, If-Range and HEAD are all answered, because real clients need them: Ubuntu’s casper and Red Hat’s anaconda both range-fetch, and UEFI HTTP Boot sends HEAD before it fetches.

Why it is a second listener

Not preference — three separate reasons, any one of which would be enough:

  • The answer endpoint answers on any path, because the URL is baked into an ISO. A /media/… prefix would carve a reserved space out of one that is deliberately open.
  • RESCRIPTUM_TIMEOUT_SECS is a whole-connection deadline of ten seconds. A 1.5 GB transfer is fifteen seconds on gigabit and two minutes on 100 Mbit, so every download would be killed mid-flight — and it would look like a flaky network, not a setting.
  • A download holds a connection permit for minutes. Sharing that budget with answers means a rollout starves its own installs.

The two have separate budgets, and a test proves it rather than hoping: answers keep succeeding with four transfers in flight.

Booting a machine from it

media ipxe writes the boot stanza for one image:

$ rescriptum media ipxe pve-8.4
#!ipxe
# Proxmox Virtual Environment 8.4-1 — generated by `rescriptum media ipxe pve-8.4`.
# An ordinary answer document: selection, layering and templating all apply.
kernel http://192.0.2.10:8001/pve-8.4/kernel ramdisk_size=16777216 rw quiet initrd=initrd.img \
    splash=silent proxmox-start-auto-installer
initrd http://192.0.2.10:8001/pve-8.4/initrd initrd.img
initrd http://192.0.2.10:8001/pve-8.4/iso proxmox.iso
boot

It prints a script; it does not install one. Save it into the answers directory and it is an ordinary answer document — selected, layered and templated like any other:

$ rescriptum media ipxe pve-8.4 > /srv/answers/groups/rack-a/boot.ipxe

Which is the point. The server does not become clever about booting; it gains a generator, and the composition engine you already have does the rest. A {{ mac }} in the generated answer URL is filled per request from the machine’s own facts.

Each family gets what it actually needs, and they are not alike:

FamilyHow the answer reaches it
Proxmox VEinside the image, via auto-installer-mode.toml — and proxmox-start-auto-installer on the command line to select the automated path
Debianpreseed/url=…
Ubuntuds=nocloud-net;s=…/, from which cloud-init fetches user-data and meta-data
RHEL familyinst.ks=…
SUSEautoyast=…
Fedora CoreOSignition.config.url=…

Proxmox is the odd one out, and it is worth knowing why: it is the only one that carries the answer’s location inside the image rather than on the kernel command line. That is also why it is the only one that needs prepare-iso run over it once — see Preparing installer media.

::: tip Already ran prepare-iso --pxe? That leaves a directory holding vmlinuz, initrd.img and a trimmed ISO. Point RESCRIPTUM_MEDIA_DIR at it and it works as-is — the trimmed image is still recognised as Proxmox, and the kernel and initrd beside it are found and served. :::

Preparing a Proxmox image

Proxmox is the only family that carries the answer’s location inside the image, in /auto-installer-mode.toml. That used to mean running proxmox-auto-install-assistant prepare-iso somewhere else first.

$ rescriptum media prepare pve-8.4
pve-8.4-http  prepared from pve-8.4
  answer   http://192.0.2.10:8000/proxmox
  injects  /auto-installer-mode.toml (198 bytes)
  image    1610612736 bytes (source 1610610688 + 2048 appended)
  wrote    /srv/media/pve-8.4-http.media

Nothing was copied. Serve it as /pve-8.4-http/iso, or write it to a stick with
  rescriptum media export pve-8.4-http /tmp/pve-8.4-http.iso

What that wrote is a sidecar: about two hundred bytes standing in for 1.5 GB. The source is never modified, never copied, and its published digest stays verifiable. The file is injected on the wire, so changing the answer URL later rewrites those two hundred bytes rather than a gigabyte — and both entries appear in media list, backed by one image on disk.

--as NAME picks the derived entry’s name, and --url, --cert-fingerprint and --token say what goes in the file.

For a USB stick

$ rescriptum media export pve-8.4-http /tmp/pve-auto.iso

Materialises exactly what the listener would have served, through the same code path. A stick written any other way would be a second implementation to keep honest, and the difference would only show up on somebody’s desk.

When it refuses

Refusing is a complete answer here, because the fallback is one command on any Debian box and this server is perfectly happy to serve its output:

$ proxmox-auto-install-assistant prepare-iso pve.iso --fetch-from http --url …

It refuses when the image has neither Rock Ridge nor Joliet — the file could then only exist under a mangled 8.3 name like AUTO_INS.TOM;1, and the installer would never find it. It refuses a UDF image, because a Windows ISO keeps its large files only in the UDF tree and patching the ISO9660 tree would produce something that looks right and is not. And it refuses when the root directory has no slack in any of its sectors: relocating the extent would drag in the path tables, which is deliberately not done.

It also refuses to prepare a non-Proxmox image, and names the alternative: every other family takes the URL on the kernel command line, where media ipxe already puts it.

If the source changes underneath

The injection offsets are computed against one image. A source that changed would be patched in the wrong place, producing an image that mounts and is wrong — so the sidecar records the source’s length and the catalogue refuses when it no longer matches:

  problem: pve-8.4-http.media: pve-8.4 was 1610610688 bytes when this was prepared and
  is 1610612736 now. The injection offsets no longer apply — re-run `media prepare`.

Telling the server its own name

The moment it writes URLs into scripts, the server needs a name for itself that a machine can actually reach. 0.0.0.0:8001 is not one.

$ export RESCRIPTUM_PUBLIC_HOST=192.0.2.10

A host, never a URL. No scheme, no port, no path — the server writes URLs for two listeners, and a value carrying one port would pin every generated script to one of them. Each URL appends its own listener’s port. A value with any of the three is refused at startup, naming which.

Left unset, it asks the routing table which of this host’s addresses faces outward — and on a segment with no default route, falls back to the interface list, which on a host with one address is not a guess at all. Either way it says at startup what it settled on, and whether there was anything to settle:

RESCRIPTUM_PUBLIC_HOST is not set — using 192.0.2.10, the only address this host has.
Every generated URL will name it.
warning: RESCRIPTUM_PUBLIC_HOST is not set — derived 192.0.2.10, which is what every
generated URL will name. This host also has 10.8.0.4. If the machines reach it on one of
those instead, set it explicitly.

The second is the one to take seriously: a wrong guess produces a machine that boots, chains, and hangs on an address that does not exist. Naming the alternatives is what makes that answerable from the log itself, rather than by going to look at the host. NAT is the case neither line can catch — the address is genuinely this host’s, and genuinely not the one the machines reach.

Keeping it honest

$ rescriptum media check
checking media in /srv/media
  2 image(s), 1 verified against a recorded digest
  note: ubuntu-24.04 has no recorded digest — `media add` records one
  ok — everything recorded still matches

Its exit code is a contract, like check’s: zero when everything recorded still matches, one when something drifted. deploy.sh keys on it.

An image that changed under a recorded digest is the one failure that silently installs something nobody reviewed, so it is loud:

  FAIL pve-8.4: the image no longer matches what was recorded
       recorded 9f86d081884c7d65…
       found    7d793037a0760186…

What this proves is integrity, not authenticity: what is served is what was registered. Whether what was registered is what the vendor published is a question for the vendor’s own signatures, and --sha256 at media add is where that check belongs.

Who may fetch

Boot traffic is unauthenticated, and necessarily so — a PXE ROM has no credentials, the same necessity that already governs the answer endpoint. The controls are therefore structural: read-only, catalogue-bound, and no filesystem path is ever built from a request. Plus one that can say not you:

$ export RESCRIPTUM_BOOT_ALLOW=10.0.0.0/8,192.168.0.0/16

Unset means anyone who can reach the port, which on a provisioning VLAN is the honest configuration. A boot VLAN is the recommendation that actually works; see Security.

Tuning

VariableDefaultWhat it is for
RESCRIPTUM_MEDIA_ADDR0.0.0.0:8001The listener
RESCRIPTUM_MEDIA_TIMEOUT_SECS600Whole-transfer deadline
RESCRIPTUM_MEDIA_MAX_CONNECTIONS16Concurrent transfers

Sixteen is low on purpose. Each transfer holds its permit for minutes, and the small end of what this has to run on is a NAS with one spinning disk: sixteen transfers at 64 KiB a chunk is about two megabytes of buffers, which is arithmetic that has to hold in 512 MB of RAM.

On a datacenter host, raise it. The answer endpoint has its own budget and is untouched either way.

Updated Edit this page