rescriptum
L'exploiter
Guide
L'exploiter
L’exploiter
rescriptum est un processus, configuré entièrement par l’environnement, qui n’écrit rien en dehors du store que vous lui indiquez. Bien l’exploiter consiste surtout à décider ce qu’il a le droit de servir, et à qui.
- Déploiement — une unité systemd, un conteneur, ou rien du tout.
- Synology DSM 7 — la cible d’origine : une installation par Package Center qui crée le partage, enregistre le port et démarre au boot.
- Sécurité — les deux jetons, pourquoi ils se comportent différemment, et ce qu’aucun des deux ne protège.
- Capturer les requêtes — enregistrer ce que les machines envoient réellement, et le rejouer hors ligne.
- Le store SQLite — pour un parc administré par outillage plutôt qu’à la main.
- L’API d’administration — gérer les réponses en HTTP, sur son propre listener, avec une écriture qui ne peut pas casser le parc.
- Dépannage — la ligne de log est tout le diagnostic disponible.
- Servir les médias de démarrage — le noyau, l’initrd et l’image de l’installeur, depuis le même serveur.
- Démarrer une machine par le réseau — TFTP, le chargeur, le menu, et les deux lignes de leur DHCP.
La forme d’un déploiement
| Un processus | pas d’arbre de supervision, pas de workers à dimensionner, pas de sidecar |
| Un port par défaut | plus un second, seulement si vous activez l’API d’administration |
| Aucune écriture | en dehors du répertoire de réponses ou de la base, et aucune du tout à moins d’activer l’API d’administration ou la capture |
| Aucun état | entre les requêtes. Un redémarrage ne perd rien |
| Arrêt propre | sur SIGTERM (ce qu’envoie le planificateur DSM) et Ctrl-C |
La configuration se fait par variables d’environnement uniquement. Une valeur numérique nulle ou impossible à parser retombe sur sa valeur par défaut plutôt que de démarrer un serveur qui accepte des connexions sans jamais répondre.
Ce dont il a besoin du réseau
Que l’installateur puisse l’atteindre, et c’est tout. Il n’ouvre aucune connexion sortante, n’a besoin d’aucun DNS, et se moque d’être derrière un NAT.
Le HTTP en clair est le choix normal sur un réseau de provisioning. Si vous avez besoin de TLS — certaines versions d’installateur demandent une empreinte de certificat — terminez-le devant avec nginx ou Caddy et pointez l’ISO dessus. Voir Sécurité.
Déploiement
Déploiement
Le binaire se suffit à lui-même : copiez-le quelque part, donnez-lui un répertoire de réponses, et lancez-le. Tout ce qui suit sert à le faire de façon reproductible.
Pour Synology DSM 7 — qui n’a pas de systemd — voir sa page dédiée.
Un fichier d’environnement
Gardez la configuration dans un fichier lisible par root seul, plutôt que dans une unité ou
sur une ligne de commande. Tout ce qui est sur une ligne de commande est visible par tous
les utilisateurs de la machine via ps, ce qui compte dès qu’un jeton entre en jeu :
# /etc/rescriptum.env (chmod 600, appartenant à root)
RESCRIPTUM_ANSWERS_DIR=/srv/answers
RESCRIPTUM_LISTEN_ADDR=0.0.0.0:8000
RESCRIPTUM_TIMEOUT_SECS=10
# RESCRIPTUM_ANSWER_TOKEN=…
Sous systemd, l’EnvironmentFile= ci-dessous le lit et vous n’avez besoin de rien d’autre.
Ailleurs — et sur DSM 7, qui n’a pas de systemd — pointez
RESCRIPTUM_ENV_FILE sur le même
fichier et le binaire le lit lui-même, en refusant de démarrer s’il n’y arrive pas.
Une unité systemd
# /etc/systemd/system/rescriptum.service
[Unit]
Description=rescriptum — per-machine answer files for unattended installs
After=network-online.target
Wants=network-online.target
[Service]
ExecStart=/usr/local/bin/rescriptum
EnvironmentFile=/etc/rescriptum.env
Restart=on-failure
RestartSec=2
# Il doit lire un répertoire et binder un port. Rien d'autre.
DynamicUser=yes
ReadOnlyPaths=/srv/answers
NoNewPrivileges=yes
PrivateTmp=yes
PrivateDevices=yes
ProtectSystem=strict
ProtectHome=yes
ProtectKernelTunables=yes
ProtectControlGroups=yes
RestrictAddressFamilies=AF_INET AF_INET6
SystemCallFilter=@system-service
[Install]
WantedBy=multi-user.target
$ sudo systemctl enable --now rescriptum
$ curl -s http://localhost:8000/health
OK
Ajustez selon ce que vous activez réellement :
- Store SQLite — la base doit être accessible en écriture, donc
ReadWritePaths=/srvet retirezReadOnlyPaths. - Capture des requêtes —
ReadWritePaths=sur le répertoire de capture. - Un port sous 1024 — ajoutez
AmbientCapabilities=CAP_NET_BIND_SERVICE.
Les logs partent sur stderr, donc journalctl -u rescriptum -f est la vue en direct.
En conteneur
Il n’y a rien à installer, donc l’image est le binaire :
FROM scratch
COPY rescriptum /rescriptum
ENV RESCRIPTUM_ANSWERS_DIR=/answers RESCRIPTUM_LISTEN_ADDR=0.0.0.0:8000
EXPOSE 8000
ENTRYPOINT ["/rescriptum"]
Utilisez le build de la bonne architecture — les builds musl sont liés statiquement, ce qui est ce qui
fait fonctionner FROM scratch. Montez le répertoire de réponses en lecture seule.
Le dimensionner
Les valeurs par défaut sont déjà bonnes aux deux extrémités de la gamme pour laquelle il a été conçu.
| Réglage | Défaut | Le changer quand |
|---|---|---|
RESCRIPTUM_WORKERS | nombre de CPU | vous partagez une petite machine et voulez plafonner les threads |
RESCRIPTUM_MAX_CONNECTIONS | 2048 | vous voyez des 503 pendant une rafale — ou voulez délester plus tôt |
RESCRIPTUM_TIMEOUT_SECS | 10 | les clients sont sur un lien lent, ou vous voulez couper le slowloris plus tôt |
MAX_CONNECTIONS n’est pas une limite de débit. Au-delà du plafond, le serveur écrit un
503 immédiat et ferme plutôt que de mettre en file — un client à qui on dit de réessayer
s’en sort mieux qu’un client garé dans une file qui ne se videra pas.
Un déploiement de 2 000 machines se termine en moins de deux secondes au débit mesuré, donc le dimensionnement est rarement le problème intéressant. Le dépannage l’est généralement.
Remplacer une instance en cours
$ ./deploy.sh admin@nas
$ ./deploy.sh admin@nas /volume1/netboot # un autre répertoire distant
Ce qu’il fait, dans l’ordre :
- Construit pour la cible (
TARGET, par défautarmv7-unknown-linux-gnueabihf). - Vérifie les réponses locales avec
rescriptum checket refuse de continuer si quoi que ce soit échoue — expédier un jeu de réponses cassé est pire que ne pas déployer. - Copie le binaire sous un nom temporaire, puis le renomme en place. Remplacer un binaire en cours d’exécution sur place est la façon dont un fichier à moitié copié se fait exécuter.
- Arrête l’instance en cours, démarre la nouvelle en détaché, et confirme qu’elle est restée en vie.
- Confirme que
/healthrépond par le réseau, pour qu’un problème de pare-feu soit signalé comme tel plutôt que comme un silence mystérieux.
| Environnement | Défaut |
|---|---|
TARGET | armv7-unknown-linux-gnueabihf |
ANSWERS | <répertoire-distant>/answers |
PORT | 8000 |
Il remplace ce qui tourne ; il n’installe pas l’autostart. Sur DSM c’est une
entrée du planificateur de tâches ; avec systemd c’est
systemctl enable.
Mettre à jour
Les réponses sont des données, pas de l’état : rien n’est migré, et un nouveau binaire lit le même répertoire. Remplacez-le et redémarrez.
L’exception est le store SQLite, qui porte une version de schéma. Il n’y en a qu’une pour l’instant, donc rien à migrer ; ce que la version apporte, c’est l’autre sens : un binaire plus ancien refuse d’ouvrir une base écrite par un plus récent plutôt que de la deviner. Voir le store SQLite.
Synology DSM 7
Synology DSM 7
Un Synology DS416j est la raison d’être de ce projet : ARMv7, 512 Mo de RAM, DSM 7, pas de Docker. Un binaire statique sans runtime n’y est pas une préférence esthétique — c’est la seule chose qui rentre.
DSM 7 fait tourner systemd, mais il n’offre aucun endroit supporté pour une unité à vous :
les fichiers de /usr/lib/systemd/system appartiennent à Synology, et une mise à jour de
DSM est libre de les remplacer. La route supportée vers un service, c’est un paquet —
installez-en un et DSM génère pkgctl-rescriptum.service à partir de lui. C’est par là que
cette page commence ; la route par le planificateur de tâches fonctionne
toujours et reste en bas.
Installer le paquet
Téléchargez le .spk de votre modèle depuis la
page des releases :
| Fichier | Pour |
|---|---|
rescriptum-<version>-armv7.spk | DS416j et les autres modèles Marvell armada38x |
rescriptum-<version>-x86_64.spk | tous les modèles Intel |
Un doute ? Demandez à la machine :
$ ssh admin@nas synogetkeyvalue /etc.defaults/synoinfo.conf unique
synology_armada38x_ds416j
Puis Package Center → Installation manuelle, choisissez le fichier, et passez
l’avertissement disant que le paquet n’est pas vérifié par Synology. Cet avertissement ne
vise pas ce paquet en particulier : DSM 7 a supprimé la signature tierce et n’offre plus de
réglage de niveau de confiance, donc tout paquet non-Synology l’affiche. Notre vérification
à nous, c’est la somme SHA-256 publiée à côté du .spk :
$ shasum -a 256 -c rescriptum-0.2.0-1-armv7.spk.sha256
L’assistant pose deux questions — où vivent les réponses et sur quel port écouter — puis le paquet :
- crée un dossier partagé
rescriptumet s’accorde un accès lecture/écriture dessus (si vous en avez déjà un de ce nom, il est conservé et gagne simplement le droit) ; - crée le répertoire
answersdedans à chaque démarrage ; - enregistre le port auprès du pare-feu DSM, pour que le service soit sélectionnable par son nom ;
- lie
rescriptum-clidans/usr/local/bin; - démarre au boot, et s’arrête et redémarre depuis Package Center comme n’importe quoi d’autre.
Ce que le paquet ne fait pas
Cinq choses à savoir avant qu’elles ne vous surprennent.
- Il ne peut pas ouvrir le port 69 tout seul. DSM 7 n’autorise pas un paquet non signé à tourner en root, donc le TFTP vous demande une commande root, une seule fois — voir Le TFTP demande une commande root. Tant qu’elle n’est pas donnée, le serveur avertit, continue de répondre et de servir les images, et seule la livraison du chargeur est coupée.
- Il n’ouvre pas le pare-feu. Enregistrer le port fait apparaître rescriptum par son nom dans l’éditeur de règles au lieu d’un numéro à taper. Si votre pare-feu est actif avec une règle par défaut qui refuse, il faut toujours créer la règle.
- Il ne vous annonce pas les mises à jour. Il n’y a pas de source de paquets à
interroger — le modèle de distribution est : téléchargez le nouveau
.spkdepuis la page des releases et installez-le à la main, pour une mise à jour comme pour une première installation. Surveillez les releases. - Un chemin de réponses personnalisé, les permissions sont à vous. Le paquet tourne sans
privilèges et ne peut pas s’accorder l’accès à un dossier que vous nommez ; si vous
pointez hors du partage
rescriptum, donnez vous-même l’accès en lecture à l’utilisateurrescriptum. - Le droit sur le partage est réappliqué à chaque démarrage. Si vous le restreignez délibérément, vous le retrouverez rétabli au démarrage suivant du paquet.
Où vit quoi
| Quoi | Où | Survit à une mise à jour | Survit à la désinstallation |
|---|---|---|---|
binaire, rescriptum-cli, le fichier d’environnement d’exemple | /var/packages/rescriptum/target/ | non — remplacé | non |
| le fichier d’environnement | /var/packages/rescriptum/etc/rescriptum.env | oui | oui — voir ci-dessous |
| journal, pidfile, captures | /var/packages/rescriptum/var/ | oui | oui |
| les réponses | /var/packages/rescriptum/shares/rescriptum/answers/ | oui | oui — toujours |
| la base SQLite, si vous en utilisez une | à côté des réponses, dans le même partage | oui | oui — toujours |
Utilisez le chemin shares/ plutôt que /volume1/… : c’est un lien symbolique maintenu par
DSM, donc il continue de marcher sur un NAS dont les données ne sont pas sur le volume 1.
La désinstallation laisse le dossier partagé et tout ce qu’il contient tranquilles. C’est à la fois le comportement de DSM et le nôtre : quand le magasin est SQLite, la base est vos réponses.
Elle laisse aussi votre configuration derrière elle, et ça vaut d’être su. etc/ et
var/ sont des liens vers /volume1/@appconf/rescriptum et /volume1/@appdata/rescriptum,
que DSM conserve — le fichier d’environnement reste donc sur le volume après la disparition
du paquet, avec les jetons qu’il contient. Une réinstallation le reprend, ce qui est
généralement ce qu’on veut. Si vous retirez rescriptum pour de bon et qu’il portait un
jeton, supprimez /volume1/@appconf/rescriptum vous-même.
L’application de bureau
Le paquet installe une application sur le bureau DSM — l’icône est dans le menu principal, et le bouton Ouvrir de Package Center y mène. C’est une vraie application DSM, bâtie sur le framework d’interface du bureau : elle est dans le thème DSM et dans la langue de DSM. Le français d’un DSM en français est aussi celui de l’application.
Elle a trois onglets :
- Réglages — chaque variable de configuration, sous forme de formulaire. Chaque champ dit d’où vient sa valeur, et une valeur définie dans l’environnement est affichée mais verrouillée, parce que modifier le fichier n’y changerait rien. Enregistrer écrit le fichier et propose de redémarrer le paquet, le serveur ne lisant sa configuration qu’une fois, au démarrage.
- État — la version, si le paquet tourne, le dossier des réponses et s’il est vraiment
lisible par l’utilisateur du service, et la sortie de
check. - Journal — les dernières lignes du journal des requêtes et de
startup.log.
Trois propriétés valent mieux d’être sues que découvertes :
- Elle édite le fichier, pas le serveur qui tourne. Elle fonctionne donc encore quand le serveur refuse de démarrer, c’est-à-dire précisément quand un panneau de réglages sert à quelque chose. Une modification qui laisserait le serveur incapable de démarrer est refusée avant toute écriture, avec la raison affichée.
- Elle ne vous montre jamais un jeton.
RESCRIPTUM_ANSWER_TOKENetRESCRIPTUM_ADMIN_TOKENapparaissent comme défini ou non défini, et un champ vide veut dire « n’y touche pas », jamais « efface-le ». En saisir un nouveau le remplace. - Elle exige un administrateur DSM. Être connecté à DSM ne suffit pas. Voir sécurité pour pourquoi ce contrôle est toute la porte.
Redémarrer maintenant arrête et redémarre le paquet via DSM lui-même, donc DSM ferme la fenêtre pendant ce temps — rouvrez-la pour voir le nouvel état. L’application le dit à côté du bouton plutôt que de vous laisser la surprise.
Elle demande DSM 7.1 ou plus récent (os_min_ver="7.1-42661"). Elle est bâtie sur le
framework ExtJS de DSM, présent en 7.1.1 comme en 7.2.2 — les deux mesurés. DSM 7.2 embarque
un framework Vue plus récent, et le guide actuel de Synology ne documente que celui-là ; mais
le DS416j qui justifie ce projet plafonne en 7.1.1, où Vue n’existe pas. ExtJS couvre donc
tous les DSM que ce paquet prend en charge plutôt que les seuls récents. Le 7.0 n’est pas
revendiqué : rien n’y a jamais tourné.
Le configurer
L’application ci-dessus est la voie confortable. Tout ce qu’elle fait se fait aussi depuis un shell, et sur une machine où le bureau n’est pas à portée c’est plus rapide :
$ sudo rescriptum-cli config
env file: /var/packages/rescriptum/etc/rescriptum.env
RESCRIPTUM_STORE files default
RESCRIPTUM_ANSWERS_DIR /var/packages/rescriptum/shares/rescriptum/answers file
RESCRIPTUM_LISTEN_ADDR 0.0.0.0:8000 file
…
$ sudo rescriptum-cli config set RESCRIPTUM_LOG=problems
wrote /var/packages/rescriptum/etc/rescriptum.env
config set conserve les commentaires du fichier, décommente un réglage au lieu de le
dupliquer, et refuse une modification qui empêcherait le serveur de démarrer. Son code
de sortie dit si la configuration en est une sur laquelle le serveur démarrerait, ce qui le
rend utilisable depuis un script.
Dessous, c’est le même fichier, et l’éditer à la main reste parfaitement raisonnable :
$ sudo vi /var/packages/rescriptum/etc/rescriptum.env
postinst l’écrit complet à une installation neuve, avec les variables utilisées
décommentées et les autres commentées avec une ligne disant à quoi elles servent.
Arrêtez et redémarrez le paquet depuis Package Center pour appliquer un changement — le
serveur lit le fichier à chaque démarrage.
Une mise à jour n’y touche jamais. L’exemple complet pour la version que vous avez est dans
/var/packages/rescriptum/target/etc/rescriptum.env.example, réécrit à chaque installation
et chaque mise à jour : c’est ainsi qu’une nouvelle variable devient visible sans déranger
votre fichier vivant. Toutes les variables sont dans la
référence de configuration.
Le fichier est en chmod 600 et appartient à l’utilisateur du paquet. C’est là que vivent
RESCRIPTUM_ANSWER_TOKEN et RESCRIPTUM_ADMIN_TOKEN et — étant sous etc/ — c’est un
passager plausible d’une sauvegarde de configuration DSM. Mieux vaut le savoir que le
découvrir.
L’API d’administration est désactivée par défaut et, quand vous l’activez,
devrait rester sur la boucle locale et être atteinte par un tunnel SSH ; elle n’est
délibérément pas enregistrée auprès du pare-feu. Elle exige aussi
RESCRIPTUM_STORE=sqlite et un jeton d’au moins 16 caractères, deux erreurs de
démarrage — donc se tromper se manifeste par un paquet qui ne démarre pas, avec la raison
dans /var/log/packages/rescriptum.log.
Mettre les réponses en place
Déposez les fichiers dans le répertoire answers du dossier partagé rescriptum, via File
Station ou en SSH, exactement comme ailleurs — voir
écrire des réponses. Puis validez-les en tant qu’utilisateur du
paquet :
$ sudo -u rescriptum rescriptum-cli check
Le sudo -u compte. Lancé en root, il réussit quoi que disent les permissions du dossier
partagé, ce qui rend un succès dénué de sens. rescriptum-cli est l’enveloppe fournie par
le paquet : elle nomme le fichier d’environnement, pour que check et render regardent
les réponses de cette machine plutôt que /srv/answers.
Le pare-feu
Panneau de configuration → Sécurité → Pare-feu — créez une règle autorisant rescriptum depuis votre réseau de provisionnement. Le service apparaît par son nom parce que le paquet a enregistré son port.
Le pare-feu de DSM est la première raison pour laquelle une machine « ne contacte jamais le serveur ».
Si vous changez le port plus tard, modifiez RESCRIPTUM_LISTEN_ADDR dans le fichier
d’environnement puis déplacez l’entrée du pare-feu, qui ne suit pas toute seule :
$ sudo /usr/syno/sbin/synopkghelper update rescriptum port-config
Servir les médias d’installation, et le PXE
Le paquet sait aussi servir l’installeur lui-même — noyaux, initrds et images — depuis le NAS qui décide déjà la réponse. C’est éteint jusqu’à ce que vous l’allumiez :
- Décommentez
RESCRIPTUM_MEDIA_DIRdans le fichier d’environnement et redémarrez le paquet. - Posez une ISO dans le dossier
mediadu partagerescriptum, via File Station ou SMB. - Enregistrez-la, pour qu’elle soit vérifiée et analysée une fois plutôt qu’à chaque requête :
$ rescriptum-cli media add /volume1/rescriptum/media/proxmox-ve_8.4-1.iso \
--sha256 9f86d081884c7d65…
$ rescriptum-cli media list
Le listener média est sur le port 8001, déjà déclaré au pare-feu à côté du port de réponse — il reste à créer la règle.
Aucune image n’est livrée avec le paquet, et aucune ne le sera jamais : une ISO est l’artefact de quelqu’un d’autre, elle pèse des gigaoctets, et elle évolue à son rythme. Ce dossier est là où vous les gardez, et c’est l’archive — rien ici ne modifie une image après son arrivée. Préparer une image Proxmox produit un fichier compagnon de deux cents octets et une injection appliquée au fil de l’eau, donc les octets sur disque restent exactement ce que Proxmox a publié et leur somme reste vérifiable contre celle de Proxmox. Voir Servir les médias de démarrage.
Le TFTP demande une commande root
C’est rescriptum le serveur TFTP ici, pas DSM. Le port 69 est privilégié et DSM 7 refuse qu’un paquet non signé tourne en root : le paquet ne peut donc pas s’accorder le port lui-même — mais il n’a pas besoin de root pour s’en servir, seulement qu’on l’y autorise une fois :
$ sudo setcap cap_net_bind_service=+ep /volume1/@appstore/rescriptum/bin/rescriptum
$ sudo synopkg restart rescriptum
Après quoi le paquet ouvre udp/69 sous son propre utilisateur non privilégié
rescriptum, à côté de 8000 et 8001. Les trois sont enregistrés auprès du pare-feu.
Rendez-la durable, car une mise à jour la perd. Installer une nouvelle version remplace
le binaire, et les capacités de fichier appartiennent au fichier — elles partent donc avec
l’ancien. Panneau de configuration → Planificateur de tâches → Créer → Tâche déclenchée
→ Script défini par l’utilisateur, utilisateur root, événement Démarrage, avec la
ligne setcap comme script. Relancez-la depuis cette page après chaque mise à jour, ou
redémarrez.
Rien d’autre ne casse pendant ce temps. Un port TFTP qu’on ne peut pas ouvrir est le
seul écouteur de ce serveur dont l’échec n’est pas fatal, et c’est délibéré : les réponses
sont le produit, et une mise à jour ne doit pas couper les installations d’une flotte pour
signaler qu’un second port n’a pas pu être ouvert. Ce que vous obtenez à la place, c’est un
avertissement dans le journal, une ligne tftp: dans l’onglet État du panneau de
réglages, et :
$ rescriptum-cli 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
Notez qu’il demande un chargeur au port plutôt que d’essayer de l’ouvrir. Ouvrir le port prouve le contraire de ce qu’on croit : une ouverture qui réussit signifie que personne n’écoute.
Les chargeurs sont dans le paquet. Le dossier boot du partage arrive rempli au
premier démarrage, et une mise à jour les rafraîchit — il n’y a pas de second
téléchargement. C’est iPXE, en GPLv2, des fichiers séparés servis à côté plutôt que soudés
dans quoi que ce soit, et le NOTICE posé avec eux nomme le commit amont exact dont ils
sont issus.
$ rescriptum-cli boot check
ok 0.0.0.0:69 handed over ipxe-undionly.kpxe
Les remplacer est possible, mais pas en modifiant ce dossier : une mise à jour réécrit les
noms de fichiers que ce paquet fournit. Pointez plutôt RESCRIPTUM_BOOT_DIR ailleurs, et
rien ici n’y écrira jamais.
Puis faites pointer le DHCP vers ce NAS — Panneau de configuration → Serveur DHCP → PXE si le NAS sert le DHCP, ou votre propre serveur avec ce qu’imprime :
$ rescriptum-cli boot dhcp-snippet --format dnsmasq
Si vous préférez éviter setcap
RESCRIPTUM_TFTP_ADDR accepte un port non privilégié, qui ne demande aucune capacité — il
faut alors le dire à votre serveur DHCP, puisqu’une ROM PXE a 69 gravé dedans et que seul
un premier étage de chaînage peut être redirigé. Ou mettez-le à off et laissez un autre
service de ce NAS livrer le chargeur ; DSM a son propre serveur TFTP sous Panneau de
configuration → Services de fichiers → Avancé, pointé sur le dossier boot du partage. Ce
sont deux contournements pour un déploiement qui les veut, pas ce que le paquet attend.
L’onglet Images
L’application a un quatrième onglet, et c’est là que les images d’installation se gèrent sans toucher à un terminal : ce qui est présent, un catalogue où piocher, et un champ URL pour ce que le catalogue ne propose pas.
Le catalogue n’est pas une liste livrée par ce paquet. Chaque entrée nomme l’index de sommes que l’éditeur publie déjà à côté de ses propres images ; en choisir un lit cet index par le réseau, donc les versions proposées sont celles que l’éditeur a aujourd’hui et l’empreinte vérifiée est la sienne. Cela veut dire aussi que cet onglet a besoin que le NAS atteigne Internet — la seule partie de ce paquet qui en a besoin.
Le téléchargement d’une image de 1,5 Go ne peut pas être tenu par une requête web : l’onglet
le lance et le suit. media add écrit dans un fichier .part à côté de sa destination et
ne le renomme qu’une fois l’empreinte vérifiée — la taille du fichier partiel est donc
l’avancement, et sa disparition est la fin. Fermer la fenêtre n’arrête pas le
téléchargement.
Préparer une image Proxmox est aussi un bouton. Toutes les autres familles prennent l’URL de leur réponse sur la ligne de commande du noyau : il n’y a rien à préparer, et l’onglet le dit plutôt que de proposer une étape sans effet.
Un réglage qui mérite d’être rempli
RESCRIPTUM_PUBLIC_HOST=192.168.1.10
Chaque script généré nomme cette adresse. Laissée vide, elle est déduite en interrogeant la table de routage, et le panneau de réglages affiche ce que cela a donné plutôt qu’une case vide — donc sur un NAS à une seule interface, il n’y a rien à remplir ici.
C’est le NAS à deux interfaces qui mérite la lecture. La déduction en retient une, et le journal de démarrage nomme les autres à côté :
warning: RESCRIPTUM_PUBLIC_HOST is not set — derived 192.168.1.10, which is what every
generated URL will name. This host also has 10.0.0.10. If the machines reach it on one of
those instead, set it explicitly.
Se tromper produit une machine qui démarre, enchaîne, et se bloque sur une adresse qui n’existe pas — long à diagnostiquer depuis la machine.
Le journal
RESCRIPTUM_LOG_FILE pointe le serveur vers
/var/packages/rescriptum/var/rescriptum.log, et le paquet installe une strophe logrotate
pour lui — hebdomadaire, huit conservés, copytruncate (le serveur ouvre son journal une
fois et ne le rouvre jamais, donc tout le reste arrêterait silencieusement la
journalisation). À côté, var/startup.log contient ce que le serveur dit avant de savoir où
vit son journal : une erreur de configuration, un fichier d’environnement mal formé.
Une fois qu’un déploiement devient routinier, RESCRIPTUM_LOG=problems garde les échecs et
laisse tomber les réponses réussies, seule chose à fort volume là-dedans.
Quand ça ne démarre pas
Trois endroits disent pourquoi, dans cet ordre :
$ cat /var/log/packages/rescriptum.log # la sortie des scripts du paquet
$ cat /var/packages/rescriptum/var/startup.log # ce que le serveur a dit avant d'avoir un journal
$ cat /var/packages/rescriptum/var/rescriptum.log
$ systemctl status pkgctl-rescriptum # ce qu'a vu le gestionnaire de services
Une configuration refusée — un jeton d’administration de moins de 16 caractères, un
magasin impossible à ouvrir — est signalée après que le serveur sait où vit son journal :
elle atterrit donc dans rescriptum.log ; un fichier d’environnement mal formé est signalé
avant, et atterrit dans startup.log. Le start du paquet affiche la fin des deux quand le
serveur sort immédiatement, pour que Package Center vous montre la raison et pas seulement
l’échec.
DSM ne relance pas le processus s’il meurt. L’unité qu’il génère est Type=oneshot avec
RemainAfterExit=yes et sans Restart= : un serveur qui sort reste arrêté jusqu’à ce que
vous le redémarriez depuis Package Center. Ce n’est pas une régression — la route par le
planificateur ne le relançait pas non plus — mais mieux vaut le savoir avant de compter
dessus.
Un paquet qui s’installe, démarre, puis répond 404 à tout, c’est presque toujours le
répertoire des réponses : vérifiez avec sudo -u rescriptum rescriptum-cli check. Sur un
NAS avec un dossier partagé chiffré, c’est aussi à cela que ressemble un démarrage avant que
le volume soit déverrouillé — déverrouillez-le et redémarrez le paquet.
Vérifier
$ curl http://IP_DU_NAS:8000/health
OK
Sans le paquet
La route manuelle fonctionne toujours, et c’est le choix honnête si vous préférez ne rien installer du tout.
Utilisez le build armv7-unknown-linux-gnueabihf (ou x86_64-unknown-linux-musl, ou
aarch64-unknown-linux-musl pour un modèle ARM plus récent) de la
page des releases, ou compilez-en un
vous-même (voir construire).
$ scp rescriptum admin@nas:/volume1/netboot/rescriptum
$ ssh admin@nas chmod +x /volume1/netboot/rescriptum
$ ssh admin@nas mkdir -p /volume1/netboot/answers
Si ARMv7 se comporte mal, confirmez la vraie architecture avant de supposer :
$ ssh admin@nas uname -m
armv7l
Prenez le build ARMv7 publié, pas un build musl que vous auriez fait vous-même. Le
binaire armv7 publié est lié à la glibc 2.17, que DSM possède ; un build musl du même code
s’installe, répond à --version, puis meurt dès qu’il veut l’heure. Les noyaux 3.10 de
Synology répondent EINVAL aux appels time64 là où musl 1.2 n’attend qu’ENOSYS pour se
replier — la page de build
porte la mesure. Les builds x86_64 et aarch64 sont en musl statique et ne sont pas
concernés.
$ file rescriptum
ELF 32-bit LSB pie executable, ARM, EABI5 version 1 (SYSV), dynamically linked, ...
RESCRIPTUM_ANSWERS_DIR vaut /srv/answers par défaut, qui n’existe pas sur DSM : il faut
donc la définir explicitement. Le fichier d’environnement ci-dessous est l’endroit le plus
propre pour le faire.
Panneau de configuration → Planificateur de tâches → Créer → Tâche déclenchée → Script défini par l’utilisateur
| Champ | Valeur |
|---|---|
| Événement | Démarrage |
| Utilisateur | root |
| Commande | voir ci-dessous |
Si vous utilisez un jeton, ne le mettez pas dans cette case. Tout ce qui se trouve dans
les arguments d’un processus — et, dans le cas de DSM, dans la définition de la tâche — est
lisible par tous les utilisateurs de la machine via ps. Mettez la configuration dans un
fichier réservé à root et nommez-le :
# /volume1/netboot/rescriptum.env (chmod 600, appartenant à root)
RESCRIPTUM_ANSWERS_DIR=/volume1/netboot/answers
RESCRIPTUM_LOG_FILE=/volume1/netboot/rescriptum.log
RESCRIPTUM_STORE=sqlite
RESCRIPTUM_DB_PATH=/volume1/netboot/answers.db
RESCRIPTUM_ADMIN_ADDR=127.0.0.1:8001
RESCRIPTUM_ADMIN_TOKEN=…
RESCRIPTUM_ANSWER_TOKEN=…
# l'entrée du planificateur de tâches exécute ceci
RESCRIPTUM_ENV_FILE=/volume1/netboot/rescriptum.env exec /volume1/netboot/rescriptum
Préférez ceci au sourcing. La forme plus ancienne —
. /volume1/netboot/rescriptum.env && exec … — fonctionne, et fonctionne toujours, mais
elle échoue silencieusement : oubliez le . du début, tapez une ligne de travers, ou
ratez les permissions, et le shell ne source rien pendant que le serveur démarre sur ses
valeurs par défaut — le répertoire de réponses par défaut, aucun jeton d’administration,
et pas un mot dans le journal. Avec RESCRIPTUM_ENV_FILE, le binaire lit le fichier
lui-même et refuse de démarrer s’il ne peut pas. Il prévient aussi si le fichier est
lisible par quelqu’un d’autre que root, et nomme toute clé qu’il ne reconnaît pas, de sorte
qu’un RESCRIPTUM_ADMIN_TOKENN est attrapé plutôt qu’ignoré en silence.
Les détails du format sont dans la référence de configuration.
Lancez la tâche une fois à la main depuis le planificateur plutôt que d’attendre un reboot pour découvrir qu’elle ne marche pas. Puis ouvrez le port dans le pare-feu par son numéro, et faites tourner le journal vous-même — le serveur ne le fait pas, et rien d’autre non plus.
Remplacer une instance en cours
$ ./deploy.sh admin@nas
Il compile pour ARMv7, vérifie les réponses d’abord, copie le
binaire sous un nom temporaire pour qu’un fichier à moitié copié ne soit jamais exécuté, le
redémarre, et confirme que /health répond. Détails dans
déploiement.
L’entrée du planificateur reste ce qui le démarre après un reboot — deploy.sh ne remplace
que ce qui tourne maintenant. Sur une installation par paquet, passez par Package Center.
Arrêt
Les deux routes envoient SIGTERM, que le serveur gère : il arrête d’accepter et sort. Il
n’y a rien à perdre dans un cas comme dans l’autre.
Ce qu’on peut attendre d’un DS416j
512 Mo et un cœur ARMv7, ce n’est pas grand-chose, et il n’y a pas besoin que ça le soit. Mesuré sur un DS416j faisant tourner le paquet, à travers le réseau local : 3 à 4 ms pour composer et servir une réponse, aller-retour réseau compris, pour une machine revendiquée par un groupe et fusionnée avec son propre fichier. Une connexion coûte des kilooctets plutôt qu’un thread, le listing du répertoire est mis en cache et invalidé par la mtime plutôt que parcouru à chaque requête, et un groupe sans surcharge par machine est rendu une fois au chargement puis servi comme une chaîne préparée.
La chose qui vaut d’être sue : le travail sur le système de fichiers se fait sur un pool de
threads bloquants, parce que read_dir sur un NAS dont le disque dort n’est pas un appel
rapide, et bloquer un worker asynchrone bloquerait toutes les autres connexions qu’il
pilote.
Sécurité
Sécurité
Les documents de réponse portent root-password-hashed et root-ssh-keys. Qui peut les
lire peut se connecter à chaque machine que vous installez ; qui peut les écrire décide de
ces identifiants. C’est tout le modèle de menace, et il mérite d’être dit franchement.
L’endpoint de réponse est ouvert par défaut
Par défaut, quiconque atteint le port peut récupérer une réponse. Ce n’est pas un oubli :
la plupart des installateurs n’ont aucun identifiant à présenter. Un client kickstart qui
va chercher inst.ks=http://… n’a rien à offrir, et le refuser reviendrait à refuser
l’installation.
Le bon contrôle principal, c’est le réseau. Un VLAN de provisioning où ne se trouvent que des machines en cours de démarrage PXE vaut plus que n’importe quel jeton.
RESCRIPTUM_ANSWER_TOKEN
Proxmox peut présenter un identifiant quand son ISO a été préparée pour :
$ proxmox-auto-install-assistant prepare-iso … --answer-auth-token 'une-longue-chaine-aleatoire'
$ export RESCRIPTUM_ANSWER_TOKEN='une-longue-chaine-aleatoire'
L’installateur envoie alors Authorization: Bearer …, et le serveur refuse tout ce qui ne
l’a pas, en comparant en temps constant.
Les échecs ici sont journalisés mais jamais limités en débit. Une baie entière peut se trouver derrière une seule adresse, et l’exclure transformerait un mauvais jeton en déploiement raté. L’API d’administration, à laquelle aucun installateur ne parle, verrouille bel et bien.
Un jeton de moins de 16 caractères est un avertissement au démarrage, pas une erreur — refuser de démarrer laisserait un parc incapable de s’installer.
GET /health reste ouvert dans tous les cas, pour que la supervision ne s’éteigne pas.
Le jeton de l’API d’administration
RESCRIPTUM_ADMIN_TOKEN est une chose différente protégeant une surface différente, et il est
traité en conséquence. L’API d’administration décide du mot de passe root et des clés SSH de
chaque machine installée ensuite, donc :
- elle ne partage jamais le listener de l’endpoint de réponse — elle a son propre
RESCRIPTUM_ADMIN_ADDR; - le serveur refuse de démarrer sans jeton, avec un jeton de moins de 16 caractères, ou au-dessus du store fichiers — des erreurs, pas des avertissements ;
- une adresse qui insiste est exclue : cinq échecs en une minute valent un blocage, doublant à chaque récidive jusqu’à quinze minutes, et le blocage s’applique aussi à un jeton correct venant de cette adresse — sinon deviner jusqu’à tomber juste ne coûterait rien.
Générez-en un vrai. Pas un mot auquel vous avez pensé :
$ openssl rand -hex 24 # ou : head -c 24 /dev/urandom | base64
Détails complets sur la page de l’API d’administration.
Pourquoi une comparaison en temps constant
Un == ordinaire s’arrête dès que deux octets diffèrent, donc un mauvais jeton partageant un
préfixe plus long met mesurablement plus de temps à être rejeté. Cette différence suffit à
récupérer un jeton octet par octet — quelques milliers de requêtes plutôt qu’un nombre
impossible. Comparer tous les octets quoi qu’il arrive supprime le signal.
Sur un réseau, le timing se perd généralement dans la gigue, donc c’est une précaution. Ça coûte cinq lignes.
Ne mettez pas un jeton sur une ligne de commande
Tout ce qui se trouve dans les arguments d’un processus est visible par tous les autres
utilisateurs de la machine via ps. Cela inclut le mettre directement dans une tâche
planifiée DSM. Gardez-le dans un fichier réservé à root :
# /etc/rescriptum.env (chmod 600, appartenant à root)
RESCRIPTUM_STORE=sqlite
RESCRIPTUM_DB_PATH=/srv/answers.db
RESCRIPTUM_ADMIN_ADDR=127.0.0.1:8001
RESCRIPTUM_ADMIN_TOKEN=…
Puis donnez-le au serveur — EnvironmentFile=/etc/rescriptum.env sous systemd, ou
RESCRIPTUM_ENV_FILE=/etc/rescriptum.env
partout ailleurs. La seconde forme fait lire le fichier par le binaire : un fichier
illisible devient donc une erreur de démarrage plutôt qu’un serveur tournant discrètement
sans le jeton que vous croyiez avoir posé. Le fichier n’est jamais découvert tout seul — il
n’y a pas de ./.env, délibérément : ce processus tourne en root, et un fichier ramassé
dans le répertoire courant serait un moyen de donner le jeton admin à quelqu’un.
Ce que le serveur refuse de lui-même
| Traversée de chemin | un chemin de système de fichiers n’est jamais construit à partir de données de requête. Seules les entrées directes du répertoire de réponses sont lues. Les identifiants arrivant à l’API d’administration n’acceptent que lettres, chiffres et - _ . :, parce qu’export les retransforme en noms de fichiers |
| Corps surdimensionnés | un Content-Length invraisemblable est refusé depuis l’en-tête, avant toute lecture ; le corps est plafonné à 1 Mo quoi qu’il arrive |
| Clients lents | un délai de lecture des en-têtes et une échéance sur la connexion entière, pour qu’un client qui promet un corps sans l’envoyer ne puisse pas garer une connexion |
| Rafales | au-delà de RESCRIPTUM_MAX_CONNECTIONS en vol, un 503 immédiat et fermeture, plutôt qu’une mise en file jusqu’à l’épuisement mémoire |
| Entrées malformées | un échec de parsing est une réponse d’erreur et une ligne de log, jamais un panic emportant une connexion — ou un serveur — en pleine installation |
TLS
Le serveur parle HTTP en clair. Sur un réseau de provisioning de confiance c’est normalement très bien, et c’est ce qui garde le binaire petit et sans dépendance.
Si vous avez besoin de TLS — certaines versions d’installateur veulent une empreinte de certificat lors d’une récupération en HTTPS — terminez-le devant avec nginx ou Caddy et pointez l’ISO dessus. L’endpoint de réponse se moque de ce qu’il y a en amont.
L’API d’administration est le seul endroit où cela compte par défaut : elle parle aussi HTTP en clair, donc le jeton traverse le réseau en clair. Sur la boucle locale c’est sans objet. Ailleurs, mettez un proxy terminant TLS devant.
L’application de bureau
Sur Synology uniquement, et uniquement là — l’application DSM
fait partie du paquet, pas du serveur. Son backend est un CGI que DSM sert depuis
/webman/3rdparty/rescriptum/, et deux choses concernant ce chemin décident de tout son
modèle de sécurité. Les deux ont été mesurées sur une machine DSM 7.2.2 plutôt que lues
dans un guide, qui n’en mentionne aucune :
- Un CGI y tourne sous le propriétaire du script. DSM attribue l’arborescence d’un
paquet à l’utilisateur du paquet : le backend tourne donc en tant que
rescriptum, la même identité qui possède le fichier d’environnement en0600et le journal. C’est ce qui lui permet d’éditer la configuration et de lire le journal pendant que le serveur est arrêté, c’est-à-dire précisément quand un panneau de réglages sert à quelque chose. Ce n’est pas root, et il ne peut devenir personne : il n’a aucun droit de démarrer ou d’arrêter le paquet, et c’est pourquoi le redémarrage passe par l’API de DSM avec la session de l’administrateur. (Un script resté possédé par root, lui, tourne bien en root là-bas. Bon à savoir, et à ne jamais faire.) - DSM n’authentifie pas ce chemin. Une requête non authentifiée atteint le script et reçoit une réponse. DSM protège ses propres pages ; celles d’un paquet regardent le paquet.
Mis ensemble : les contrôles à l’intérieur du script sont la seule chose devant lui. Il en fait donc trois, dans cet ordre, avant de toucher à quoi que ce soit.
- Une session DSM. Il exécute l’
authenticate.cgide DSM, qui affiche le nom de l’utilisateur connecté et n’affiche rien du tout s’il n’y a pas de session. - Un administrateur. Être connecté ne suffit pas ; l’utilisateur doit appartenir à
administrators. Moins que cela laisserait n’importe quel compte du NAS fixer le mot de passe root de chaque machine qu’il installe. - L’intention, pour une écriture. Une écriture doit porter un en-tête que l’application
envoie et qu’un formulaire d’un autre site ne peut pas : un navigateur n’envoie pas un
en-tête inventé en cross-origin sans un préalable (preflight), et ce script n’y répond
pas. Le
SynoTokende DSM est envoyé en plus, ce qui garde l’application fonctionnelle avec la protection contre la falsification de requête inter-sites activée.
check-spk.sh vérifie que les deux premiers sont toujours dans le script, et
lifecycle-test.sh le pilote avec un authentificateur bouchonné pour prouver que les trois
refusent réellement. Ils ont été vus échouer : retirer le contrôle de session fait passer
quatre verts au rouge.
L’application ne reçoit jamais de jeton. RESCRIPTUM_ANSWER_TOKEN et
RESCRIPTUM_ADMIN_TOKEN lui parviennent comme défini ou non défini, et rien de plus —
la commande qu’elle appelle refuse d’afficher un identifiant, quoi qu’on lui demande.
Connu et accepté
- La limitation par adresse n’arrête pas un attaquant disposant de nombreuses adresses. C’est la longueur du jeton d’administration qui rend la devinette sans espoir — d’où le plancher de 16 caractères.
- L’endpoint de réponse n’est pas limité en débit du tout, délibérément, pour la raison ci-dessus.
- Binder l’API d’administration au-delà de la boucle locale est votre choix, et le
serveur le dit dans le log quand vous le faites.
127.0.0.1plus un tunnel SSH est le défaut sûr.
Capturer les requêtes
Capturer les requêtes
L’essentiel de ce que rescriptum sait des installateurs vient de leur documentation. Tant qu’un vrai installateur ne lui a pas parlé, c’est une affirmation plutôt qu’un fait — et quand un déploiement dérape, « qu’est-ce que node07 a réellement envoyé ? » est généralement la seule question qui vaille.
$ export RESCRIPTUM_CAPTURE_DIR=/var/log/rescriptum-captures
Désactivé sauf si défini.
Ce qu’il écrit
Deux fichiers par requête :
20260824T084337Z-10.0.0.42-0000.body le corps, verbatim
20260824T084337Z-10.0.0.42-0000.meta qui a demandé, et ce qu'il a reçu
time: 2026-08-24T08:43:37Z
peer: 10.0.0.42:51234
request: POST /proxmox/answer
body-bytes: 1876
outcome: 200 format=toml machine=98fa9b50d810 group=rack-a
Le .body est octet pour octet ce qui est arrivé, donc il se rejoue sans modification.
Le nom de fichier porte l’horodatage, l’adresse du pair (assainie — les deux-points d’un pair
IPv6 n’ont rien à faire dans un nom de fichier) et un numéro de séquence, pour que deux
requêtes dans la même seconde n’entrent pas en collision.
En rejouer une
$ rescriptum render --body /var/log/rescriptum-captures/20260824T084337Z-10.0.0.42-0000.body
Cela résout exactement comme le serveur l’a fait, hors ligne, sans aucune machine — ce qui rend une mauvaise réponse débogable à votre bureau plutôt que devant une baie.
C’est aussi la meilleure façon de construire des sélecteurs contre un format de corps que
vous n’avez jamais vu : capturez une vraie requête, puis itérez avec render --body jusqu’à
ce qu’elle se résolve comme vous le vouliez.
Les limites, et pourquoi
- Plafonné à 1000 captures. Un serveur de provisioning qui remplit son propre disque est pire qu’un qui ne capture rien. En atteignant le plafond, il le signale une fois et arrête d’écrire. Le compte porte sur les captures, pas sur les fichiers, et il survit à un redémarrage : le serveur compte ce qui se trouve déjà dans le répertoire avant d’écrire.
- Rien n’est jamais supprimé. Faire tourner ou vider le répertoire vous incombe ; le serveur compte ce qui s’y trouve déjà au démarrage pour qu’un redémarrage ne dépasse pas le plafond.
- Un échec de capture ne fait jamais échouer une requête. Il est journalisé, et l’installation continue. Perdre un diagnostic ne vaut pas de perdre une installation.
Avant d’en joindre une à un rapport de bug
Un corps capturé est un inventaire matériel : adresses MAC, numéros de série de disques, DMI.
Le .meta dit quelle réponse a été reçue. Ni l’un ni l’autre ne contient vos hachages de mot
de passe — mais la réponse, si. Nettoyez donc tout ce que vous collez à côté.
Voir aussi
- Dépannage — lire le log, et les causes habituelles.
- Validation —
rendersous ses autres formes.
Le store SQLite
Le store SQLite
Un répertoire de fichiers est le défaut, et c’est la bonne réponse pour une poignée de machines : greppable, diffable, dans git si vous voulez, sans base à faire tourner, sauvegarder ou migrer.
Pour un parc administré par outillage plutôt qu’à la main, les mêmes réponses peuvent vivre dans une base SQLite. Elle est compilée dans le binaire, donc il n’y a toujours rien à installer.
$ export RESCRIPTUM_STORE=sqlite RESCRIPTUM_DB_PATH=/srv/answers.db
$ rescriptum import /srv/answers # faire passer les fichiers
$ rescriptum check # les mêmes vérifications, sur la base
$ rescriptum # servir depuis elle
Pourquoi vous le feriez
- L’API d’administration en a besoin. Gérer les réponses en HTTP exige la base ; au-dessus de fichiers il y aurait deux façons de changer la même configuration — à la main et par le réseau — en concurrence l’une avec l’autre.
- Les écritures concurrentes sont sûres. Mode WAL, donc une écriture administrative ne bloque jamais une installation en cours.
- Un seul fichier à sauvegarder, et il se déplace atomiquement.
Pourquoi peut-être pas
- Un répertoire est lisible.
git log answers/répond à « qui a changé cette baie et pourquoi » ; une base non, à moins que votre outillage ne l’enregistre. - Un fichier s’édite avec n’importe quoi.
vi,scp, un Makefile. - C’est 1,2 Mo de binaire — 2,4 Mo avec SQLite contre 1,3 Mo sans, sur ARMv7. Construisez
avec
cargo build --no-default-featuressi cela compte et que vous n’en avez pas besoin.
Le comportement est identique
Correspondance, groupes, extends, fusion, templating, render, check — tout cela vit
au-dessus du store, qui est délibérément mince : il rend le texte brut des documents et un
jeton de version bon marché, et ne décide de rien.
Ce n’est pas affirmé mais imposé. tests/stores.rs fait tourner chaque cas de comportement
deux fois, une fois par store, et exige le résultat identique. Un nouveau comportement a sa
place dans cette suite, pas dans un test propre à un store.
Passer de l’un à l’autre
$ rescriptum import /srv/answers # répertoire → store configuré
$ rescriptum export /tmp/backup # store configuré → répertoire
$ RESCRIPTUM_STORE=sqlite RESCRIPTUM_DB_PATH=/srv/answers.db rescriptum import examples
copying files:examples -> sqlite:/srv/answers.db
10 group(s), 8 machine(s)
ok — now run `check` against the target
L’aller-retour est identique octet pour octet. Importez un répertoire, réexportez-le, et
diff -r ne signale rien — commentaires, mise en forme et tout le reste. C’est ce qui rend
la base sûre à adopter et sûre à quitter, et cela vaut la peine de rester vrai.
Les deux sens lancent check sur votre initiative plutôt qu’automatiquement ; la sortie
ci-dessus vous le dit.
Versions de schéma
La base porte une version de schéma (user_version). Il n’y en a qu’une pour l’instant, et
rien n’a été publié sous une plus ancienne : il n’y a donc rien à migrer.
Ce à quoi sert la version, c’est l’autre sens : un binaire plus ancien refuse d’ouvrir une base écrite par un plus récent plutôt que de deviner ce qui a changé.
database schema is version 2, this binary understands 1
Un retour arrière au-delà d’un futur changement de schéma a donc besoin de l’export d’avant
la mise à jour, ou d’un binaire assez récent pour lire la base. Gardez un export sous la
main quand vous en franchissez un.
Notes d’exploitation
version()est un atomique en mémoire, pas une requête, parce qu’il est appelé à chaque requête HTTP. Un changement fait par un autre processus est rattrapé par le filet de rechargement d’une seconde.- Le fichier de base et ses compagnons
-wal/-shmdoivent tous être accessibles en écriture, et appartiennent tous à la même sauvegarde. RESCRIPTUM_DB_PATHvaut par défaut/srv/answers.db, voisin du répertoire de réponses par défaut. La base contient le même contenu curé, pas de l’état d’exécution : elle a sa place dans le même arbre.- Le répertoire parent est créé s’il n’existe pas.
Voir aussi
- L’API d’administration — la raison pour laquelle la plupart des gens activent ceci.
- Comment les stores sont construits — le trait, et pourquoi il est mince.
L'API d'administration
L’API d’administration
Avec RESCRIPTUM_STORE=sqlite, les réponses peuvent être gérées en HTTP plutôt qu’en
éditant des fichiers. Elle est désactivée sauf si vous la configurez, et elle tourne sur
son propre listener.
$ export RESCRIPTUM_STORE=sqlite RESCRIPTUM_DB_PATH=/srv/answers.db
$ export RESCRIPTUM_ADMIN_ADDR=127.0.0.1:8001
$ export RESCRIPTUM_ADMIN_TOKEN=$(openssl rand -hex 24)
$ rescriptum
2026-08-24T08:52:30Z - admin API listening on 127.0.0.1:8001
2026-08-24T08:52:30Z - rescriptum 0.1.0 listening on 0.0.0.0:8000 — store=sqlite:/srv/answers.db …
Trois propriétés porteuses
1. Son propre listener. L’endpoint de réponse n’est pas authentifié par nécessité — l’installateur n’a aucun identifiant à offrir. Cette API décide du mot de passe root et des clés SSH de chaque machine installée ensuite. Elle ne partage jamais ce port.
2. SQLite uniquement. Au-dessus d’un répertoire de fichiers il y aurait deux façons de changer la même configuration, à la main et par le réseau, en concurrence.
3. Une écriture ne peut jamais laisser le jeu de réponses cassé. Chaque écriture prend un
instantané des problèmes courants, s’applique, puis compare. Tout ce qui est nouvellement
cassé est annulé et répondu 409.
Le serveur refuse de démarrer — en erreur, pas en avertissement — si vous pointez l’API d’administration sur le store fichiers, omettez le jeton, ou définissez un jeton de moins de 16 caractères.
Endpoints
| Requête | Rôle |
|---|---|
GET /machines, GET /groups | lister les identifiants |
GET /machines/{id}, GET /groups/{name}, GET /default | le document stocké, tel qu’écrit — commentaires et mise en forme intacts |
PUT /machines/{id}, PUT /groups/{name}, PUT /default | stocker un document (le corps est le document) |
DELETE /machines/{id}, DELETE /groups/{name}, DELETE /default | en supprimer un |
GET /resolve/{id} | la réponse fusionnée que cette machine recevrait |
GET /check | les problèmes actuels, le même jeu que la sous-commande check |
GET /health | vivacité — le seul endpoint sans jeton, et jamais bloqué |
Chaque endpoint nommant un document prend ?format= — l’extension du document. Il vaut
toml par défaut, ce que ce serveur servait à ses débuts :
$ curl -H "$AUTH" -X PUT --data-binary @base.preseed \
'http://127.0.0.1:8001/groups/base?format=preseed'
Comme la clé d’un document est (identifiant, format), un identifiant
apparaît dans GET /machines une fois par format dans lequel il existe — une machine qui
est à la fois un nœud Proxmox et un nœud Debian est listée deux fois.
Exemples
$ AUTH="Authorization: Bearer $RESCRIPTUM_ADMIN_TOKEN"
$ curl -s -H "$AUTH" http://127.0.0.1:8001/groups
{"group":["base","example-rack","rhel-compute","ubuntu-web"]}
$ curl -s -H "$AUTH" -X PUT --data-binary @rack-a.toml \
http://127.0.0.1:8001/groups/rack-a
{"status":"stored","problems":[]}
$ curl -s -H "$AUTH" http://127.0.0.1:8001/resolve/98:fa:9b:50:d8:10
[global]
country = "fr"
keyboard = "fr"
…
GET /resolve renvoie aussi l’en-tête de réponse X-Answer-Source, portant la même
description que la ligne de log :
x-answer-source: format=toml machine=98fa9b50d810 group=example-rack
Répéter une vraie requête
GET /resolve accepte les mêmes étiquettes que porterait une vraie requête, ce qui permet de
répéter une URL particulière — la différence entre /user-data et /meta-data, par exemple :
$ curl -s -H "$AUTH" 'http://127.0.0.1:8001/resolve?path=/rhel/ks&serial=7ABC123'
Quand une query string est présente, l’identifiant du chemin est ignoré — les faits
viennent de la query seule. Donc GET /resolve/98:fa:9b:50:d8:10?format=toml ne résout
rien, parce que format=toml n’est pas une identité. Utilisez la forme sans query, ou
mettez l’identité dans la query : ?mac=98:fa:9b:50:d8:10.
Elle ne vous laissera pas casser le parc
Chaque écriture est vérifiée après application. Si elle a introduit un problème — un cycle
entre groupes, un document référençant un groupe qui n’existe plus — l’écriture est
annulée et vous recevez un 409 disant ce que vous avez cassé :
$ curl -s -H "$AUTH" -X DELETE 'http://127.0.0.1:8001/groups/base?format=preseed'
{"error":"refused: this would break the answer set (rolled back)",
"problems":["machine \"98fa9b50d810\": extends unknown group \"base\""]}
Deux choses découlent de ce fonctionnement :
- Une écriture réussie signale quand même les problèmes préexistants, dans le tableau
problems. Une réponse propre n’implique jamais que tout le jeu est sain — seulement que vous ne l’avez pas aggravé. - C’est pourquoi un
extendsde machine pointant sur un groupe manquant est détecté au chargement plutôt qu’au moment où cette machine demande. Le garde-fou ne peut attraper que ce que le rapport de problèmes connaît.
Les documents malformés sont également refusés à l’écriture, plutôt que de devenir un 500
la prochaine fois qu’une machine les demande :
$ curl -s -H "$AUTH" -X PUT --data-binary 'x = = 1' http://127.0.0.1:8001/machines/aa-bb-cc-dd-ee-01
{"error":"document: invalid TOML: TOML parse error at line 1, column 5 …"}
Identifiants
Lettres, chiffres et - _ . : uniquement. Ils deviennent des noms de répertoires sous
export et dans le store fichiers, donc tout ce qui pourrait traverser un répertoire est
rejeté — à la frontière de l’API et dans les deux stores. groups et default sont
réservés comme identifiants de machine pour la même raison : ce sont les répertoires que
l’agencement garde pour lui, et une base qui en accepterait un exporterait vers un répertoire
incapable de le contenir.
Codes de statut
| Code | Signifie |
|---|---|
200 | fait |
400 | document malformé, identifiant invalide, ou corps non-UTF-8 |
401 | jeton manquant ou faux |
404 | document ou endpoint inexistant ; rien ne se résout pour cet identifiant |
409 | l’écriture aurait cassé le jeu de réponses (annulée), ou un resolve qui n’a pas pu rendre |
413 | document de plus de 256 Ko |
429 | cette adresse est bloquée après des échecs d’authentification répétés |
500 | le store n’a pas pu être lu ou écrit |
Veiller sur le jeton
Le jeton constitue toute l’authentification, et ce qu’il protège mérite d’être dit
franchement : les documents de réponse portent root-password-hashed et root-ssh-keys,
donc quiconque peut écrire sur cette API décide des identifiants root de chaque machine que
vous installerez ensuite.
Générez-en un vrai — pas un mot auquel vous avez pensé :
$ openssl rand -hex 24 # ou : head -c 24 /dev/urandom | base64
Ne le mettez pas sur une ligne de commande. Tout ce qui se trouve dans les arguments d’un
processus est visible par tous les autres utilisateurs via ps, ce qui inclut le mettre
directement dans une tâche planifiée DSM. Gardez-le dans un fichier réservé à root et
sourcez-le — voir
Sécurité.
Ce que le serveur fait de son côté :
- Compare le jeton en temps constant, pour qu’il ne puisse pas être récupéré octet par octet par qui chronomètre les réponses.
- Exclut une adresse qui insiste. Cinq échecs en une minute valent un blocage, doublant à chaque récidive jusqu’à un maximum de quinze minutes, et chaque tentative est journalisée. Le blocage s’applique aussi à un jeton correct venant de cette adresse — sinon deviner jusqu’à tomber juste ne coûterait rien.
- Borne sa propre comptabilité à 4096 adresses suivies, pour que le garde-fou ne puisse pas lui-même être transformé en fuite de mémoire.
- Laisse
GET /healthnon authentifié et non bloqué, pour que la supervision ne s’éteigne pas pendant une attaque.
2026-08-24T08:52:32Z - admin: 10.0.0.9 failed authentication 5 times — blocked for 60s
Deux limites à prévoir
- Elle parle HTTP en clair, donc le jeton traverse le réseau en clair. Sur la boucle locale c’est sans objet. Ailleurs, mettez un reverse proxy terminant TLS devant.
- Le blocage par adresse n’arrête pas un attaquant disposant de nombreuses adresses. C’est la longueur du jeton qui rend la devinette sans espoir — d’où le plancher de 16 caractères au démarrage.
Binder au-delà de la boucle locale est votre choix, et le serveur le dit dans le log quand vous le faites :
2026-08-24T08:52:30Z - warning: the admin API is not bound to loopback — it rewrites what gets installed on every machine, so restrict it to a management network
127.0.0.1 plus un tunnel SSH est le défaut sûr.
Voir aussi
- Le store SQLite — le prérequis.
- Comment le garde-fou est construit — les internes.
Dépannage
Dépannage
Quand une installation PXE ne démarre pas, le log est le seul diagnostic dont dispose qui que
ce soit — il est donc délibérément ennuyeux et greppable : une ligne par requête, sur
stderr. Les deux moitiés se règlent :
RESCRIPTUM_LOG jette les requêtes qui ont
abouti, et RESCRIPTUM_LOG_FILE envoie les lignes dans un fichier.
2026-08-24T08:43:36Z - rescriptum 0.1.0 listening on 127.0.0.1:8999 — store=files:answers workers=10 max_conn=2048 timeout=10s
2026-08-24T08:43:37Z 127.0.0.1:61720 GET /health 200
2026-08-24T08:43:37Z 127.0.0.1:61721 POST /answer body=102 200 format=toml machine=98fa9b50d810 group=example-rack bytes=431
2026-08-24T08:43:37Z 127.0.0.1:61722 GET /rhel/ks?serial=7ABC123 body=0 200 format=text group=rhel-compute bytes=747
2026-08-24T08:43:37Z 127.0.0.1:61723 POST /answer body=27 404 no answer file applies
Lire une ligne
2026-08-24T08:43:37Z 127.0.0.1:61721 POST /answer body=102 200 format=toml machine=98fa9b50d810 group=example-rack bytes=431
└─ horodatage UTC └─ pair └─ requête └─ corps └─ statut
└─ comment la réponse a été composée └─ octets envoyés
Les lignes portant - à la place d’une adresse de pair sont au niveau serveur : démarrage,
échecs d’accept, délestage, problèmes de chargement du jeu de réponses.
format=… nomme la famille (toml, yaml, json, xml, text) plutôt que
l’extension — ks et preseed remontent tous deux comme text.
Échecs courants
| Symptôme | Cause probable |
|---|---|
404 no answer file applies | Rien n’a revendiqué la requête et il n’y a pas de default pour le format demandé. Capturez le corps et vérifiez que la MAC y est vraiment |
404 sur une URL qui marchait | L’URL nomme maintenant un alias de format qui exclut votre document — /ubuntu/answer ne servira pas un .toml |
500 … extends unknown group | Un document référence un groupe inexistant. Délibéré : servir une configuration dont la base manque installerait la machine à moitié configurée |
500 sur une seule machine | Le document de cette machine, ou son groupe, ne parse pas. La raison est sur la même ligne de log. rescriptum check le trouve sans attendre que la machine demande |
500 template needs {{ … }} | Un placeholder que la requête n’a pas pu remplir. Jamais servi comme chaîne vide, volontairement |
401 bad or missing token | RESCRIPTUM_ANSWER_TOKEN est défini mais l’ISO n’a pas été préparée avec le même --answer-auth-token |
413 | Un corps de plus de 1 Mo, ou un Content-Length qui en annonce un. Refusé depuis l’en-tête, avant toute lecture |
503 | Plus de connexions simultanées que RESCRIPTUM_MAX_CONNECTIONS. Augmentez-le, ou trouvez qui se connecte |
| Réponse servie, installation quand même ratée | Le document est du TOML valide mais pas du Proxmox valide. Passez render dans validate-answer |
| L’installateur ne contacte jamais le serveur | L’URL de l’ISO ou un pare-feu, pas ce serveur. curl http://SERVER:8000/health depuis le même réseau |
Le serveur démarre mais tout part en 404
Regardez la ligne de démarrage. Les deux causes habituelles s’annoncent :
warning: /srv/answers does not exist yet — every request will 404 until it does
warning: /srv/answers cannot be read: Permission denied (os error 13) — every request
will 404 until that is fixed; check the directory's owner against the user this server
runs as
Le second est ce que vous obtenez quand le répertoire existe mais que le processus ne peut pas le lister — la cause habituelle est un répertoire créé en root et un serveur tournant sous quelqu’un d’autre. La question est posée au système de fichiers plutôt que déduite des bits de permission, donc elle tient compte du propriétaire, du groupe, des ACL et du montage.
… store=files:/srv/answers …
— cette seconde est le répertoire de réponses par défaut. Si vous en vouliez un autre,
RESCRIPTUM_ANSWERS_DIR n’a pas atteint le processus. Une valeur vide ou
composée d’espaces est traitée comme non définie, et un nombre nul ou impossible à parser
retombe sur sa valeur par défaut.
Le reproduire hors ligne
C’est le chemin le plus rapide entre « une machine a reçu la mauvaise chose » et un correctif :
$ export RESCRIPTUM_CAPTURE_DIR=/var/log/rescriptum-captures # puis laissez-le échouer une fois de plus
$ rescriptum render --body /var/log/rescriptum-captures/2026…-0000.body
render résout exactement comme le serveur, donc ce qu’il affiche est ce que cette machine
aurait reçu. Pas besoin de baie. Voir capturer les requêtes.
Sans capture, répétez depuis l’identité et l’URL :
$ rescriptum render --query "path=/rhel/ks&mac=98:fa:9b:50:d8:10&serial=7ABC123"
Ajoutez path= — sans lui, la résolution n’est pas contrainte par le format et peut choisir
un document que la vraie URL aurait exclu, ce qui est exactement le bug que vous pourriez être
en train de chasser.
Vérifier l’ensemble
$ rescriptum check
Problèmes de chargement, chaque machine et chaque membre de groupe rendus, et le validateur de l’installateur lancé là où il est dans le PATH. Détails dans validation.
Signaler quelque chose
Pour une mauvaise réponse, le rapport utile est ce que la machine a envoyé et ce qu’elle a
reçu — le .body et le .meta d’une capture, plus la ligne de log.
Nettoyez les hachages de mot de passe et les clés SSH avant de joindre quoi que ce soit :
la réponse n’est dans le outcome de la capture que par son nom, mais si vous collez aussi le
document rendu, il porte de vrais identifiants.
Ouvrez un ticket avec cela et la version tirée de la ligne de démarrage.
Servir les médias de démarrage
Servir les médias de démarrage
Une réponse dit à une machine comment s’installer. Elle ne dit rien de l’endroit d’où vient l’installeur — et jusqu’ici c’était le serveur web de quelqu’un d’autre, hébergeant des images que personne ne confrontait aux réponses écrites pour elles.
Avec un répertoire de médias, le même serveur fait les deux. La MAC d’une machine choisit sa réponse et l’image pour laquelle cette réponse a été écrite, et les deux ne peuvent plus diverger puisqu’un seul composant décide des deux.
$ export RESCRIPTUM_MEDIA_DIR=/srv/media
Non défini, tout est éteint. Rien ne change pour un déploiement existant tant que vous ne la définissez pas.
Où vivent les images de base
Aucune image d’installation n’est dans ce projet, ni dans une version publiée. Une
ISO est l’artefact de quelqu’un d’autre, elle pèse un à quatre gigaoctets, et elle change
à son propre rythme — trois raisons distinctes pour qu’elle vive sur votre disque plutôt
que dans le nôtre. RESCRIPTUM_MEDIA_DIR est l’endroit où vous les gardez, et ce
répertoire est l’archive : ce que l’éditeur a publié, sur disque, jamais modifié
ensuite.
Ce dernier point est une propriété, pas une promesse. Rien ici ne réécrit une image —
en préparer une produit un fichier compagnon et une injection appliquée au fil de
l’eau (voir Préparer une image Proxmox), de sorte que les
octets sur disque restent exactement ce que l’éditeur a publié et que leur empreinte
reste vérifiable contre le SHA256SUMS de l’éditeur. media list dit quelles entrées
sont l’archive et lesquelles en dérivent.
Faire entrer une image
Trois façons, et la première est celle à privilégier.
Choisir dans un 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
Rien concernant une image précise n’est stocké dans ce serveur. Chaque catalogue nomme l’index de sommes que l’éditeur publie déjà à côté de ses propres images, et les noms comme les empreintes en sont lus au moment où vous demandez — la liste est donc celle que cet éditeur a aujourd’hui, et l’empreinte est la sienne. Une table d’URL figée dans une version publiée proposerait les images du trimestre dernier, dont certaines supprimées depuis.
Ce que cela vaut, dit franchement. Prendre l’empreinte sur le serveur qui sert l’image n’est pas une vérification de signature. En HTTPS cela authentifie le domaine de l’éditeur et cela attrape un téléchargement tronqué, un miroir corrompu et un fichier qui a changé en dessous — l’essentiel de ce qui arrive vraiment — et rien de plus. Si vous voulez davantage, utilisez la section suivante avec une empreinte que vous avez obtenue vous-même.
Laisser le serveur la récupérer
$ 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
Elle atterrit sous un nom en .part et n’est renommée qu’une fois l’empreinte
vérifiée : un téléchargement partiel ne devient jamais une entrée du catalogue — le
catalogue analyse ce qu’il trouve, et une ISO tronquée s’analyse comme une image inconnue
qu’une machine essaierait ensuite de démarrer. Une récupération interrompue laisse le
.part en place, et relancer la commande la reprend.
--sha256 est obligatoire ici, parce que rien d’autre ne vérifierait ce qui est
arrivé. Les éditeurs publient un SHA256SUMS à côté de l’image. Si vous voulez vraiment
vous en passer, dites --unverified — l’important est que sauter cette vérification soit
un acte délibéré et non le défaut, puisque cela décide ce que chaque machine du réseau
installe.
--as NOM.iso choisit le nom de fichier quand l’URL n’en implique pas d’utilisable.
::: tip Il n’y a pas de TLS dans ce binaire
rustls et un magasin de racines, c’est une quarantaine de crates et plus d’un mégaoctet
sur ARMv7, pour un travail dont chaque hôte a déjà l’outil. Donc ceci lance curl, ou
wget si c’est lui qui est installé, et le dit franchement s’il n’en trouve aucun — auquel
cas la réponse est celle ci-dessous.
:::
Ou la poser vous-même
En SMB, en scp, depuis là où l’ISO se trouve déjà — le geste naturel sur un NAS — puis
l’enregistrer :
$ 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 est facultatif et mérite d’être fourni : une empreinte qui ne correspond pas,
c’est soit un téléchargement tronqué soit le mauvais fichier, et les deux installeraient
la mauvaise chose sur chaque machine qui demande. Rien n’est enregistré en cas d’écart.
Rien n’est copié et l’image n’est jamais modifiée. Ce que media add écrit, c’est le
fichier compagnon .media posé à côté, qui retient l’empreinte et ce que la détection a
trouvé. C’est tout l’intérêt : hacher 1,5 Go prend près d’une minute, et le serveur ne
doit jamais passer une minute dans une requête.
Une image sans compagnon apparaît quand même et est servie quand même — elle n’a simplement pas d’empreinte à revérifier, et elle est analysée à la volée.
Ce qu’il sait dire d’une 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 familles sont reconnues — Proxmox, Debian, Ubuntu, RHEL et ses dérivés, SUSE et Fedora CoreOS — à partir d’une table de marqueurs situés dans l’image. Là où un éditeur a laissé une chaîne de version, elle est reprise ; l’identifiant de volume sert de repli.
Une image que rien ne reconnaît est quand même listée et quand même servie. Ne pas
savoir la décrire n’est pas la même chose que ne pas savoir s’en servir : elle peut être
sanbootée, écrite sur une clé, ou récupérée entière par le firmware. Ce qu’elle ne peut
pas faire, c’est produire une strophe de démarrage, et le serveur le dit plutôt que de
deviner.
Les points d’entrée
Le listener média a sa propre socket, sur 0.0.0.0:8001 par défaut.
| Route | Ce qui revient |
|---|---|
GET / | le catalogue en texte, ou en JSON avec Accept: application/json |
GET /<id>/iso | l’image |
GET /<id>/kernel | le noyau, diffusé depuis l’intérieur de l’image |
GET /<id>/initrd | l’initrd, de même |
GET /<id>/initrd+iso | l’initrd avec l’image ajoutée, pour les vieux chargeurs |
GET /<id>/file/<chemin> | n’importe quel fichier dans l’image |
GET /health | 200 OK |
Rien n’est extrait et rien n’est décompressé. Un fichier dans une image ISO9660 est une
plage d’octets contiguë : servir /pve-8.4/kernel est donc un positionnement et une
longueur — le même travail de quelques kilo-octets que l’image fasse 400 Mo ou 4 Go.
Les plages (Range), ETag, If-Range et HEAD sont tous traités, parce que les vrais
clients en ont besoin : casper d’Ubuntu et anaconda de Red Hat récupèrent tous deux par
plages, et le démarrage HTTP UEFI envoie un HEAD avant de récupérer quoi que ce soit.
Pourquoi c’est un second listener
Ce n’est pas une préférence — trois raisons distinctes, dont une seule suffirait :
- Le point de réponse répond sur n’importe quel chemin, puisque l’URL est gravée dans
une ISO. Un préfixe
/media/…découperait un espace réservé dans un espace délibérément ouvert. RESCRIPTUM_TIMEOUT_SECSest une échéance de connexion entière de dix secondes. Un transfert de 1,5 Go dure quinze secondes en gigabit et deux minutes en 100 Mbit : tous les téléchargements seraient tués en vol — et cela ressemblerait à un réseau instable, pas à un réglage.- Un téléchargement retient un jeton de connexion pendant des minutes. Partager ce budget avec les réponses, c’est un déploiement qui affame ses propres installations.
Les deux ont des budgets séparés, et un test le prouve au lieu de l’espérer : les réponses continuent d’aboutir avec quatre transferts en cours.
Démarrer une machine depuis tout ça
media ipxe écrit la strophe de démarrage d’une 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
Il imprime un script, il n’en installe pas. Enregistrez-le dans le répertoire des réponses et c’est un document de réponse ordinaire — sélectionné, superposé et gabarisé comme n’importe quel autre :
$ rescriptum media ipxe pve-8.4 > /srv/answers/groups/rack-a/boot.ipxe
C’est bien le point. Le serveur ne devient pas malin sur le démarrage ; il gagne un
générateur, et le moteur de composition que vous avez déjà fait le reste. Un {{ mac }}
dans l’URL de réponse générée est rempli à chaque requête depuis les faits de la machine.
Chaque famille reçoit ce dont elle a réellement besoin, et elles ne se ressemblent pas :
| Famille | Comment la réponse lui parvient |
|---|---|
| Proxmox VE | dans l’image, via auto-installer-mode.toml — et proxmox-start-auto-installer sur la ligne de commande pour choisir la voie automatisée |
| Debian | preseed/url=… |
| Ubuntu | ds=nocloud-net;s=…/, d’où cloud-init récupère user-data et meta-data |
| Famille RHEL | inst.ks=… |
| SUSE | autoyast=… |
| Fedora CoreOS | ignition.config.url=… |
Proxmox est le cas à part, et il vaut la peine de savoir pourquoi : c’est le seul qui
porte l’emplacement de la réponse à l’intérieur de l’image plutôt que sur la ligne de
commande du noyau. C’est aussi pour cela que c’est le seul à devoir passer une fois par
prepare-iso — voir Préparer les médias d’installation.
::: tip Vous avez déjà lancé prepare-iso --pxe ?
Cela laisse un répertoire contenant vmlinuz, initrd.img et une ISO allégée. Pointez
RESCRIPTUM_MEDIA_DIR dessus et cela fonctionne tel quel : l’image allégée est toujours
reconnue comme Proxmox, et le noyau et l’initrd posés à côté sont trouvés et servis.
:::
Préparer une image Proxmox
Proxmox est la seule famille à porter l’emplacement de la réponse à l’intérieur de
l’image, dans /auto-installer-mode.toml. Cela imposait jusqu’ici de lancer
proxmox-auto-install-assistant prepare-iso ailleurs d’abord.
$ 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
Ce qui vient d’être écrit est un fichier compagnon : environ deux cents octets qui
tiennent lieu de 1,5 Go. La source n’est jamais modifiée, jamais copiée, et son
empreinte publiée reste vérifiable. Le fichier est injecté au fil de l’eau, donc
changer plus tard l’URL de réponse réécrit ces deux cents octets plutôt qu’un gigaoctet —
et les deux entrées apparaissent dans media list, adossées à une seule image sur disque.
--as NOM choisit le nom de l’entrée dérivée, et --url, --cert-fingerprint et
--token disent ce qui va dans le fichier.
Pour une clé USB
$ rescriptum media export pve-8.4-http /tmp/pve-auto.iso
Matérialise exactement ce que le listener aurait servi, par le même chemin de code. Une clé écrite autrement serait une seconde implémentation à maintenir honnête, et l’écart ne se verrait que sur le bureau de quelqu’un.
Quand il refuse
Refuser est ici une réponse complète, parce que le repli tient en une commande sur n’importe quelle Debian et que ce serveur sert très bien son résultat :
$ proxmox-auto-install-assistant prepare-iso pve.iso --fetch-from http --url …
Il refuse quand l’image n’a ni Rock Ridge ni Joliet — le fichier ne pourrait alors
exister que sous un nom 8.3 tronqué comme AUTO_INS.TOM;1, et l’installeur ne le
trouverait jamais. Il refuse une image UDF, parce qu’une ISO Windows ne garde ses gros
fichiers que dans l’arbre UDF et que patcher l’arbre ISO9660 produirait quelque chose qui
a l’air juste et ne l’est pas. Et il refuse quand le répertoire racine n’a pas de mou
dans aucun de ses secteurs : déplacer l’extent entraînerait les tables de chemins, ce qui
n’est délibérément pas fait.
Il refuse aussi de préparer une image non-Proxmox, en nommant l’alternative : toutes les
autres familles prennent l’URL sur la ligne de commande du noyau, là où media ipxe la
met déjà.
Si la source change en dessous
Les décalages d’injection sont calculés contre une image donnée. Une source qui aurait changé serait patchée au mauvais endroit, produisant une image qui se monte et qui est fausse — le fichier compagnon retient donc la taille de la source, et le catalogue refuse quand elle ne correspond plus :
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`.
Dire au serveur son propre nom
Dès qu’il écrit des URL dans les scripts qu’il sert, le serveur a besoin d’un nom pour
lui-même qu’une machine puisse réellement atteindre. 0.0.0.0:8001 n’en est pas un.
$ export RESCRIPTUM_PUBLIC_HOST=192.0.2.10
Un hôte, jamais une URL. Pas de schéma, pas de port, pas de chemin — le serveur écrit des URL pour deux listeners, et une valeur portant un port épinglerait chaque script généré sur l’un d’eux. Chaque URL ajoute le port de son propre listener. Une valeur portant l’un des trois est refusée au démarrage, en nommant lequel.
Laissée vide, elle demande à la table de routage laquelle des adresses de cet hôte fait face à l’extérieur — et sur un segment sans route par défaut, se rabat sur la liste des interfaces, ce qui sur un hôte à une seule adresse n’est pas une déduction du tout. Dans les deux cas elle dit au démarrage ce qu’elle a retenu, et s’il y avait quelque chose à trancher :
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.
C’est la seconde qu’il faut prendre au sérieux : une mauvaise déduction produit une machine qui démarre, enchaîne, et se bloque sur une adresse qui n’existe pas. Nommer les autres adresses est ce qui rend la question tranchable depuis le journal lui-même, plutôt qu’en allant regarder l’hôte. Le NAT est le cas qu’aucune des deux lignes ne peut attraper : l’adresse est bien celle de cet hôte, et bien celle que les machines n’atteignent pas.
Le garder honnête
$ 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
Son code de sortie est un contrat, comme celui de check : zéro quand tout ce qui a été
enregistré correspond toujours, un quand quelque chose a dérivé. deploy.sh s’y fie.
Une image qui a changé sous une empreinte enregistrée est la seule panne qui installe silencieusement quelque chose que personne n’a relu, donc elle est bruyante :
FAIL pve-8.4: the image no longer matches what was recorded
recorded 9f86d081884c7d65…
found 7d793037a0760186…
Ce que cela prouve, c’est l’intégrité, pas l’authenticité : ce qui est servi est ce
qui a été enregistré. Savoir si ce qui a été enregistré est bien ce que l’éditeur a
publié relève de ses propres signatures, et --sha256 au moment du media add est
l’endroit où cette vérification se place.
Qui a le droit de récupérer
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 : lecture seule, borné au catalogue, et aucun chemin de système de fichiers n’est jamais construit à partir d’une requête. Plus un qui peut dire pas vous :
$ export RESCRIPTUM_BOOT_ALLOW=10.0.0.0/8,192.168.0.0/16
Non définie, n’importe qui pouvant atteindre le port, ce qui sur un VLAN de provisionnement est la configuration honnête. Un VLAN de démarrage est la recommandation qui fonctionne vraiment ; voir Sécurité.
Réglages
| Variable | Défaut | À quoi elle sert |
|---|---|---|
RESCRIPTUM_MEDIA_ADDR | 0.0.0.0:8001 | Le listener |
RESCRIPTUM_MEDIA_TIMEOUT_SECS | 600 | Échéance du transfert entier |
RESCRIPTUM_MEDIA_MAX_CONNECTIONS | 16 | Transferts simultanés |
Seize, c’est bas volontairement. Chaque transfert retient son jeton pendant des minutes, et le petit bout de ce sur quoi cela doit tourner est un NAS avec un disque mécanique : seize transferts à 64 Kio par morceau font environ deux méga-octets de tampons, une arithmétique qui doit tenir dans 512 Mo de RAM.
Sur une machine de datacenter, montez-la. Le point de réponse a son propre budget et n’est touché dans aucun des deux cas.
Démarrer une machine par le réseau
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.