Une console, plusieurs clients
Si vous gérez Proxmox pour le compte d’autres gens, chaque nouveau nœud est le même travail, fait à chaque fois un peu différemment. Chaque client a son propre pays, son fuseau horaire, ses clés root, sa disposition des disques, sa règle de nommage des hôtes et sa clé de souscription. Ratez l’un d’eux sur un nœud neuf et vous le découvrez la première fois que quelqu’un se connecte, ou la première fois qu’un audit demande pourquoi l’horloge a une heure de décalage.
J’ai construit cela pour un client qui avait exactement ce problème, et sa demande était d’une simplicité rafraîchissante. Chaque site s’installe avec son propre nommage, son propre mot de passe root et ses propres clés SSH. Personne sur le site ne fait quoi que ce soit de subtil : on monte un serveur dans la baie, on le branche sur le bon VLAN et on l’allume. Personne ne relève de numéros de série, et personne n’écrit de fichier de réponse par machine. Et un serveur ajouté de cette façon finit dans le cluster existant du site.
| Ce qu’ils voulaient | Ce qui le fournit ici |
|---|---|
| le nommage, le mot de passe et les clés propres au site | une réponse préparée par site, choisie par le jeton de ce site |
| brancher sur le bon VLAN, allumer, repartir | la découverte DNS, iPXE, et Caddy qui choisit le jeton d’après le sous-réseau source |
| pas de numéros de série, pas de fichier de réponse par machine | les réponses ne filtrent que sur le produit ; le site vient du réseau |
| le nouveau serveur rejoint le cluster existant | pas dans ce billet : PDM n’a pas de hook de premier démarrage, voir plus bas |
Ce billet couvre les trois premiers points : amener un serveur nu de la mise sous tension à un nœud Proxmox installé avec les paramètres du bon site. C’est cette moitié-là qui se présente à presque tous ceux qui ont plus d’un site, MSP ou non, donc elle mérite d’exister seule. Rejoindre le cluster est un travail à part, et la section sur le hook de premier démarrage absent explique pourquoi PDM ne peut pas encore le faire pour vous.
Proxmox Datacenter Manager (PDM) 1.1 a ajouté la pièce qui manquait1. Il peut désormais servir de serveur de réponses pour l’installateur automatique de Proxmox : une machine nue démarre, envoie ce qu’elle est, reçoit une réponse préparée et s’installe toute seule, sans personne à la console. Pour un fournisseur de services managés (MSP) qui utilise déjà PDM comme console de gestion, c’est l’endroit tout désigné pour garder les paramètres d’installation de chaque client.
Ce que vous ne voulez pas, c’est un serveur PXE par client. Chacun d’eux est un serveur Trivial File Transfer Protocol (TFTP), une configuration DHCP, un jeu de fichiers de démarrage et une ISO à reconstruire chaque fois que Proxmox publie une version mineure. Dix clients, c’est dix de ces ensembles à tenir à jour. Ça ne passe pas à l’échelle. Le but ici est un seul parc PXE, une seule image de démarrage, un seul enregistrement DNS et un seul PDM, chaque site recevant malgré tout sa propre réponse.
PDM 1.1 fait l’essentiel du chemin tout seul. Il s’arrête court à deux endroits, et les deux méritent d’être connus avant de bâtir un déploiement autour : il n’a aucun moyen d’associer une réponse à un site, et aucun moyen de donner un script de premier démarrage à un nouveau nœud. Ce billet prouve les deux à partir du code source de PDM, contourne la première, et construit l’ensemble sur mon PDM de labo en 1.1.9, avec un proxy durci pour que les parties qui détiennent des secrets restent fermées.
Ce que PDM 1.1 vous donne
La fonction se trouve sous Remotes → Automated Installations et comporte trois parties : les installations, les réponses préparées et les jetons d’authentification2.
Une réponse préparée est la version PDM du answer.toml de l’installateur, remplie via un formulaire au lieu d’être écrite à la main.
| Partie | Ce qu’elle fait | Le détail qui compte |
|---|---|---|
| Réponses préparées | pays, clavier, fuseau horaire, hash du mot de passe root, clés SSH, réseau, disposition des disques, clé de souscription, action après installation | générées à neuf pour chaque requête |
| Filtres de cible | décident quelle machine reçoit quelle réponse | JSON Pointer dans les informations système de l’installateur, glob pour la valeur3 |
| Modèles | MiniJinja dans les champs FQDN, e-mail de l’administrateur, IP, passerelle et DNS | des compteurs comme installation_nr augmentent à chaque réponse servie4 |
| Jetons d’authentification | l’installateur envoie Authorization: Bearer <name>:<secret> | un type de jeton distinct des jetons d’API de PDM ; chaque réponse liste les jetons qu’elle accepte2 |
| Installations | un enregistrement par requête, avec toutes les informations système | passe de en cours à terminée quand le webhook de post-installation rappelle |
| Permissions | Sys.Audit pour consulter, Sys.Modify pour modifier | les deux sur /system/auto-installation4 |
Le côté installateur existe depuis un moment.
La commande proxmox-auto-install-assistant prepare-iso construit une ISO qui va chercher sa réponse en HTTP, et depuis l’ISO de Proxmox VE 9.2 elle sait aussi produire le noyau, l’initrd, l’ISO et le boot.ipxe dont iPXE a besoin pour démarrer le tout par le réseau5.
Les compteurs sont la partie qui n’a aucun équivalent dans un answer.toml autonome.
Une seule réponse préparée numérote chaque hôte qu’elle construit, là où un fichier statique devrait être copié pour chaque machine.
C’est un vrai pas en avant, et cela reste une première version.
Le formulaire n’est pas encore à parité avec le fichier de réponse autonome6, et un domaine manque entièrement : il n’y a aucun script de premier démarrage dans la 1.1, sous aucune forme7.
Ce manque, et le site sur lequel il ne sait pas filtrer, font l’objet des deux sections suivantes.
Il ne sait pas filtrer sur le site
PDM choisit une réponse en comparant des filtres aux informations système que l’installateur envoie. La question est donc de savoir ce que contient ce document, car tout ce qui n’y figure pas ne peut pas servir de critère.
| Clé | Ce qu’elle contient | Pourrait-elle désigner le site ? |
|---|---|---|
product | fullname, product (pve, pbs, pmg), enable_btrfs | non |
iso | release, isorelease | non |
dmi | système, carte mère et châssis : nom, numéro de série, UUID, SKU, étiquette d’inventaire | seulement si quelqu’un a écrit le site dans l’étiquette d’inventaire |
network_interfaces | une liste de {link, mac} | seulement via un inventaire que vous tenez vous-même |
C’est tout8,9. Pas d’adresse, pas de nom d’hôte, pas d’option DHCP, rien sur l’endroit où la machine est branchée.
Les champs Desktop Management Interface (DMI) paraissent prometteurs jusqu’à ce qu’on en lise de vrais.
Chacune de mes installations de labo est revenue avec le nom de système Standard PC (Q35 + ICH9, 2009), un numéro de série vide et une étiquette d’inventaire vide.
Les vrais serveurs les remplissent, mais avec ce que le constructeur y a mis et non avec le nom de votre client, et filtrer sur les numéros de série ou les adresses MAC ne marche que si vous tenez la liste de chaque châssis que vous installerez jamais, ce qui est exactement la paperasse que tout cela devait supprimer.
Par hôte, oui. Par site, non.
Alors, sur quel réseau la machine est-elle branchée ? PDM ne le sait jamais. L’installateur a son bail DHCP en main quand il envoie sa requête, et le seul fait qu’un MSP connaît à coup sûr sur un nouveau nœud est justement celui qu’il laisse de côté. Le filtrage sur l’adresse source, IPv4 ou IPv6, est une demande de fonctionnalité ouverte, que j’ai moi-même déposée sous le numéro 826310.
Une deuxième limite se cache dans les règles de sélection, et mon labo est tombé en plein dedans.
Le jeton est vérifié en premier, et seules les réponses autorisées sur ce jeton sont prises en compte, y compris celle par défaut4.
PDM n’autorise qu’une seule réponse par défaut pour tout le système.
Vous ne pouvez donc pas donner à chaque site son propre repli avec l’indicateur par défaut.
Mon labo avait une réponse par défaut appelée fallback sur son propre jeton, et elle n’a jamais été servie une seule fois, parce que rien n’envoie jamais ce jeton.
Chaque site a plutôt besoin d’un filtre attrape-tout sur son propre jeton, comme /product/product=*, placé en dernier dans le fichier.
Deux autres points du même code sont bons à avoir en tête quand vous déboguez :
- La première correspondance gagne, dans l’ordre du fichier, et une seconde correspondance ne produit qu’un avertissement dans le journal. C’est la règle depuis la 1.1.111.
- Un filtre ne compare que des chaînes.
/product/enable_btrfsest un booléen et ne correspond jamais à rien, et un tableau a besoin d’un indice, comme dans/network_interfaces/0/mac.
En 1.1.9, un jeton inconnu ou expiré reçoit un 401 après un délai volontaire d’environ une demi-seconde et ne laisse aucune trace.
Un jeton valide sans aucune correspondance reçoit un 400 avec le corps no answer file found, et cette tentative est enregistrée, avec toutes les informations système.
Cet enregistrement est le moyen le plus rapide de lire les vrais numéros de série et adresses MAC d’un nouveau châssis avant d’écrire un filtre pour lui.
Il ne sait pas lancer un script au premier démarrage
L’installateur lui-même prend en charge un hook de premier démarrage.
Une réponse peut porter une section [first-boot] avec source = "from-iso" ou "from-url", un ordre parmi before-network, network-online ou fully-up, et une empreinte de certificat facultative9.
prepare-iso --on-first-boot intègre un script dans l’ISO pour le cas from-iso.
PDM n’envoie jamais cette section.
Pas une seule fois.
Le type de réponse de l’installateur a bien le champ, first_boot: Option<FirstBootHookInfo>12.
La réponse préparée de PDM n’a aucun champ pour cela, et le code qui construit la réponse à partir d’elle écrit first_boot: None sans condition4.
Rien dans le formulaire, rien dans l’API, et rien dans le fichier de configuration que vous pourriez ajouter à la main.
C’est une demande de fonctionnalité ouverte, le bug 7699, déposée en juin 2026 et toujours sans responsable au moment où j’écris7.
Près de cinq mois plus tard, Proxmox n’y a pas répondu, pas plus qu’au 8263.
C’est ce silence qui inquiète. Quelqu’un qui prend le temps d’écrire comment votre produit pourrait être meilleur mérite une réponse, même si cette réponse est non, et c’est une simple courtoisie professionnelle.
À plus forte raison quand ceux qui écrivent sont des clients payants et des partenaires, qui mettent déjà de l’argent dans le produit avant d’y mettre aussi leur temps.
Et le script sur l’ISO ne vous sauve pas.
L’installateur ne met en place le service de premier démarrage que if let Some(first_boot) = &answer.first_boot, et c’est le seul chemin qui lise jamais le fichier intégré13.
prepare-iso n’émet aucun avertissement quand vous intégrez un script qu’aucune réponse ne demandera jamais.
Il reste sur l’ISO et ne fait rien.
Cela pèse plus lourd pour un MSP que pour n’importe qui d’autre.
Un nœud sorti de l’installateur est un système nu avec un mot de passe root haché et quelques clés SSH.
Ce n’est pas un remote PDM, il n’est dans aucun cluster, et il n’a rien de votre supervision, de vos dépôts ni de votre stockage.
Dans mon labo, les trois nœuds ont tous terminé et rappelé, et le remote SITEA dans PDM a quand même dû être ajouté à la main ensuite.
Donc, quel que soit l’outil qui finalise un nœud, Ansible ou autre chose, il doit être déclenché de l’extérieur : par le webhook de post-installation, par une tâche qui surveille la liste Installations, ou par une personne. Je m’arrête là. Ce billet traite de la façon de faire servir tous les sites par un seul parc PXE, et le manque de premier démarrage est une limite à prévoir dans vos plans, pas une que je vais masquer ici.
Le jeton, c’est le site
PDM ne voit pas le site, mais il délimite bien les réponses par jeton, et un jeton n’est qu’un en-tête. Celui qui pose l’en-tête choisit quel jeu de réponses l’installateur peut obtenir. Tout le tour est là.
Placez donc devant PDM quelque chose qui sait d’où vient la requête :
- L’image de démarrage porte un jeton générique,
catch-all:not-a-secret, identique dans chaque image et publié ici même. Ce n’est pas un jeton PDM et il n’ouvre rien à lui seul. - Un reverse proxy Caddy devant PDM regarde le sous-réseau source d’une requête de réponse qui porte le jeton générique, et remplace l’en-tête par le vrai jeton de ce site.
- PDM voit
site2-2026q4:<secret>et ne propose que les réponses de site2.
Le secret n’approche jamais l’image de démarrage, si bien qu’une clé USB perdue ou une ISO copiée ne donne rien à personne. Renouveler le jeton d’un site est une modification de Caddy, pas une reconstruction d’image. Et ajouter un site, c’est une ligne de configuration Caddy et un nouveau jeton dans PDM.
Cela repose sur l’honnêteté de l’adresse source, qui est une propriété de votre réseau, pas du proxy. Ce compromis et les autres sont exposés plus bas, sous « Ce qui reste à décider ».
Comment l’installateur trouve PDM sans URL intégrée
Une seule image pour tous les sites ne fonctionne que si l’image ne porte pas non plus l’adresse de PDM. L’installateur cherche l’URL de sa réponse à trois endroits, dans l’ordre14 :
| Règle | Ce que fait le code | Ce que cela signifie pour vous |
|---|---|---|
| Ordre | une URL dans l’ISO l’emporte ; sinon le DHCP ; tout échec du DHCP retombe sur le DNS14 | construisez l’image sans --url, et n’envoyez d’option 250 nulle part |
| Noms d’enregistrement | proxmox-auto-installer.<search domain> pour l’URL, proxmox-auto-installer-cert-fingerprint.<search domain> pour l’empreinte | une zone, deux enregistrements TXT |
| Domaine de recherche | la première ligne search de /etc/resolv.conf, découpée sur une seule espace | distribuez exactement un domaine ; deux en font un seul nom invalide |
| Pas de domaine | échoue avec Could not find search domain in resolv.conf. | vérifiez que l’option DHCP 15 ou 119 est bien envoyée |
| Valeur TXT | dig txt +short, tous les " retirés, puis espaces de bord supprimés | une valeur TXT découpée en plusieurs chaînes laisse une espace dans l’URL ; gardez une seule chaîne |
| Empreinte | prise dans le DNS seulement si l’URL vient aussi du DNS et qu’aucune n’est intégrée | publiez l’empreinte du certificat de PDM, que Caddy sert aussi |
| Jeton | envoyé comme Authorization: Bearer <token>, stocké en clair sur l’ISO9 | c’est pourquoi l’image ne porte que le jeton générique |
Le certificat que vérifie l’installateur est celui qui répond sur le 443. Dans cette conception, Caddy y sert le propre certificat de PDM, donc un client voit sur le 443 le même certificat que PDM présente sur le 8443, et il n’y a qu’une seule empreinte à publier.
La construction : PDM, fichiers de démarrage et DNS
Les exemples utilisent le domaine example.doc et trois sites, 10.10.50.0/24, 10.20.50.0/24 et 10.30.50.0/24.
.doc n’est pas un vrai domaine de premier niveau, remplacez-le par le vôtre.
PDM, Caddy, dnsmasq et les fichiers de démarrage vivent tous sur un seul hôte, pdm.example.doc en 10.10.50.10, et les sites distants l’atteignent par un relais DHCP sur leurs passerelles.
La gestion vit sur 10.99.0.0/24 et, à ce titre, c’est le seul réseau qui voie jamais l’interface web de PDM.
Jetons et réponses dans PDM
Sous Remotes → Automated Installations → Authentication Tokens, ajoutez un jeton par site et donnez-lui une expiration qui correspond à votre fenêtre de déploiement.
Le nom n’est pas secret, et vous le verrez dans les journaux, alors faites-lui dire quelque chose : site1-2026q4.
Copiez le secret quand il s’affiche.
Ajoutez ensuite une réponse préparée par site et par produit, autorisée sur le seul jeton de ce site :
| Champ | site1, Proxmox VE |
|---|---|
| Jeton autorisé | site1-2026q4 |
| Filtre de cible | /product/product=pve |
| Pays, clavier, fuseau horaire | gb, en-gb, Europe/London |
| Accès root | clés SSH, plus un mot de passe défini dans le formulaire, que PDM stocke et n’envoie que sous forme de hash4 |
| Réseau | DHCP, noms d’interface figés |
| FQDN | {{ product.product }}-site1-{{ installation_nr }}.example.doc |
| Disque | votre disposition, par exemple un miroir ZFS sur deux disques désignés par leur nom |
| Webhook de post-installation | URL de base https://pdm.example.doc, avec l’empreinte du certificat de PDM |
Le compteur augmente à chaque réponse servie, nouvelles tentatives comprises, donc les noms peuvent sauter des numéros. Le compteur de mon labo est à 4 après trois installations terminées. Si vous avez besoin de noms prévisibles, utilisez FQDN from DHCP et mettez le nom d’hôte sur la réservation DHCP.
L’URL de base du webhook est le seul réglage qu’il faut changer pour cette conception. Il est naturel de la pointer sur PDM lui-même, port 8443, c’est ainsi que mon labo l’avait, et le 8443 est sur le point d’être fermé. Pointez-la sur Caddy.
Les fichiers de démarrage
Sur n’importe quel hôte Proxmox VE ou Debian avec le dépôt Proxmox :
apt install proxmox-auto-install-assistant xorriso
mkdir -p /srv/www/pve-9.2
proxmox-auto-install-assistant prepare-iso proxmox-ve_9.2-1.iso \
--fetch-from http \
--answer-auth-token 'catch-all:not-a-secret' \
--pxe-loader ipxe \
--output /srv/www/pve-9.2/
Pas de --url, car le DNS la fournit.
--pxe-loader ipxe implique --pxe, et avec --pxe la sortie doit être un répertoire qui existe déjà15.
Vous obtenez quatre fichiers : vmlinuz, initrd.img recompressé de zstd en gzip parce qu’iPXE ne sait pas charger un initrd zstd, l’ISO préparée avec /boot retiré, et un menu boot.ipxe qui lance par défaut l’installation automatique au bout de dix secondes et désigne les autres fichiers par nom relatif15.
Gardez les quatre dans un même répertoire.
Chaque cible a besoin d’assez de mémoire pour contenir le noyau, l’initrd et toute l’ISO.
Les notes de Proxmox VE 9.2 l’expriment ainsi : « at least 6 GiB of available memory on the host »5.
Les mêmes notes signalent un problème connu avec le noyau de l’ISO 9.2 sur les contrôleurs megaraid_sas et certains disques NVMe en mode JBOD (just a bunch of disks), avec le conseil d’installer depuis l’ISO 9.1 puis de mettre à niveau5.
Testez votre matériel avant de lui confier le chemin réseau.
Les enregistrements DNS
Dans la zone qui correspond à l’unique domaine de recherche distribué par le DHCP :
$ORIGIN example.doc.
pdm IN A 10.10.50.10
proxmox-auto-installer IN TXT "https://pdm.example.doc/api2/json/auto-install/answer"
proxmox-auto-installer-cert-fingerprint IN TXT "63:C4:D6:...:85:07"
L’empreinte est celle du propre certificat de PDM :
openssl x509 -in /etc/proxmox-datacenter-manager/auth/api.pem -noout -fingerprint -sha256
Vérifiez-les comme le fera l’installateur :
dig txt +short proxmox-auto-installer.example.doc @10.10.50.10
# one quoted string, nothing split
Caddy, la seule porte d’entrée vers PDM
L’API de PDM écoute sur [::]:8443, et c’est une constante compilée dans le binaire, pas un réglage16.
Vous ne pouvez pas la lier à la boucle locale.
Caddy devient donc la seule entrée, et le pare-feu s’en assure.
PDM a exactement deux routes qui fonctionnent sans connexion PDM : POST /api2/json/auto-install/answer avec un jeton d’installateur, et POST …/installations/{uuid}/post-hook avec le jeton à usage unique dans le corps4.
Ce sont les deux seules qu’un réseau de provisionnement doit pouvoir atteindre.
Utilisez Caddy 2.11.1 ou plus récent. La 2.11.1 a corrigé un contournement du filtre de chemin, CVE-2026-27587, et c’est exactement le genre de filtrage par chemin sur lequel tout cela repose17. Le paquet de Debian 13 est en 2.6.2, prenez-le donc dans trixie-backports, qui porte la 2.11.2, ou dans le dépôt de Caddy lui-même18.
Le Caddyfile, /etc/caddy/Caddyfile :
{
# the admin API on a socket only root can open, never on TCP
admin unix//run/caddy/admin.sock|0600
auto_https disable_redirects
servers {
protocols h1 h2
}
}
# PDM itself, on the loopback, trusted by its own certificate
(pdm_upstream) {
reverse_proxy https://[::1]:8443 {
transport http {
tls_trust_pool file /etc/proxmox-datacenter-manager/auth/api.pem
tls_server_name pdm.example.doc
}
}
log_append pdm_status {http.reverse_proxy.status_code}
}
# one site: name, subnet, PDM token name, PDM token secret
(site) {
@{args[0]} {
method POST
path /api2/json/auto-install/answer
remote_ip {args[1]}
header Authorization "Bearer catch-all:not-a-secret"
}
handle @{args[0]} {
log_append site {args[0]}
log_append pdm_token {args[2]}
request_header Authorization "Bearer {args[2]}:{args[3]}"
import pdm_upstream
}
}
pdm.example.doc, 10.10.50.10 {
tls /etc/proxmox-datacenter-manager/auth/api.pem /etc/proxmox-datacenter-manager/auth/api.key
import site site1 10.10.50.0/24 site1-2026q4 {env.PDM_SECRET_SITE1}
import site site2 10.20.50.0/24 site2-2026q4 {env.PDM_SECRET_SITE2}
import site site3 10.30.50.0/24 site3-2026q4 {env.PDM_SECRET_SITE3}
@post_hook {
method POST
path /api2/json/auto-install/installations/*/post-hook
remote_ip 10.10.50.0/24 10.20.50.0/24 10.30.50.0/24
}
handle @post_hook {
request_header -Authorization
import pdm_upstream
}
# the PDM web UI and API, from the management network only
@mgmt remote_ip 10.99.0.0/24
handle @mgmt {
import pdm_upstream
}
# anything else, from anywhere, never reaches PDM
handle {
abort
}
log {
output file /var/log/caddy/pdm.log
format filter {
request>headers>Csrfpreventiontoken replace REDACTED
}
}
}
# boot files, to the sites and management only
http://:80 {
@hidden path_regexp /\.
handle @hidden {
abort
}
@boot remote_ip 10.10.50.0/24 10.20.50.0/24 10.30.50.0/24 10.99.0.0/24
handle @boot {
root * /srv/www
file_server
}
handle {
abort
}
}
Les parties qui font un travail de sécurité, et la raison de chacune :
| Ligne | Pourquoi |
|---|---|
header Authorization "Bearer catch-all:not-a-secret" dans le filtre du site | sans elle, n’importe quel POST venu d’un sous-réseau de site reçoit le vrai jeton ; une requête doit venir du bon sous-réseau et porter le jeton générique |
request_header Authorization … | pose l’en-tête en écrasant ce que le client a envoyé, donc un jeton forgé est remplacé, jamais transmis19 |
remote_ip | le pair immédiat ; ne mettez ni NAT ni répartiteur de charge entre les installateurs et Caddy20 |
blocs handle nommés | gardés dans l’ordre d’écriture et mutuellement exclusifs, donc le handle { abort } nu a le dernier mot21 |
@mgmt | toute l’interface et l’API de PDM, intactes, donc la connexion propre à PDM, ses jetons d’API et ses cookies fonctionnent normalement ; un sous-réseau de site n’en reçoit rien |
admin unix//…|0600 | l’API d’administration écoute par défaut en TCP sur localhost:2019, sans authentification22 |
protocols h1 h2 | Caddy écoute par défaut en UDP 443 pour HTTP/322 ; ceci le désactive au lieu de laisser un port que personne n’a voulu ouvrir |
@hidden | file_server sert tous les fichiers sous sa racine, fichiers cachés compris, et hide est documenté comme n’étant pas une frontière de sécurité23 |
Csrfpreventiontoken replace REDACTED | Caddy masque Authorization et Cookie dans son journal d’accès par défaut, mais pas l’en-tête CSRF de PDM24 |
Le filtre de chemin est plus tolérant que PDM, et c’est le bon sens.
Caddy nettoie le chemin, décode l’encodage pourcent, fusionne les barres obliques répétées et ignore la casse avant de comparer20.
Donc //answer, %61nswer et ANSWER comptent tous comme la route de réponse, et une variante /api2/extjs/… ou une barre oblique finale ne correspond à aucune route de site et retombe sur abort.
Deux choix méritent une explication.
Caddy sert le certificat de PDM, directement depuis les fichiers de PDM.
Caddy lit api.pem et api.key là où PDM les garde, donc les installateurs, le webhook et un navigateur voient tous le certificat que PDM présente sur le 8443, et il n’y a pas de seconde copie qui dérive.
La façon évidente de lui donner cet accès est d’ajouter l’utilisateur caddy au groupe www-data.
Sur mon labo, j’ai vérifié ce que cette appartenance au groupe atteint réellement, en testant chaque fichier en tant qu’utilisateur caddy :
Fichier sous /etc/proxmox-datacenter-manager/ | Lisible par caddy dans www-data |
|---|---|
remotes.shadow, les identifiants de chaque remote géré | oui |
autoinst/tokens.shadow, tous les secrets des jetons d’installateur | oui |
auth/api.key, auth/csrf.key | oui |
access/user.cfg, subscriptions/ | oui |
Rien dans cette liste n’a sa place à proximité d’un proxy web. Un bogue dans le proxy deviendrait alors une compromission de chaque cluster géré par PDM.
Caddy reste donc hors de www-data et reçoit une ACL sur exactement les deux fichiers dont il a besoin, plus le droit de traverser les deux répertoires au-dessus sans pouvoir les lister :
#!/bin/sh
# /usr/local/sbin/pdm-cert-acl
# let Caddy read PDM's certificate and key in place, and nothing else of PDM's
set -e
D=/etc/proxmox-datacenter-manager; A=$D/auth
setfacl -m u:caddy:x "$D" "$A"
setfacl -m u:caddy:r "$A/api.pem" "$A/api.key"
Testé en tant qu’utilisateur caddy sur le labo après l’avoir lancé :
| Chemin | caddy |
|---|---|
auth/api.pem, auth/api.key | peut lire |
auth/csrf.key, auth/authkey.key, auth/authkey.pub | ne peut pas lire |
remotes.shadow, remotes.cfg, autoinst/tokens.shadow, access/user.cfg | ne peut pas lire |
lister auth/ | refusé |
Le hic, c’est le renouvellement. PDM écrit un certificat renouvelé dans un nouveau fichier puis le renomme à sa place, et un nouveau fichier ne porte pas l’ACL de l’ancien. Donc, quand PDM renouvelle, systemd remet l’ACL et recharge Caddy, sans intervention de personne.
Une vérification que le certificat et la clé vont ensemble, parce qu’un renouvellement les écrit l’un après l’autre :
#!/bin/sh
# /usr/local/sbin/pdm-cert-pair-ok
# exit 0 only when PDM's certificate and key belong together
d=/etc/proxmox-datacenter-manager/auth
c=$(openssl x509 -noout -pubkey -in "$d/api.pem") || exit 1
k=$(openssl pkey -pubout -in "$d/api.key") || exit 1
[ "$c" = "$k" ]
Une unité path qui surveille les deux fichiers, et le service qu’elle lance :
# /etc/systemd/system/caddy-pdm-cert.path
[Unit]
Description=Watch the PDM certificate and key for renewal
[Path]
PathChanged=/etc/proxmox-datacenter-manager/auth/api.pem
PathChanged=/etc/proxmox-datacenter-manager/auth/api.key
Unit=caddy-pdm-cert.service
[Install]
WantedBy=multi-user.target
# /etc/systemd/system/caddy-pdm-cert.service
[Unit]
Description=Let Caddy read the renewed PDM certificate in place, then reload Caddy
[Service]
Type=oneshot
ExecCondition=/usr/local/sbin/pdm-cert-pair-ok
ExecStart=/usr/local/sbin/pdm-cert-acl
ExecStart=/usr/bin/systemctl try-reload-or-restart caddy.service
apt install acl
systemctl daemon-reload
systemctl enable --now caddy-pdm-cert.path
La surcharge plus bas lance le même script d’ACL avant chaque démarrage de Caddy, donc un renouvellement survenu pendant que Caddy était arrêté n’est jamais manqué.
Les détails qui font que ça marche, chacun vérifié :
| Élément | Pourquoi |
|---|---|
| les deux fichiers surveillés | la première écriture déclenche l’unité ; la condition saute l’exécution tant que la paire ne correspond pas, et la seconde écriture la déclenche de nouveau avec une paire cohérente |
ExecCondition | une condition en échec saute l’exécution au lieu de la faire échouer, donc un renouvellement à moitié écrit n’atteint jamais Caddy25 |
PathChanged | se déclenche quand un fichier surveillé qui était ouvert en écriture est fermé26 ; sur le labo, il s’est aussi déclenché quand un nouveau fichier a été renommé par-dessus l’ancien |
| le script d’ACL à chaque exécution | un fichier arrivé par renommage n’a pas d’ACL, donc Caddy ne pouvait pas le lire tant que le script n’avait pas tourné de nouveau |
try-reload-or-restart | recharge Caddy s’il tourne et ne fait rien sinon |
reload --force dans l’unité de Caddy | sans --force, Caddy voit la même configuration et continue de servir l’ancien certificat |
Cette dernière ligne n’est pas une supposition.
Dans un conteneur Caddy 2.11.7 jetable, j’ai remplacé le fichier de certificat derrière un serveur en marche : un simple caddy reload continuait de servir l’ancien certificat, et caddy reload --force servait le nouveau.
L’unité du paquet recharge déjà avec --force27, gardez donc cette ligne si vous surchargez ExecReload.
Sur le labo, j’ai remplacé api.key et api.pem de PDM comme le fait un renouvellement, en écrivant de nouveaux fichiers puis en les renommant par-dessus les anciens.
Juste après le renommage, api.key n’avait aucune entrée d’ACL pour caddy.
En moins de quatre secondes, l’unité path s’était déclenchée, l’ACL était de retour sur les deux fichiers, et Caddy s’était rechargé sur place avec le même identifiant de processus.
Le nom du jeton et le secret sont tenus à part.
Le nom va dans le Caddyfile, où il est journalisé.
Le secret va dans un fichier d’environnement que seul root peut lire, et Caddy le lit via {env.*} au moment de la requête.
Journaliser quel jeton a servi, jamais le secret
La première version de mon labo fuyait.
L’unité du paquet Caddy le lance en caddy run --environ, et --environ affiche tout l’environnement avant de démarrer28,27.
Chaque redémarrage mettait le jeton du site dans le journal systemd en clair : huit lignes au moment où je les ai comptées.
Rien dans le journal d’accès, rien dans les journaux de PDM, tout venait de ce seul paramètre.
Le fichier de secrets ne contient donc que des secrets :
install -m 0600 /dev/null /etc/caddy/pdm-secrets.env
printf 'PDM_SECRET_SITE1=%s\nPDM_SECRET_SITE2=%s\nPDM_SECRET_SITE3=%s\n' \
'<secret1>' '<secret2>' '<secret3>' > /etc/caddy/pdm-secrets.env
et la surcharge de l’unité retire --environ et donne un emplacement au socket d’administration :
# systemctl edit caddy
[Service]
EnvironmentFile=/etc/caddy/pdm-secrets.env
RuntimeDirectory=caddy
RuntimeDirectoryMode=0700
# PDM's certificate and key, read where PDM keeps them
ExecStartPre=+/usr/local/sbin/pdm-cert-acl
# the access log directory and its files belong to Caddy
ExecStartPre=+/usr/bin/install -d -o caddy -g caddy -m 0750 /var/log/caddy
ExecStartPre=+/usr/bin/chown -R caddy:caddy /var/log/caddy
ExecStart=
ExecStart=/usr/bin/caddy run --config /etc/caddy/Caddyfile
ExecReload=
ExecReload=/usr/bin/caddy reload --config /etc/caddy/Caddyfile --force
Le + exécute ces lignes en root, avant que Caddy ne démarre sous son propre utilisateur.
Les lignes du journal comptent dès que quoi que ce soit d’autre touche /var/log/caddy : un journal tourné par root, ou un fichier créé à la main pendant les tests, laisse Caddy incapable d’ouvrir son propre journal d’accès.
Sur le labo, j’ai mis pdm.log en root:root et redémarré Caddy ; il est revenu en caddy:caddy et Caddy n’a journalisé aucune erreur de permission.
Redémarrez plutôt que de recharger après l’avoir modifié, parce qu’un rechargement ne relit pas l’environnement.
Utilisez {env.NAME}, jamais {$NAME} : la seconde forme est substituée au moment où le Caddyfile est adapté, donc le secret atterrit dans la configuration JSON, dans le fichier d’autosauvegarde et dans tout ce qui les vide à l’écran29.
Le socket d’administration compte ici aussi.
{env.*} résout n’importe quoi dans l’environnement du processus, sans liste d’autorisation, donc quiconque peut charger une configuration via une API d’administration en TCP peut faire relire un secret par Caddy à son intention30.
Chaque ligne de réponse du journal d’accès nomme désormais le site et le jeton, avec l’en-tête lui-même masqué. Depuis le labo, où les deux s’appellent siteA :
{"site":"siteA","pdm_token":"siteA","pdm_status":400,"status":400,
"request":{"headers":{"Authorization":["REDACTED"]}}}
Je l’ai vérifié au lieu de m’y fier.
Sur une copie podman de cette configuration avec des secrets factices, puis sur le labo, j’ai envoyé des réponses par tous les chemins auxquels j’ai pu penser : normal, journalisation de débogage active, amont coupé pour un 502, un rechargement et un redémarrage.
Puis j’ai compté chaque secret dans le journal systemd, le journal d’accès, les répertoires de configuration et de données de Caddy et le JSON adapté.
Chaque compte était à zéro.
L’exécution témoin, la même configuration lancée avec --environ, a affiché le secret deux fois.
Deux réserves sur ce test.
Avec debug actif, Caddy journalise toute sa configuration adaptée, donc le jeton générique et les marqueurs {env.*} y apparaissent ; les valeurs qu’ils recouvrent, non.
Laissez de toute façon le débogage désactivé en production.
Et si un secret est déjà passé par un journal, comme le mien, régénérez-le dans PDM et mettez à jour le fichier ; supprimer des lignes de journal n’est pas la solution.
Le pare-feu : le 8443 ne quitte jamais la machine
Mon labo n’avait aucun pare-feu, donc le 8443 répondait à tous les hôtes du réseau de provisionnement et chaque règle du Caddyfile pouvait être contournée. Fermez-le. nftables, avec une politique par défaut drop :
#!/usr/sbin/nft -f
flush ruleset
define SITES = { 10.10.50.0/24, 10.20.50.0/24, 10.30.50.0/24 }
define MGMT = { 10.99.0.0/24 }
table inet filter {
chain input {
type filter hook input priority filter; policy drop;
iifname "lo" accept
ct state established,related accept
ct state invalid drop
meta l4proto { icmp, ipv6-icmp } accept
# PDM's API listens on [::]:8443 and cannot be rebound
tcp dport 8443 counter drop
ip saddr $MGMT tcp dport { 22, 80, 443 } accept
udp dport { 67, 69 } accept
ip saddr $SITES meta l4proto { tcp, udp } th dport 53 accept
ip saddr $SITES tcp dport { 80, 443 } accept
}
chain forward {
type filter hook forward priority filter; policy drop;
}
}
Les connexions de PDM vers ses remotes sont sortantes, donc fermer le 8443 en entrée n’y change rien. Chargez un nouveau jeu de règles avec un retour arrière minuté la première fois, pour qu’une erreur vous coûte quatre-vingt-dix secondes plutôt qu’un déplacement jusqu’à la console :
systemd-run --unit=nft-rollback --on-active=90 /usr/sbin/nft flush ruleset
nft -f /etc/nftables.conf
# still connected? then:
systemctl stop nft-rollback.timer && systemctl enable nftables
Sur le labo, une fois tout cela en place :
| Vérification | Résultat |
|---|---|
| le 8443 depuis le réseau de gestion | pas de connexion ; le compteur de drop qui grimpe |
| l’interface web de PDM via Caddy sur le 443, depuis la gestion | connexion et fonctionnement normaux |
| l’interface web de PDM depuis une adresse de site | interrompue |
| POST de réponse depuis un site avec le jeton générique | atteint PDM avec le jeton du site : une sonde volontairement sans correspondance a reçu 400 no answer file found |
| la même sonde envoyée directement à PDM avec un jeton inventé | 401 |
POST de réponse sans le jeton générique, ou en /api2/extjs/… | interrompu |
fichiers où vivent les secrets de PDM, lus en tant que caddy | aucun lisible |
| le secret du site dans le journal systemd, le journal d’accès ou les données de Caddy depuis le changement | 0 |
DHCP, iPXE et la boucle
Le firmware d’un serveur ne sait récupérer son premier fichier qu’en TFTP, donc TFTP sert le petit chargeur iPXE et iPXE récupère tout le reste en HTTP.
Le piège, c’est qu’iPXE envoie sa propre requête DHCP, et un serveur qui lui répond encore ipxe.efi fait tourner la machine en rond pour toujours.
Le serveur DHCP doit distinguer le firmware d’iPXE.
| Serveur DHCP | Comment il casse la boucle |
|---|---|
| dnsmasq | marque les requêtes qui portent l’option 175, que seul iPXE envoie, et leur remet l’URL HTTP31 |
| ISC dhcpd | if exists user-class and option user-class = "iPXE"32 |
| FortiGate | le champ filename ne prend qu’une valeur, donc construisez un chargeur iPXE avec l’URL intégrée et servez-le à tout le monde |
dnsmasq, qui sert son propre réseau et les deux autres via des relais, avec des réservations uniquement :
# /etc/dnsmasq.d/pxe.conf
local=/example.doc/
domain=example.doc
address=/pdm.example.doc/10.10.50.10
txt-record=proxmox-auto-installer.example.doc,"https://pdm.example.doc/api2/json/auto-install/answer"
txt-record=proxmox-auto-installer-cert-fingerprint.example.doc,"63:C4:D6:...:85:07"
dhcp-authoritative
dhcp-range=set:site1,10.10.50.0,static,255.255.255.0,1h
dhcp-range=set:site2,10.20.50.0,static,255.255.255.0,1h
dhcp-range=set:site3,10.30.50.0,static,255.255.255.0,1h
dhcp-option=tag:site1,option:router,10.10.50.1
dhcp-option=tag:site2,option:router,10.20.50.1
dhcp-option=tag:site3,option:router,10.30.50.1
dhcp-option=option:dns-server,10.10.50.10
dhcp-option=option:domain-search,example.doc
dhcp-match=set:ipxe,175
dhcp-match=set:efi64,option:client-arch,7
dhcp-match=set:efi64,option:client-arch,9
dhcp-boot=tag:efi64,tag:!ipxe,ipxe.efi
dhcp-boot=tag:ipxe,http://pdm.example.doc/pve-9.2/boot.ipxe
# no option 250 anywhere: DNS discovery depends on it
enable-tftp
tftp-root=/srv/tftp
tftp-secure
# TFTP block-size ceiling: lower it for an IPsec or other tunnel in the path
tftp-mtu=1500
# dhcp-host=<mac>,<ip inside that site's /24>,<hostname>
dhcp-host=aa:bb:cc:dd:ee:01,10.10.50.21,pve01
dhcp-host=aa:bb:cc:dd:ee:02,10.20.50.21,pve01
static veut dire pas de plage dynamique : seules les machines réservées reçoivent une adresse, ce qui tient les inconnus à l’écart du chemin d’installation.
L’adresse de chaque réservation doit se trouver dans le /24 de son propre site, parce que c’est cette adresse que Caddy utilise pour choisir le jeton.
Un nœud réservé dans le mauvais sous-réseau reçoit la réponse du mauvais site, ou rien.
Les réservations sont une liste d’adresses MAC, et c’est exactement le genre de comptabilité machine par machine dont le client ne voulait pas.
Si brancher et démarrer est vraiment l’exigence, donnez plutôt à chaque VLAN de provisionnement une petite plage dynamique, par exemple dhcp-range=set:site1,10.10.50.100,10.10.50.150,255.255.255.0,1h.
Le site vient toujours du sous-réseau, donc Caddy choisit toujours le bon jeton, et le modèle de FQDN avec installation_nr nomme le nœud.
Le prix, c’est la première ligne de « Ce qui reste à décider » : tout ce qui est branché sur ce VLAN peut désormais récupérer la réponse du site, si bien que le VLAN lui-même devient la barrière.
dnsmasq négocie la taille de bloc TFTP avec le client.
Livré à lui-même, il la plafonne à la MTU de sa propre interface31, ce qui marche jusqu’au jour où le chemin vers un site est plus petit que le lien sur lequel se trouve dnsmasq.
La configuration fixe donc le plafond avec tftp-mtu=1500 au lieu d’en hériter.
Cela met le chiffre sous les yeux de quiconque s’occupe d’un site derrière un tunnel IPsec ou n’importe quoi d’autre qui grignote le paquet, et il le baisse à ce que le chemin transporte vraiment, au lieu de le découvrir par un transfert qui cale en route.
Si une ROM de carte réseau se comporte mal dès que l’option est accordée, tftp-no-blocksize coupe la négociation.
Copiez ipxe.efi depuis le paquet ipxe de Debian dans /srv/tftp, avec l’utilisateur dnsmasq comme propriétaire, parce que tftp-secure refuse tout le reste.
Préférez l’UEFI.
Secure Boot exige un iPXE signé, donc soit vous mettez cela en place, soit vous désactivez Secure Boot pendant l’installation et le réactivez ensuite.
Démarrer, surveiller, et ce qui casse
Testez avec une seule machine virtuelle sur un réseau de site avant de toucher au matériel : un firmware UEFI, 8 Gio de mémoire et une carte réseau sur ce VLAN reproduisent toute la chaîne.
Et le tout dans l’ordre, de la mise sous tension à une installation que PDM marque Finished :
| Étape | Ce qui se passe | Où regarder |
|---|---|---|
| 1 | le firmware reçoit ipxe.efi en TFTP, iPXE reçoit boot.ipxe en HTTP | journalctl -u dnsmasq, le journal de Caddy |
| 2 | au bout de dix secondes, le menu lance l’entrée automatique ; noyau, initrd et ISO se téléchargent | quatre GET, dont un d’environ 1,6 Go |
| 3 | l’installateur prend un bail, lit le domaine de recherche, interroge les deux enregistrements TXT | /tmp/fetch_answer.log sur la cible |
| 4 | POST vers Caddy, jeton remplacé, PDM choisit une réponse | le journal de Caddy nomme le jeton ; PDM affiche une nouvelle installation |
| 5 | l’installation se déroule | /tmp/auto_installer.log, /tmp/install-low-level-start-session.log |
| 6 | toujours dans l’installateur, le webhook rappelle via Caddy, puis la machine redémarre33 | l’installation passe à Finished avec disques, cartes réseau et clés d’hôte SSH rattachés |
En cas d’échec, l’installateur tombe sur un shell, sauf si la réponse active Reboot on Error, laissez donc ce réglage désactivé pendant les tests.
| Symptôme | Cause probable |
|---|---|
Could not find search domain in resolv.conf. | le DHCP n’a envoyé aucun domaine |
| une requête TXT pour un nom contenant une espace | plus d’un domaine de recherche |
Got empty response. | enregistrement absent, mauvaise zone, ou l’option 6 pointe vers un autre serveur DNS |
| une URL avec une espace au milieu | valeur TXT découpée en plusieurs chaînes |
| le journal dit que l’URL vient du DHCP | l’option 250 est présente quelque part sur le réseau |
| erreur TLS sur le POST | l’empreinte du DNS n’est pas celle du certificat actuel de Caddy |
| connexion fermée sans réponse | Caddy l’a interrompue : mauvais sous-réseau, pas de jeton générique, ou une route qui n’est pas l’une des deux |
400 no answer file found | le jeton a fonctionné ; aucun filtre n’a correspondu et il n’y a pas de réponse par défaut sur ce jeton |
| la mauvaise réponse | des filtres qui se chevauchent ; le premier du fichier gagne |
| le statut reste en cours | l’URL de base du webhook pointe encore sur le 8443 |
| iPXE se recharge sans fin | le serveur DHCP ne distingue pas iPXE du firmware |
délai TFTP PXE-E32 | next-server erroné, ou UDP 69 bloqué |
Ajouter Proxmox Backup Server
Rien dans ce chemin n’est propre à Proxmox VE.
Construisez un second jeu de fichiers de démarrage à partir de l’ISO de PBS 4 avec le même jeton générique, dans /srv/www/pbs-4/, et ajoutez une réponse PBS par site avec le filtre /product/product=pbs, autorisée sur le jeton de ce site.
Les enregistrements DNS, Caddy, les jetons et le pare-feu ne changent pas.
Pour un site qui a les deux, il faut que quelque chose décide quel produit reçoit une machine, et un menu n’est pas le bon outil pour le décider.
Le choose d’iPXE sans --timeout attend à la console qu’on appuie sur une touche, indéfiniment, ce qui est tout le contraire d’une installation automatique.
Laissez plutôt la réservation DHCP décider.
Vous réservez déjà chaque nœud par adresse MAC, alors marquez ceux qui seront des PBS et envoyez à chaque marque son propre boot.ipxe :
# /etc/dnsmasq.d/pxe.conf, replacing the single iPXE dhcp-boot line
dhcp-host=aa:bb:cc:dd:ee:01,10.10.50.21,pve01
dhcp-host=aa:bb:cc:dd:ee:31,set:pbs,10.10.50.31,pbs01
dhcp-boot=tag:ipxe,tag:!pbs,http://pdm.example.doc/pve-9.2/boot.ipxe
dhcp-boot=tag:ipxe,tag:pbs,http://pdm.example.doc/pbs-4/boot.ipxe
set:pbs sur une réservation marque cette seule machine31, et tag:!pbs rend les deux lignes de démarrage mutuellement exclusives, donc la question de savoir laquelle l’emporte ne se pose pas.
Le boot.ipxe de chaque produit lance déjà son entrée automatique au bout de dix secondes sans personne au clavier15.
Ces lignes passent dnsmasq --test en 2.91, la version de mon labo, et suivent la syntaxe documentée de dhcp-host et des marques.
Je n’ai pas encore démarré de nœud PBS en PXE à travers elles, alors essayez une machine marquée avant de vous y fier.
À partir de là, c’est le même chemin que pour Proxmox VE.
L’installateur PBS envoie "product": "pbs", Caddy y substitue le jeton du site, et PDM choisit la réponse PBS de ce site.
Rien n’est choisi à la main à aucun moment, et le produit que devient un nœud est écrit à un seul endroit, la réservation, à côté de son adresse.
| Ce qui décide | Où cela vit |
|---|---|
| quelle ISO démarre | la marque set:pbs sur la réservation DHCP |
| quelles réponses de site s’appliquent | le sous-réseau source, via la substitution de jeton dans Caddy |
| quelle réponse au sein de ce site | le filtre /product/product=pbs dans PDM |
L’installateur ne pose que le système d’exploitation. Le datastore, sur un stockage séparé du disque système, est un autre travail pour ce qui finalise le nœud, ce qui nous ramène au hook de premier démarrage absent.
Ce qui reste à décider
Aucun de ces points ne vous empêche d’utiliser cette conception. Chacun a besoin de quelqu’un qui en soit responsable.
| Compromis | Pourquoi c’est important | Que faire |
|---|---|---|
| l’adresse source est la barrière | tout ce qui est sur un VLAN de provisionnement, ou capable d’y usurper une adresse, peut récupérer la réponse de ce site : hash du mot de passe root, clés SSH, clé de souscription | isoler le VLAN, port security, réservations uniquement, expiration des jetons, désactiver les jetons entre deux déploiements |
| le jeton générique n’est pas un secret | il est dans chaque image et sur cette page | il ne fait que filtrer les requêtes égarées ; la barrière, ce sont le sous-réseau et le vrai jeton détenu par Caddy |
| Caddy peut lire la clé de PDM | servir le certificat de PDM implique que le proxy lise cette clé privée-là | une ACL sur api.pem et api.key seulement, rien d’autre de PDM lisible, l’hôte verrouillé ; la clé ne le quitte jamais |
| le DNS est de confiance | un enregistrement TXT forgé envoie l’installateur ailleurs, et si l’empreinte vient aussi du DNS, le faussaire contrôle les deux | protéger le DNS de provisionnement, ou intégrer l’empreinte de PDM avec --cert-fingerprint et reconstruire quand elle change |
| les fichiers de démarrage passent en HTTP clair | ils peuvent être lus ou remplacés sur le fil | garder le réseau isolé ; iPXE en HTTPS si votre build le permet |
| le jeton du webhook | 16 octets aléatoires, à usage unique, effacé après le rappel, mais sans limite de durée4 | purger les anciens enregistrements d’installation |
| la clé de souscription | activée une fois quand le réseau est monté ; en cas d’échec, la clé est supprimée et l’unité échoue, sans nouvelle tentative34 | vérifier systemctl --failed sur les nouveaux nœuds |
| PDM est un point unique | il n’est pas hautement disponible aujourd’hui | une panne arrête les nouvelles installations, pas les clusters en service |
| une seule carte réseau de gestion | la réponse n’a pas d’options de bond, de VLAN ou de bridge9 | c’est du travail après installation, avec le même problème de premier démarrage |
Une première version, sur laquelle on bâtit proprement
C’est une première version de la fonction, et pour une première version, elle est bonne. Le formulaire de réponse est sensé, les règles de sélection sont claires une fois qu’on les a lues, le webhook fonctionne, et le code est ouvert, ce qui est la seule raison pour laquelle tout ce qui précède a pu être prouvé plutôt que deviné. La série de correctifs est passée par quatre tours sur la liste de diffusion publique avant d’être intégrée35, et ça se voit.
Les manques sont précisément les deux choses qui transforment un serveur d’installation en système de provisionnement. Le site est l’unité dans laquelle pense un MSP, et l’installateur se tient dessus au moment où il pose sa question. Un hook de premier démarrage fait la différence entre un nœud installé et un nœud terminé. Les deux sont déjà à moitié là : l’installateur a le hook et PDM a le jeton. Je m’attends à ce que les deux arrivent, et quand ce sera le cas, le Caddy au milieu de cette conception deviendra plus simple au lieu de disparaître, parce qu’un proxy qui tient les secrets de PDM hors de portée et le 8443 hors du réseau vaut la peine d’être gardé de toute façon.
La raison de procéder ainsi en attendant est celle du début. Dix serveurs PXE, ce sont dix choses qui prennent du retard à dix vitesses différentes, et chacune n’est regardée que le jour où elle tombe en panne, justement le jour où vous en avez besoin. Un seul parc, tenu à jour une seule fois, où le site est déduit par le réseau et où les secrets sont gardés en un seul endroit, c’est moins de choses à porter. Moins de choses à porter, c’est moins de choses à rater, et c’est l’essentiel du métier.
Proxmox — Proxmox Datacenter Manager 1.1 press release — les installations automatiques, avec un nouvel onglet sous Remotes qui suit la progression des installations. ↩︎
Proxmox Datacenter Manager documentation — Automated Installations — réponses préparées, filtres de cible, modèles, et « a dedicated token mechanism, separate from the normal API tokens ». ↩︎ ↩︎
proxmox-datacenter-manager,
lib/pdm-api-types/src/auto_installer.rs— les clés des filtres de cible « are JSON Pointers as per [RFC6901], the values globs » ; une seule réponse par défaut. ↩︎proxmox-datacenter-manager,
server/src/api/auto_installer/mod.rs— vérification du jeton avant la réponse par défaut, première correspondance dans l’ordre du fichier,first_boot: None, incrément des compteurs, le jeton de post-hook à usage unique, les deux routes accessibles sans connexion PDM, et les privilègesSys.Audit/Sys.Modifysur/system/auto-installation. ↩︎ ↩︎ ↩︎ ↩︎ ↩︎ ↩︎ ↩︎Proxmox VE Roadmap — Proxmox VE 9.2 — préparation PXE et iPXE, « at least 6 GiB of available memory on the host », et le problème connu
megaraid_saset NVMe en JBOD. ↩︎ ↩︎ ↩︎Proxmox Bugzilla 8078 — « prepared answer disk setup does not allow to specify multiple udev filters with same name », ouvert le 26 septembre 2026, statut NEW au 7 novembre 2026. ↩︎
Proxmox Bugzilla 7699 — « Add on-first-boot parameter to the PDM remote installation », ouvert le 12 juin 2026, statut NEW au 7 novembre 2026. ↩︎ ↩︎
proxmox,
proxmox-installer-types/src/lib.rs—SystemInfo: produit, ISO, DMI et interfaces réseau. ↩︎Proxmox VE wiki — Automated Installation — les options
[first-boot]et[network], les informations système envoyées, et le fait que le jeton d’authentification « is stored in plain text inside the prepared ISO ». ↩︎ ↩︎ ↩︎ ↩︎Proxmox Bugzilla 8263 — « [Feature Request] auto-installer: client source IP (or X-Forwarded-For) for answer selection and templating », ouvert le 31 octobre 2026, statut NEW au 7 novembre 2026. ↩︎
proxmox-datacenter-manager,
debian/changelog— 1.1.1 : « match prepared answers in the configuration’s stable order and let the first match win ». ↩︎proxmox,
proxmox-installer-types/src/answer.rs—AutoInstallerConfigportefirst_boot: Option<FirstBootHookInfo>. ↩︎pve-installer,
proxmox-auto-installer/src/bin/proxmox-auto-installer.rs— l’exécutable de premier démarrage n’est mis en place que si la réponse a une sectionfirst_boot. ↩︎pve-installer,
proxmox-fetch-answer/src/fetch_plugins/http.rs— URL de l’ISO d’abord, puis DHCP, puis DNS ; les noms TXT ; l’analyse deresolv.conf;dig txt +shortavec les guillemets retirés ; l’en-tête Bearer. ↩︎ ↩︎pve-installer,
proxmox-auto-install-assistant/src/main.rs—--pxe-loaderimplique--pxe, le répertoire de sortie, l’initrd en gzip,/bootretiré de l’ISO, et leboot.ipxegénéré. ↩︎ ↩︎ ↩︎proxmox-datacenter-manager,
server/src/bin/proxmox-datacenter-api/main.rs—PDM_LISTEN_ADDRest l’adresse IPv6 non spécifiée surPDM_PORT, 8443, fixée à la compilation. ↩︎Caddy releases — la 2.11.1 a corrigé CVE-2026-27587, un contournement du filtre de chemin par séquences d’échappement. ↩︎
Debian sources — caddy —
2.6.2-12+deb13u1dans trixie,2.11.2-1~bpo13+1dans trixie-backports. ↩︎Caddy —
request_header— « With no prefix, the field is set (overwritten). » ↩︎Caddy — Request matchers —
remote_ipest le pair immédiat ; les correspondances de chemin ignorent la casse, sont décodées, nettoyées, avec les barres obliques répétées fusionnées. ↩︎ ↩︎Caddy —
handle— « only the first matchinghandleblock will be evaluated ». ↩︎Caddy — Global options — admin vaut par défaut
localhost:2019;protocolsvaut par défauth1 h2 h3. ↩︎ ↩︎Caddy —
file_server—hidene doit pas être traité comme une frontière de sécurité. ↩︎Caddy —
log—Cookie,Set-Cookie,AuthorizationetProxy-Authorizationsont journalisés commeREDACTED. ↩︎systemd.service —
ExecCondition=— pour un code de sortie de 1 à 254, « the remaining commands are skipped and the unit is not marked as failed ». ↩︎systemd.path —
PathChanged=— « activated if the file which was open for writing gets closed ». ↩︎caddyserver/dist,
init/caddy.service—ExecStart=/usr/bin/caddy run --environ --config /etc/caddy/Caddyfile. ↩︎ ↩︎Caddy — Command line —
--environaffiche l’environnement avant le démarrage. ↩︎Caddy — Caddyfile concepts, environment variables —
{$NAME}est substitué avant l’analyse du Caddyfile. ↩︎caddyserver/caddy,
replacer.go— toute clé commençant parenv.est passée àos.Getenv. ↩︎dnsmasq manual page —
dhcp-match,dhcp-boot,dhcp-hostavecset:<tag>,enable-tftp,tftp-secure,tftp-mtu,tftp-no-blocksizeet les plagesstatic. ↩︎ ↩︎ ↩︎iPXE — Configuring ISC dhcpd —
if exists user-class and option user-class = "iPXE"remet le script à iPXE et le chargeur au firmware. ↩︎pve-installer,
unconfigured.sh—proxmox-post-hooks’exécute juste après la réussite deproxmox-auto-installer, avant le redémarrage. ↩︎pve-installer,
proxmox-first-boot/proxmox-first-boot-subscription— « always drop the staged key, regardless of whether activation succeeded ». ↩︎pdm-devel — “add auto-installer integration”, v4, 40 patches — la série qui a ajouté la fonction, avec ses notes de changement de la v3 à la v4. ↩︎