Démarrer une machine par le réseau
Une machine s’allume. Quatre maillons plus tard, elle s’installe comme quelqu’un l’a décidé — ou, si personne n’a encore rien décidé à son sujet, elle attend dans un menu où un humain peut le faire.
mise sous tension
│
(1) ├── le DHCP dit d'où démarrer ............ À EUX. Deux options, et nous
│ générons l'extrait qui les pose.
▼
(2) ├── TFTP livre un chargeur ............... À NOUS
│ un iPXE adapté à l'architecture, qui enchaîne via ${next-server}
▼
(3) ├── iPXE demande quoi faire .............. À NOUS
│ machine connue → sa propre réponse sans surveillance
│ machine inconnue → le menu
▼
(4) └── les octets arrivent .................. À NOUS
noyau, initrd, l'image elle-même — HTTP avec plages
Le maillon 1 appartient à quelqu’un d’autre et cela ne changera pas. rescriptum ne parle pas DHCP du tout — ni serveur, ni proxy, ni derrière un drapeau. Les sites qui déploient ceci en ont déjà un, et le faire pointer vers un serveur de démarrage est un problème résolu depuis trente ans.
Mise en route
$ export RESCRIPTUM_MEDIA_DIR=/srv/media # les images
$ export RESCRIPTUM_BOOT_DIR=/srv/boot # les chargeurs
$ export RESCRIPTUM_PUBLIC_HOST=192.0.2.10 # ce que nommeront les scripts générés
RESCRIPTUM_BOOT_DIR dit où sont les chargeurs : non définie, il n’y a aucun listener
TFTP et rien sur /boot/…. La nommer démarre TFTP sur 0.0.0.0:69 sauf si vous dites le
contraire.
Le port 69 est privilégié, et c’est le seul port privilégié que ce serveur demandera jamais — sans répondeur DHCP, il n’y a rien après 67 ni 4011. Quatre façons de traiter la question, toutes portables :
$ export RESCRIPTUM_USER=rescriptum # démarrer en root, lier, puis abandonner
$ setcap cap_net_bind_service=+ep rescriptum # ou n'accorder que cette capacité
$ export RESCRIPTUM_TFTP_ADDR=0.0.0.0:6969 # ou le déplacer, si leur DHCP sait le dire
$ export RESCRIPTUM_TFTP_ADDR=off # ou n'avoir aucun listener du tout
off est une valeur, pas une absence — c’est ainsi qu’on dit qu’un autre service de
cette machine livre le chargeur pendant que rescriptum sert le reste de la chaîne. Les
chargeurs restent servis en HTTP sur /boot/… et restent vérifiés par boot check ; seul
le listener disparaît. C’est un contournement de déploiement pour qui le veut, jamais la
façon dont quoi que ce soit est livré ici : c’est rescriptum le serveur TFTP, et une
version qui le couperait par défaut aurait cédé la chose même qu’elle est. Le paquet
Synology ouvre le port 69 avec un setcap.
Un port TFTP qu’on ne peut pas lier n’arrête pas le serveur, et c’est le seul endroit
où la règle « un listener qui ne peut pas se lier est fatal » s’inverse dans ce projet. Le
port 69 est le seul port privilégié de la conception, donc le seul bind qui puisse échouer
pour quelque chose que personne n’a configuré — une capacité qu’une mise à jour a
discrètement perdue, le plus souvent. Les réponses sont le produit ; mourir ici ferait
échouer toutes les installations en cours pour signaler qu’un second port n’a pas pu être
ouvert. Donc il avertit, continue de servir, et boot check sort en non-zéro :
$ rescriptum boot check
BROKEN nothing answers on 0.0.0.0:69 and it cannot be bound either: Permission denied.
Port 69 is privileged: run as root and set RESCRIPTUM_USER to drop afterwards, or grant
the binary cap_net_bind_service with setcap — the server still answers and still serves
media, but a machine sent here by DHCP asks for a loader and gets nothing
Il demande un vrai chargeur au port plutôt que d’essayer de le lier, car lier prouve le contraire de ce qu’on croit : un bind qui réussit signifie que personne n’écoute, et un bind qui échoue ne distingue pas ce serveur d’un autre service qui squatterait le port.
On lie d’abord, on abandonne ensuite, toujours. L’ordre inverse fonctionne en test sous root et échoue au déploiement, à un redémarrage — le seul moment où personne ne regarde.
Les deux lignes de leur serveur DHCP
$ rescriptum boot dhcp-snippet --format dnsmasq
# rescriptum 0.2.0 - boot handoff for 192.0.2.10
# Architecture values are IANA option 93 codes; see docs/guide/boot/dhcp.
# Generated from the same table the TFTP server serves from.
dhcp-match=set:bios,option:client-arch,0
dhcp-match=set:efi64,option:client-arch,7
dhcp-match=set:efi64,option:client-arch,9
dhcp-match=set:efiarm64,option:client-arch,11
…
--format couvre dnsmasq, isc, kea, powershell, pfsense et mikrotik ;
--one-loader produit la forme d’une seule ligne pour un parc d’une seule architecture.
L’extrait et le serveur TFTP sont générés depuis une même table, de sorte que ce que vous collez et ce que le serveur distribue ne peuvent pas diverger. Ce qu’ils peuvent faire, c’est nommer un chargeur que personne n’a encore téléchargé, et cela échoue silencieusement au niveau de la ROM : la machine demande, ne reçoit rien, et s’arrête sans un message sur aucune console. Une commande l’attrape :
$ rescriptum boot check
checking boot assets in /srv/boot
ok ipxe-arm64.efi (1.0M)
MISSING ipxe-undionly.kpxe — every machine the snippet sends here will ask for it,
get nothing, and stop
Son code de sortie est un contrat, comme celui de check. Placez-le au même endroit.
Quatre détails que l’extrait généré traite correctement
Chacun est une façon d’échouer sans bruit sur le réseau de quelqu’un d’autre, et aucun n’est évident :
- Le champ BOOTP
fileet l’option 67. Certaines ROM ne lisent que l’un des deux, et lequel n’est pas prévisible d’après le fournisseur. - Un défaut sans étiquette à la fin. Chaque ligne d’architecture est étiquetée : une ROM qui n’envoie pas d’option 93 ne correspondrait à rien et n’obtiendrait aucun fichier de démarrage.
HTTPClientrenvoyé dans l’option 60 pour les clients UEFI HTTP Boot. Le firmware filtre les offres dessus : une réponse ne portant que l’URL est écartée, en silence, ce qui est indiscernable d’une absence de serveur DHCP.- Un next-server pour ces clients aussi, bien qu’ils récupèrent en HTTP. Sans lui, le
script embarqué du chargeur lit un
${next-server}vide et enchaîne vers nulle part.
::: tip Windows Server
Une stratégie DHCP ne peut pas se conditionner sur l’option 93 — les types de
condition sont la classe fournisseur, la classe utilisateur, la MAC, l’identifiant
client, le FQDN et les informations de relais. L’architecture n’atteint une stratégie
qu’à l’intérieur de la chaîne de l’option 60, donc le PowerShell généré définit des
classes fournisseur sur PXEClient:Arch:00007* et y accroche les stratégies. Même
résultat, mécanisme différent, et c’est exactement le genre de chose qu’on retient à
moitié.
:::
Le chargeur
TFTP livre un fichier, et la règle est écrite dans le code :
TFTP livre le chargeur. Tout ce qui suit passe en HTTP.
À 1468 octets par aller-retour, TFTP déplace environ 1,4 Mo/s sur une milliseconde de latence. Le chargeur fait un mégaoctet : deux secondes. Une image de 1,5 Go prendrait près de vingt minutes, contre quinze secondes en HTTP sur le même câble.
Quel chargeur dépend de ce que le firmware a annoncé :
| Option 93 | Client | Servi |
|---|---|---|
0x0000 | BIOS PXE | ipxe-undionly.kpxe |
0x0007, 0x0009 | UEFI x86-64 | ipxe-x86_64.efi, plus -snp / -snponly |
0x000b | UEFI ARM64 | ipxe-arm64.efi |
0x0010, 0x0013 | UEFI HTTP Boot | les mêmes fichiers, en HTTP, sans TFTP du tout |
| tout le reste | UEFI 32 bits, EBC, U-Boot | refusé, avec la raison |
0x0009 mérite un mot. La RFC 4578 le définissait comme « EFI x86-64 » ; le registre
IANA, réécrit par la RFC 5970, le liste comme « EBC ». Les vrais firmwares x64 envoient
l’un ou l’autre, donc les deux pointent vers x64 — une table produite depuis le seul
registre ne donnerait rien à la moitié d’un parc.
snponly existe parce que la construction UEFI ordinaire ne voit pas toujours la carte
réseau. Toutes les variantes sont servies et la table choisit ; c’est précisément le
savoir qu’un exploitant ne devrait pas avoir à acquérir.
Se les procurer
Chaque version publiée attache rescriptum-boot-assets-<version>.tar.gz. Décompressez-le
là où le serveur peut le lire, nommez le répertoire, et vérifiez-le :
$ tar -xzf rescriptum-boot-assets-0.2.0.tar.gz -C /srv
$ export RESCRIPTUM_BOOT_DIR=/srv/rescriptum-boot-assets-0.2.0
$ rescriptum boot check
Il contient les huit chargeurs, un SHA256SUMS, un ipxe.iso et un ipxe.usb
démarrables pour une machine sans ROM PXE utilisable, et un NOTICE — c’est iPXE, en
GPLv2, construit depuis un commit amont épinglé. C’est un téléchargement séparé, et il
ne fait partie d’aucune archive binaire ni d’aucun .spk, délibérément : des fichiers
séparés servis à côté relèvent de la simple agrégation, et packaging/ipxe/ est l’offre
écrite qui les accompagne.
Pour les construire vous-même à la place — le même script que la release exécute, depuis le même épinglage :
$ packaging/ipxe/build.sh --out /srv/boot
Un chargeur venu d’ailleurs convient aussi, à condition qu’il enchaîne vers ce serveur plutôt que vers Internet — voir ci-dessous pourquoi un chargeur d’origine ne le fait pas.
Ce qui se passe au deuxième démarrage
La première question que tout le monde se pose après une installation réussie, et elle a une vraie réponse.
Une machine qui vient d’être installée redémarre, et si le démarrage réseau est encore premier dans son BIOS, elle revient ici. Ce qui suit est décidé par un seul réglage :
RESCRIPTUM_BOOT_UNCLAIMED | Une machine qu’aucune réponse ne revendique |
|---|---|
menu (défaut) | reçoit le menu, dont la première entrée est le disque local et dont le délai y retombe — quinze secondes, puis le disque |
local | est rendue directement à son firmware, qui passe au périphérique suivant |
Ce sont deux lectures opposées de ce que signifie un fichier de réponse, et le choix appartient au déploiement.
Avec le menu, un fichier qui revendique une machine est la façon de dire laisse celle-ci tranquille — car sans lui elle atterrit dans un menu que quelqu’un pourrait cliquer. C’est juste pendant qu’on provisionne, et c’est la thèse du projet : une machine dont personne n’a rien décidé doit finir là où un humain peut décider.
Avec local, un fichier de réponse veut dire installe celle-ci, et son absence est
l’état sûr. Il n’arrive rien à une machine pour laquelle vous n’avez pas écrit de fichier —
elle démarre sur son disque, à chaque fois, sans menu à cliquer par accident. C’est la
lecture dont un parc en production a besoin, et c’est celle qui passe à l’échelle : le
nombre de machines qu’on veut réinstaller est toujours plus petit que celui des autres.
Le bénéfice, c’est que le démarrage réseau peut rester premier dans le BIOS pour toujours. Réinstaller une machine devient ajouter un fichier, redémarrer — sans console, sans menu de démarrage, sans toucher au matériel. Retirer le fichier est ce qui empêche que cela se reproduise.
$ rescriptum config set RESCRIPTUM_BOOT_UNCLAIMED=local
Dans les deux cas l’identité de la machine part d’abord. Le réglage décide seulement de ce qui arrive quand rien ne l’a revendiquée — pas s’il faut demander.
Installer une machine une fois, et une seule
Une machine revendiquée par une réponse .ipxe s’installe, redémarre, est revendiquée de
nouveau, et se réinstalle — en effaçant son disque à chaque tour. Tous les systèmes de
provisionnement répondent pareil : une machine est armée pour l’installation, et quelque
chose la désarme ensuite.
C’est la machine qui sait. Proxmox appelle un webhook après une installation réussie et avant le redémarrage, avec ses interfaces réseau dans le corps :
[post-installation-webhook]
url = "http://192.0.2.10:8000/installed"
auth-token = "nas:s3cr3t"
$ rescriptum config set RESCRIPTUM_INSTALLED_TOKEN=nas:s3cr3t
C’est tout. La machine termine, elle le dit, et son .ipxe passe de 98fa9b50d810/ à
installed-98fa9b50d810/ — un nom de répertoire qui ne lui correspond plus, le préfixe
faisant partie du nom comparé. Elle démarre sur son disque désormais, et la réarmer consiste
à remettre le document en place.
Le document désarmé va dans un répertoire frère plutôt que de rester dans celui de la
machine, pour que 98fa9b50d810/ continue de vouloir dire « la configuration de cette
machine » et que rien de ce qu’il contient n’ait à se lire comme désactivé.
Pas de jeton, pas d’endpoint — absent plutôt qu’ouvert. Sans lui, /installed est une
demande de réponse ordinaire comme n’importe quel chemin, ce qui permet à une URL de rester
gravable dans une ISO.
Trois choses qu’il ne fait pas, et chacune est délibérée :
- Il ne touche jamais un groupe. Un groupe revendique un rack entier, et une machine qui finit son installation ne doit pas désarmer ses voisines. La recherche ne consulte pas les groupes du tout, plutôt que de les écarter après coup.
- Il ne touche rien d’autre que le
.ipxe. Le.tomlde la machine, à côté dans le même répertoire, est ce que l’installateur a lu pour la construire, et il reste comme trace de la manière. - Il déplace, il ne supprime pas. C’est le seul chemin où quelque chose venu du réseau modifie le jeu de réponses : rien de ce qu’il fait n’est irréversible.
Arriver deux fois n’est pas une erreur — un webhook peut être réessayé, et une machine
installée depuis le menu n’a jamais été revendiquée. Un désarmement qui échoue est
journalisé en still armed, parce que sa conséquence est autrement silencieuse : la
machine se réinstalle au démarrage suivant et rien d’autre ne le dirait.
Toutes les autres familles rapportent aussi
Proxmox est le seul à avoir son propre webhook. La revendication n’est pas propre à
Proxmox — c’est un document .ipxe, qui concerne le chargeur et non le système
d’exploitation — donc toutes les familles ont besoin du même désarmement, et toutes ont un
endroit où lancer une ligne à la fin de leur installation :
curl -fsS -X POST -H "Authorization: Bearer nas:s3cr3t" \
"http://192.0.2.10:8000/installed?mac=$(cat /sys/class/net/*/address | head -1)"
Pas de corps, pas de JSON : la query dit quelle machine, l’en-tête dit qu’elle en a le droit. Où mettre cette ligne :
| Famille | Où |
|---|---|
| Proxmox | [post-installation-webhook] — natif, rien à écrire |
| Debian | d-i preseed/late_command string in-target sh -c '…' |
| Ubuntu | late-commands: dans le document autoinstall |
| RHEL, AlmaLinux, Rocky | la section %post du kickstart |
| SUSE | <scripts><chroot-scripts> du profil AutoYaST |
Prenez la MAC de l’interface qui a démarré, pas la première par ordre alphabétique.
L’exemple ci-dessus prend la première entrée de /sys/class/net, ce qui va sur une machine
à une carte et se trompe sur une machine à quatre — et une mauvaise MAC désarme la mauvaise
machine, ou personne. Sur une machine à plusieurs cartes, nommez l’interface.
L’endpoint accepte les deux formes du justificatif : l’auth-token de Proxmox arrive dans
le corps JSON parce que c’est ce que Proxmox envoie, et un en-tête bearer parce que c’est
ce qu’envoie un script shell. Même secret, même comparaison en temps constant.
Quand tout est juste et que la machine refuse quand même de s’installer
La chaîne peut être parfaite et échouer à la dernière marche, côté machine et non côté serveur. Deux cas réellement rencontrés, tous deux sur un Lenovo vPro :
Intel AMT en adresse statique, sur une carte partagée avec le système. Le dhclient de
l’installateur envoie deux requêtes à onze secondes d’intervalle puis abandonne ; si le
Management Engine tient l’interface avec une configuration statique pendant que l’hôte
demande du DHCP, ces onze secondes passent sans offre et l’installation s’arrête sur
Fetching answer file via HTTP failed: Network is unreachable. Mettez l’AMT en DHCP
aussi. Lancer dhclient -v eno1 à la main depuis le shell de l’installateur réussit
ensuite immédiatement, et c’est ce qui rend le diagnostic déroutant : le réseau va bien,
c’est la temporisation qui ne va pas.
Un port de commutateur qui ne transmet pas tout de suite, pour la même raison et avec le même symptôme — RSTP en convergence, ou un lien encore en négociation après que le noyau a repris la carte des mains d’iPXE. Ce serveur n’y peut rien dans les deux cas : les onze secondes sont la fenêtre de l’installateur, pas la nôtre.
L’installateur ouvre un shell root quand il abandonne, et ce shell est le diagnostic le plus rapide qui soit :
# ip link # l'interface est-elle seulement montée ?
# dhclient -v eno1 # une offre revient-elle quand on la demande à la main ?
# ip addr show eno1
Une adresse qui apparaît là et pas pendant l’installation veut dire que le réseau fonctionne et que la machine a simplement demandé trop tôt.
Comment iPXE finit par parler à nous
La question qu’on ne s’attend pas à devoir trancher. Quel que soit le livreur du chargeur :
- Un
undionly.kpxeordinaire venu d’ipxe.org fait du DHCP, se fait dire de charger iPXE, et se recharge lui-même indéfiniment — la boucle d’enchaînement documentée par iPXE. - Un binaire netboot.xyz d’origine embarque un script qui va droit au menu public
boot.netboot.xyz. Pas de boucle, mais votre menu et vos réponses ne sont jamais consultés.
Les chargeurs que rescriptum distribue portent un script de trois lignes qui enchaîne via
${next-server} — la valeur que l’option 66 a déjà posée, puisque c’est ainsi que le
chargeur est arrivé. Une seule construction générique fonctionne donc dans tous les
déploiements, sans seconde condition dans un fichier de configuration qui appartient à
quelqu’un d’autre.
Le script enchaîne vers le port 8001, et c’est un contrat plutôt qu’une préférence :
il est gravé dans le chargeur avant qu’aucun déploiement n’existe et ne peut lire aucune
configuration. Déplacer RESCRIPTUM_MEDIA_ADDR est permis, et boot check le signale.
Ce qu’une machine voit
L’étape deux met l’identité de la machine dans la chaîne de requête, la seule chose que
DHCP ne peut pas faire — une option DHCP ne peut pas porter ${net0/mac} :
$ rescriptum boot bootstrap
#!ipxe
chain http://192.0.2.10:8000/ipxe/boot?mac=${netX/mac}&uuid=${uuid}\
&serial=${serial:uristring}&asset=${asset:uristring}\
…
|| chain http://192.0.2.10:8001/ipxe/menu
Deux détails y sont porteurs. netX, pas net0 — net0 n’est que la première
interface, donc un serveur démarrant par son second port s’identifierait par le premier,
inutilisé. Et :uristring sur chaque chaîne SMBIOS, parce que ${manufacturer}
s’étend en Dell Inc. avec l’espace et qu’iPXE n’encode rien de lui-même.
Ce || final, c’est tout « un menu est la réponse par défaut » : une machine que quelque
chose réclame reçoit sa propre réponse sans surveillance, et une machine que rien ne
réclame retombe sur le menu. C’est la description de poste de default/, mot pour
mot, appliquée à un autre format.
Le menu
$ rescriptum boot menu
Rendu depuis le catalogue au moment de la requête, et non maintenu comme un fichier : posez une ISO dans le répertoire de médias et elle est dans le menu à la requête suivante.
- « Boot from the local disk » est en premier, et le délai y retombe. Une machine qui démarre en PXE par accident, et que rien ne réclame, finit sur son propre disque au bout de quinze secondes. Elle n’attend jamais un humain qui ne vient pas, et elle n’installe jamais rien. Avec la règle qui veut qu’une machine non réclamée reçoive un menu plutôt qu’une installation, le pire cas d’une erreur sur le périmètre des machines qui atteignent ce serveur est quelques secondes ajoutées à un démarrage.
- Les entrées sont filtrées sur l’architecture du client : une image ARM64 n’est pas proposée à une machine x86 — ce serait une entrée qui démarre le mauvais noyau.
- Une image qu’aucune détection n’a su placer est quand même proposée, comme un CD.
- Les entrées de diagnostic — un shell,
netinfo, et une qui démarre un autre rescriptum — sont ce dont tout serveur de démarrage finit par avoir besoin. La dernière sert à tester un serveur candidat sur site, depuis celui qui tourne, sans toucher au DHCP ni aux chargeurs.
RESCRIPTUM_BOOT_TIMEOUT_SECS (15 par défaut) règle l’attente, et
RESCRIPTUM_BOOT_TITLE la barre de titre. Le logo est récupéré par
console --picture … ||, qui tolère son propre échec : une console série via IPMI
n’a pas de framebuffer, et c’est ainsi que la moitié des installations en datacenter sont
suivies.
Ce qui casse quand ce serveur est arrêté
Cela mérite d’être dit franchement, parce que « serveur de démarrage » sonne critique et ne l’est pas :
| rescriptum arrêté | |
|---|---|
| Adressage DHCP, DNS, routage | inchangés — il ne parle aucun de ces protocoles |
| Machines déjà installées et en service | inchangées |
| Machines qui redémarrent | inchangées — elles démarrent sur disque |
| Une machine qui démarre en PXE par accident | passe au périphérique suivant, comme elle l’aurait fait |
| Démarrer une nouvelle installation | s’arrête |
Rien de ce que rescriptum installe ne dépend de rescriptum ensuite. Le point de réponse est consulté pendant une installation et plus jamais.
Sécurité
Le trafic de démarrage n’est pas authentifié, et forcément — une ROM PXE n’a aucun identifiant, la même nécessité qui gouverne déjà le point de réponse. Les contrôles sont donc structurels, et l’un d’eux peut dire pas vous :
$ export RESCRIPTUM_BOOT_ALLOW=10.0.0.0/8 # partagée par TFTP et les médias
UDP est falsifiable et TFTP est de l’UDP, donc le serveur ne répond jamais à une destination de diffusion ou de multidiffusion — de l’hygiène anti-amplification plutôt que de la politesse —, plafonne les transferts au total et par pair, et journalise chacun d’eux. Il est en lecture seule : une requête d’écriture est refusée comme violation d’accès, car écrire un chargeur en UDP non authentifié serait un moyen de changer ce que démarre chaque machine du segment.
Un VLAN de démarrage est la recommandation honnête et celle qui fonctionne vraiment. Voir Sécurité.
::: tip Secure Boot Nos chargeurs ne sont pas signés, et shim ne charge que ce que la clé de son distributeur a signé — servir un shim à côté d’un iPXE non signé n’est donc pas un support de Secure Boot, c’est un démarrage qui s’arrête sur une erreur de signature. Ce qui fonctionne : désactiver Secure Boot, enrôler une MOK, ou laisser le firmware démarrer en PXE le shim et le GRUB signés de la distribution cible, servis par le listener média comme n’importe quel fichier. Nous ne signons rien, ne retirons rien, et rien ici n’affaiblit une machine dont Secure Boot est actif. :::
Quand leur DHCP est vraiment intouchable
Rien de tout cela ne coûte une ligne de code, et les trois fonctionnent :
- UEFI HTTP Boot avec une URL saisie dans le firmware. Les firmwares serveur récents permettent d’entrer directement une URL de démarrage. La chaîne commence alors sur le listener média, sans aucune option DHCP.
- iPXE depuis un média virtuel IPMI, une clé USB ou la ROM de la carte réseau, portant l’adresse de ce serveur. Une image d’un mégaoctet, montée une fois par machine.
- dnsmasq en mode proxy-DHCP, pour un site qui a vraiment un serveur DHCP qu’il ne peut pas modifier. Il existe, il est mature, il tient en trois lignes de configuration, et ce n’est pas à nous de le réécrire. Le nommer est la réponse honnête.
rescriptum