Eén console, veel klanten
Als je Proxmox voor andere mensen beheert, is elke nieuwe node hetzelfde werk, telkens net iets anders gedaan. Elke klant heeft zijn eigen land, tijdzone, root-keys, schijfindeling, naamgeving van hosts en subscription key. Zit er één daarvan fout op een verse node, dan merk je dat de eerste keer dat iemand inlogt, of de eerste keer dat een audit vraagt waarom de klok een uur afwijkt.
Ik heb dit gebouwd voor een klant met precies dat probleem, en wat ze vroegen was verfrissend eenvoudig. Elke site wordt gebouwd met zijn eigen naamgeving, zijn eigen root-wachtwoord en zijn eigen SSH-keys. Niemand op de site doet iets slims: ze zetten een server in het rack, prikken hem in het juiste VLAN en zetten hem aan. Niemand verzamelt serienummers, en niemand schrijft een antwoordbestand per machine. En een server die zo wordt toegevoegd, komt in het bestaande cluster van de site terecht.
| Wat ze wilden | Wat het hier levert |
|---|---|
| de eigen naamgeving, het wachtwoord en de keys van de site | één voorbereid antwoord per site, gekozen door het token van die site |
| in het juiste VLAN prikken, aanzetten, weglopen | DNS-discovery, iPXE, en Caddy die het token kiest op basis van het bronsubnet |
| geen serienummers, geen antwoordbestanden per machine | antwoorden filteren alleen op het product; de site komt uit het netwerk |
| de nieuwe server treedt toe tot het bestaande cluster | niet in dit stuk: PDM heeft geen first-boot-hook, zie verderop |
Dit stuk behandelt de eerste drie: een kale server van aanzetten tot een geïnstalleerde Proxmox-node met de instellingen van de juiste site. Die helft speelt bij bijna iedereen met meer dan één site, MSP of niet, dus het is de moeite waard om die apart te hebben. Toetreden tot het cluster is een aparte klus, en de sectie over de ontbrekende first-boot-hook legt uit waarom PDM dat nog niet voor je kan doen.
Proxmox Datacenter Manager (PDM) 1.1 heeft het ontbrekende stuk toegevoegd1. Het kan nu optreden als antwoordserver voor de geautomatiseerde installer van Proxmox, zodat een kale machine opstart, doorgeeft wat hij is, een voorbereid antwoord terugkrijgt en zichzelf installeert zonder dat er iemand achter de console zit. Voor een managed service provider (MSP) die PDM al als beheerconsole draait, is dat de voor de hand liggende plek om de installatie-instellingen van elke klant te bewaren.
Wat je niet wilt, is een PXE-server per klant. Elk daarvan is een Trivial File Transfer Protocol (TFTP)-server, een DHCP-configuratie, een set bootbestanden en een ISO die je opnieuw moet bouwen telkens als Proxmox een point release uitbrengt. Tien klanten zijn er tien om bij te houden. Dat schaalt niet. Het doel hier is één PXE-omgeving, één boot-image, één DNS-record en één PDM, waarbij elke site toch zijn eigen antwoord krijgt.
PDM 1.1 komt er zelf al grotendeels. Het schiet op twee punten tekort, en die wil je allebei kennen voordat je een uitrol eromheen plant: het kan een antwoord niet koppelen aan een site, en het kan een nieuwe node geen first-boot-script meegeven. Dit stuk bewijst beide uit de eigen broncode van PDM, werkt om het eerste heen, en bouwt het geheel op mijn lab-PDM met 1.1.9, met de proxy zo gehard dat de delen die geheimen bevatten dicht blijven.
Wat PDM 1.1 je geeft
De functie zit onder Remotes → Automated Installations en heeft drie delen: installaties, voorbereide antwoorden en authenticatietokens2.
Een voorbereid antwoord is de PDM-versie van het answer.toml van de installer, ingevuld via een formulier in plaats van met de hand geschreven.
| Deel | Wat het doet | Detail dat ertoe doet |
|---|---|---|
| Voorbereide antwoorden | land, toetsenbord, tijdzone, hash van het root-wachtwoord, SSH-keys, netwerk, schijfindeling, subscription key, actie na de installatie | voor elk request vers gerenderd |
| Doelfilters | bepalen welke machine welk antwoord krijgt | JSON Pointer in de systeeminformatie van de installer, glob voor de waarde3 |
| Templating | MiniJinja in de velden FQDN, admin-e-mail, IP, gateway en DNS | tellers zoals installation_nr gaan omhoog telkens als er een antwoord wordt geserveerd4 |
| Authenticatietokens | de installer stuurt Authorization: Bearer <name>:<secret> | een ander tokentype dan de API-tokens van PDM; elk antwoord somt de tokens op die het accepteert2 |
| Installaties | één record per request, met de volledige systeeminformatie | gaat van in uitvoering naar voltooid wanneer de post-install-webhook terugbelt |
| Rechten | Sys.Audit om te kijken, Sys.Modify om te wijzigen | allebei op /system/auto-installation4 |
De kant van de installer bestaat al een tijdje.
Het commando proxmox-auto-install-assistant prepare-iso bouwt een ISO die zijn antwoord over HTTP ophaalt, en sinds de ISO van Proxmox VE 9.2 kan het ook de kernel, initrd, ISO en boot.ipxe maken die iPXE nodig heeft om het geheel over het netwerk te booten5.
De tellers zijn het deel waar een losse answer.toml niets tegenover heeft.
Eén voorbereid antwoord nummert elke host die het bouwt, waar een statisch bestand per machine gekopieerd zou moeten worden.
Dat is een echte stap vooruit, en het blijft een eerste versie.
Het formulier is nog niet gelijkwaardig aan het losse antwoordbestand6, en één onderdeel ontbreekt helemaal: een first-boot-script bestaat in 1.1 in geen enkele vorm7.
Dat gat, en de locatie waarop het niet kan filteren, zijn de volgende twee secties.
Het kan niet matchen op de site
PDM kiest een antwoord door filters te vergelijken met de systeeminformatie die de installer post. De vraag is dus wat er in dat document staat, want wat er niet in staat, kan niet gematcht worden.
| Sleutel | Wat erin staat | Kan het de site noemen? |
|---|---|---|
product | fullname, product (pve, pbs, pmg), enable_btrfs | nee |
iso | release, isorelease | nee |
dmi | systeem, baseboard en chassis: naam, serienummer, UUID, SKU, asset tag | alleen als iemand de site in de asset tag heeft gezet |
network_interfaces | een lijst van {link, mac} | alleen via een inventaris die je zelf bijhoudt |
Dat is alles8,9. Geen adres, geen hostnaam, geen DHCP-optie, niets over waar de machine ingeplugd zit.
De velden van de Desktop Management Interface (DMI) zien er veelbelovend uit tot je echte exemplaren leest.
Elke installatie in mijn lab kwam terug met de systeemnaam Standard PC (Q35 + ICH9, 2009) en een leeg serienummer en lege asset tag.
Echte servers vullen ze wel in, maar met wat de leverancier erin zette en niet met de naam van je klant, en matchen op serienummers of MAC-adressen werkt alleen als je een lijst bijhoudt van elk chassis dat je ooit gaat installeren, en dat is precies de administratie die dit had moeten wegnemen.
Per host, ja. Per site, nee.
Op welk netwerk zit de machine dan? PDM komt het nooit te weten. De installer heeft zijn DHCP-lease al in handen als hij post, en het ene feit dat een MSP zeker weet over een nieuwe node, is precies het feit dat hij achterlaat. Matchen op het bronadres, IPv4 of IPv6, is een open featureverzoek, en ik heb het zelf ingediend als bug 826310.
Er zit een tweede beperking verstopt in de selectieregels, en mijn lab liep er recht in.
Het token wordt eerst gecontroleerd, en alleen antwoorden die op dat token zijn toegestaan, worden überhaupt bekeken, het standaardantwoord inbegrepen4.
PDM staat in het hele systeem precies één standaardantwoord toe.
Je kunt elke site dus geen eigen fallback geven met de standaardvlag.
Mijn lab had een standaardantwoord met de naam fallback op een eigen token, en het is nog nooit één keer geserveerd, omdat niets dat token ooit verstuurt.
Elke site heeft in plaats daarvan een vangnetfilter op zijn eigen token nodig, zoals /product/product=*, als laatste in het bestand.
Nog twee dingen uit dezelfde code die je in je hoofd wilt hebben als je debugt:
- De eerste match wint, in bestandsvolgorde, en een tweede match logt alleen een waarschuwing. Dat is de regel sinds 1.1.111.
- Een filter matcht alleen strings.
/product/enable_btrfsis een boolean en matcht nooit iets, en een array heeft een index nodig, zoals in/network_interfaces/0/mac.
Op 1.1.9 krijgt een onbekend of verlopen token een 401 na een bewuste vertraging van ongeveer een halve seconde, en laat het geen record achter.
Een goed token waar niets op matcht, krijgt een 400 met de body no answer file found, en die poging wordt vastgelegd, met de volledige systeeminformatie.
Dat record is de snelste manier om de echte serienummers en MAC-adressen van een nieuw chassis te lezen voordat je er een filter voor schrijft.
Het kan geen first-boot-script draaien
De installer zelf ondersteunt een first-boot-hook.
Een antwoord kan een sectie [first-boot] bevatten met source = "from-iso" of "from-url", een volgorde van before-network, network-online of fully-up, en optioneel een certificaatfingerprint9.
prepare-iso --on-first-boot sluit voor het geval from-iso een script in de ISO in.
PDM stuurt die sectie nooit.
Geen enkele keer.
Het eigen antwoordtype van de installer heeft het veld, first_boot: Option<FirstBootHookInfo>12.
Het voorbereide antwoord van PDM heeft er helemaal geen veld voor, en de code die daaruit het antwoord bouwt, schrijft onvoorwaardelijk first_boot: None4.
Er staat niets in het formulier, niets in de API en niets in het configuratiebestand dat je met de hand zou kunnen toevoegen.
Het is een open featureverzoek, bug 7699, ingediend in juni 2026 en bij het schrijven nog aan niemand toegewezen7.
Bijna vijf maanden later heeft Proxmox er niet op gereageerd, en op 8263 ook niet.
Dat zwijgen is het zorgwekkende deel. Iemand die de tijd neemt om op te schrijven hoe je product beter kan, verdient een antwoord, ook als dat antwoord nee is, en dat is gewone professionele beleefdheid.
Des te meer als de mensen die schrijven betalende klanten en partners zijn, die al geld in het product steken voordat ze er ook nog hun tijd in steken.
En het script op de ISO redt je niet.
De installer zet de first-boot-service alleen op if let Some(first_boot) = &answer.first_boot, en dat is het enige pad dat het ingesloten bestand ooit leest13.
prepare-iso waarschuwt niet als je een script insluit waar geen enkel antwoord ooit om zal vragen.
Het staat op de ISO en doet niets.
Dat weegt voor een MSP zwaarder dan voor wie dan ook.
Een node uit de installer is een kaal systeem met een gehasht root-wachtwoord en wat SSH-keys.
Het is geen PDM-remote, het zit niet in een cluster, en er staat niets op van je monitoring, repositories of storage.
In mijn lab werden alle drie de nodes afgerond en belden ze terug, en de remote SITEA in PDM moest daarna nog steeds met de hand worden toegevoegd.
Dus wat je ook gebruikt om een node af te maken, of dat nu Ansible is of iets anders, het moet van buitenaf worden aangestoten: vanuit de webhook na de installatie, vanuit een job die de lijst Installations in de gaten houdt, of door een mens. Ik laat het daarbij. Dit stuk gaat erover hoe één PXE-omgeving elke site bedient, en het first-boot-gat is een beperking om omheen te plannen, niet een die ik hier ga wegpoetsen.
Het token is de site
PDM ziet de site niet, maar het beperkt antwoorden wel per token, en een token is gewoon een header. Wie de header zet, kiest welke set antwoorden de installer kan krijgen. Dat is de hele truc.
Zet dus iets voor PDM dat weet waar het request vandaan kwam:
- Het boot-image bevat een catch-token,
catch-all:not-a-secret, dat in elk image hetzelfde is en hier gewoon gepubliceerd staat. Het is geen PDM-token en opent op zichzelf niets. - Een Caddy-reverse-proxy voor PDM kijkt naar het bronsubnet van een antwoordrequest met het catch-token, en vervangt de header door het echte token van die site.
- PDM ziet
site2-2026q4:<secret>en biedt alleen de antwoorden van site2 aan.
Het secret komt nooit in de buurt van het boot-image, dus een verloren USB-stick of een gekopieerde ISO levert niemand iets op. Het token van een site roteren is een wijziging in Caddy, geen nieuw image. En een site toevoegen is één regel Caddy-configuratie en één nieuw token in PDM.
Dit leunt erop dat het bronadres eerlijk is, en dat is een eigenschap van je netwerk, niet van de proxy. Die afweging en de andere staan hieronder onder “Wat er nog te beslissen valt”.
Hoe de installer PDM vindt zonder ingebakken URL
Eén image voor elke site werkt alleen als het image ook het adres van PDM niet bevat. De installer zoekt zijn antwoord-URL op drie plekken, in volgorde14:
| Regel | Wat de code doet | Wat het voor jou betekent |
|---|---|---|
| Volgorde | een URL in de ISO wint; anders DHCP; elke DHCP-fout valt terug op DNS14 | bouw het image zonder --url, en stuur nergens optie 250 |
| Recordnamen | proxmox-auto-installer.<search domain> voor de URL, proxmox-auto-installer-cert-fingerprint.<search domain> voor de fingerprint | één zone, twee TXT-records |
| Zoekdomein | de eerste regel search van /etc/resolv.conf, gesplitst op één spatie | geef precies één domein uit; twee worden één ongeldige naam |
| Geen domein | faalt met Could not find search domain in resolv.conf. | controleer dat DHCP-optie 15 of 119 wordt meegestuurd |
| TXT-waarde | dig txt +short, elke " verwijderd, daarna getrimd | een TXT-waarde die in meerdere strings is gesplitst, laat een spatie in de URL achter; houd het één string |
| Fingerprint | alleen uit DNS gehaald als de URL ook uit DNS kwam en er geen is ingebakken | publiceer de certificaatfingerprint van PDM, die Caddy ook serveert |
| Token | verstuurd als Authorization: Bearer <token>, in platte tekst opgeslagen op de ISO9 | en daarom bevat het image alleen het catch-token |
Het certificaat dat de installer controleert, is het certificaat dat op 443 antwoordt. In dit ontwerp serveert Caddy daar het eigen certificaat van PDM, zodat een client op 443 hetzelfde certificaat ziet als PDM op 8443 heeft, en er één fingerprint te publiceren is.
Bouwen: PDM, bootbestanden en DNS
De voorbeelden gebruiken het domein example.doc en drie sites, 10.10.50.0/24, 10.20.50.0/24 en 10.30.50.0/24.
.doc is geen echt topleveldomein, dus vervang het door je eigen domein.
PDM, Caddy, dnsmasq en de bootbestanden staan allemaal op één host, pdm.example.doc op 10.10.50.10, en de andere sites bereiken die via een DHCP-relay op hun gateways.
Beheer zit op 10.99.0.0/24 en is, als zodanig, het enige netwerk dat de web-UI van PDM ooit te zien krijgt.
Tokens en antwoorden in PDM
Voeg onder Remotes → Automated Installations → Authentication Tokens één token per site toe en geef het een vervaldatum die past bij je uitrolperiode.
De naam is geen geheim en je ziet hem terug in logs, dus laat hem iets zeggen: site1-2026q4.
Kopieer het secret op het moment dat het getoond wordt.
Voeg daarna per site per product een voorbereid antwoord toe, alleen toegestaan op het token van die site:
| Veld | site1, Proxmox VE |
|---|---|
| Toegestaan token | site1-2026q4 |
| Doelfilter | /product/product=pve |
| Land, toetsenbord, tijdzone | gb, en-gb, Europe/London |
| Root-toegang | SSH-keys, plus een wachtwoord ingesteld via het formulier, dat PDM alleen als hash opslaat en verstuurt4 |
| Netwerk | DHCP, interfacenamen vastgezet |
| FQDN | {{ product.product }}-site1-{{ installation_nr }}.example.doc |
| Schijf | je eigen indeling, bijvoorbeeld een ZFS-mirror over twee benoemde schijven |
| Webhook na de installatie | basis-URL https://pdm.example.doc, met de certificaatfingerprint van PDM |
De teller gaat omhoog telkens als er een antwoord wordt geserveerd, herhaalpogingen inbegrepen, dus namen kunnen nummers overslaan. De teller in mijn lab staat op 4 na drie voltooide installaties. Wil je voorspelbare namen, gebruik dan FQDN from DHCP en zet de hostnaam op de DHCP-reservering.
De basis-URL van de webhook is de enige instelling die voor dit ontwerp moet veranderen. Het ligt voor de hand om hem naar PDM zelf op poort 8443 te laten wijzen, zo had mijn lab het staan, en 8443 gaat zo dicht. Laat hem naar Caddy wijzen.
De bootbestanden
Op een willekeurige Proxmox VE- of Debian-host met de Proxmox-repository:
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/
Geen --url, want DNS levert die.
--pxe-loader ipxe impliceert --pxe, en met --pxe moet de output een map zijn die al bestaat15.
Je krijgt vier bestanden: vmlinuz, initrd.img omgepakt van zstd naar gzip omdat iPXE geen zstd-initrd kan laden, de voorbereide ISO met /boot eruit, en een boot.ipxe-menu dat na tien seconden standaard de geautomatiseerde installatie start en naar de andere bestanden verwijst met een relatieve naam15.
Houd alle vier in één map.
Elk doel heeft genoeg geheugen nodig om de kernel, de initrd en de hele ISO vast te houden.
De release notes van Proxmox VE 9.2 houden het op “at least 6 GiB of available memory on the host”5.
Dezelfde notes noemen een bekend probleem met de kernel van de 9.2-ISO op megaraid_sas-controllers en sommige NVMe-drives in JBOD-modus (just a bunch of disks), met het advies om vanaf de 9.1-ISO te installeren en te upgraden5.
Test je hardware voordat je het netwerkpad ermee vertrouwt.
De DNS-records
In de zone die past bij het ene zoekdomein dat DHCP uitdeelt:
$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"
De fingerprint is die van het eigen certificaat van PDM:
openssl x509 -in /etc/proxmox-datacenter-manager/auth/api.pem -noout -fingerprint -sha256
Controleer ze zoals de installer dat zal doen:
dig txt +short proxmox-auto-installer.example.doc @10.10.50.10
# one quoted string, nothing split
Caddy, de enige deur naar PDM
De API van PDM luistert op [::]:8443, en dat is een constante die in de binary is gecompileerd, geen instelling16.
Je kunt hem niet aan de loopback binden.
Dus wordt Caddy de enige weg naar binnen, en de firewall zorgt dat dat zo blijft.
PDM heeft precies twee routes die werken zonder PDM-login: POST /api2/json/auto-install/answer met een installertoken, en POST …/installations/{uuid}/post-hook met het eenmalige token in de body4.
Dat zijn de enige twee die een provisioningnetwerk ooit zou moeten bereiken.
Gebruik Caddy 2.11.1 of nieuwer. 2.11.1 repareerde een omzeiling van de path-matcher, CVE-2026-27587, en dat is precies het soort padfiltering waar dit op leunt17. Het eigen pakket van Debian 13 is 2.6.2, dus haal het uit trixie-backports, dat 2.11.2 heeft, of uit de eigen repository van Caddy18.
De 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
}
}
De delen die beveiligingswerk doen, en waarom elk deel er staat:
| Regel | Waarom |
|---|---|
header Authorization "Bearer catch-all:not-a-secret" in de site-matcher | zonder deze regel krijgt elke POST vanaf een sitesubnet het echte token; een request moet van het juiste subnet komen en het catch-token meesturen |
request_header Authorization … | zet de header en overschrijft wat de client ook stuurde, zodat een vervalst token wordt vervangen en nooit doorgestuurd19 |
remote_ip | de directe peer; zet geen NAT of load balancer tussen de installers en Caddy20 |
benoemde handle-blokken | blijven in de geschreven volgorde en sluiten elkaar uit, zodat de kale handle { abort } het laatste woord heeft21 |
@mgmt | de volledige PDM-UI en API, onaangeroerd, zodat de eigen login, API-tokens en cookies van PDM gewoon werken; een sitesubnet krijgt er niets van |
admin unix//…|0600 | de admin-API staat standaard op TCP localhost:2019, zonder authenticatie22 |
protocols h1 h2 | Caddy luistert standaard op UDP 443 voor HTTP/322; dit zet dat uit in plaats van een poort open te laten die niemand bedoelde te openen |
@hidden | file_server serveert elk bestand onder zijn root, dotfiles inbegrepen, en hide is gedocumenteerd als geen beveiligingsgrens23 |
Csrfpreventiontoken replace REDACTED | Caddy redigeert standaard Authorization en Cookie in zijn accesslog, maar niet de CSRF-header van PDM24 |
De path-matcher is vergevingsgezinder dan PDM, en dat is de goede kant op.
Caddy schoont het pad op, decodeert procent-encoding, voegt herhaalde slashes samen en negeert hoofdletters voordat hij matcht20.
Dus //answer, %61nswer en ANSWER tellen allemaal als de antwoordroute, en een variant /api2/extjs/… of een slash aan het eind matcht geen siteroute en valt door naar abort.
Twee keuzes daarin hebben uitleg nodig.
Caddy serveert het certificaat van PDM, rechtstreeks uit de bestanden van PDM.
Caddy leest api.pem en api.key waar PDM ze bewaart, zodat de installers, de webhook en een browser allemaal het certificaat zien dat PDM op 8443 presenteert, en er geen tweede kopie is die uit de pas kan lopen.
De voor de hand liggende manier om hem die toegang te geven, is de gebruiker caddy aan www-data toevoegen.
In mijn lab heb ik gecontroleerd wat dat groepslidmaatschap werkelijk bereikt, door elk bestand als de gebruiker caddy te testen:
Bestand onder /etc/proxmox-datacenter-manager/ | Leesbaar voor caddy in www-data |
|---|---|
remotes.shadow, de inloggegevens voor elke beheerde remote | ja |
autoinst/tokens.shadow, elk secret van elk installertoken | ja |
auth/api.key, auth/csrf.key | ja |
access/user.cfg, subscriptions/ | ja |
Niets in die lijst hoort ook maar in de buurt van een webproxy. Een bug in de proxy zou dan een compromittering zijn van elk cluster dat PDM beheert.
Dus blijft Caddy uit www-data en krijgt hij een ACL op precies de twee bestanden die hij nodig heeft, plus toestemming om door de twee mappen erboven te gaan zonder ze te mogen opsommen:
#!/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"
Getest als de gebruiker caddy in het lab, nadat het script had gedraaid:
| Pad | caddy |
|---|---|
auth/api.pem, auth/api.key | kan lezen |
auth/csrf.key, auth/authkey.key, auth/authkey.pub | kan niet lezen |
remotes.shadow, remotes.cfg, autoinst/tokens.shadow, access/user.cfg | kan niet lezen |
auth/ opsommen | geweigerd |
Het addertje is de vernieuwing. PDM schrijft een vernieuwd certificaat naar een nieuw bestand en hernoemt dat op zijn plek, en een nieuw bestand neemt de ACL van het oude niet over. Dus als PDM vernieuwt, zet systemd de ACL terug en herlaadt Caddy, zonder dat er iemand aan te pas komt.
Een controle dat het certificaat en de key bij elkaar horen, want een vernieuwing schrijft ze één voor één:
#!/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" ]
Een path-unit die beide bestanden in de gaten houdt, en de service die hij start:
# /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
De override verderop draait hetzelfde ACL-script voor elke start van Caddy, zodat een vernieuwing terwijl Caddy stillag nooit gemist wordt.
De details die het laten werken, elk gecontroleerd:
| Onderdeel | Waarom |
|---|---|
| beide bestanden bewaakt | de eerste write start de unit; de conditie slaat hem over zolang het paar niet klopt, en de tweede write start hem opnieuw met een kloppend paar |
ExecCondition | een mislukte conditie slaat de run over in plaats van hem te laten falen, zodat een half geschreven vernieuwing Caddy nooit bereikt25 |
PathChanged | gaat af als een bewaakt bestand dat open stond om te schrijven, wordt gesloten26; in het lab ging hij ook af als een nieuw bestand over het oude werd hernoemd |
| het ACL-script bij elke run | een binnen-hernoemd bestand heeft geen ACL, dus Caddy kon het niet lezen tot het script opnieuw draait |
try-reload-or-restart | herlaadt Caddy als hij draait en doet niets als hij niet draait |
reload --force in de unit van Caddy | zonder --force ziet Caddy dezelfde config en blijft hij het oude certificaat serveren |
Die laatste rij is geen gok.
In een wegwerpcontainer met Caddy 2.11.7 heb ik het certificaatbestand achter een draaiende server verwisseld: een gewone caddy reload bleef het oude certificaat serveren, en caddy reload --force serveerde het nieuwe.
De meegeleverde unit herlaadt al met --force27, dus houd die regel aan als je ExecReload overschrijft.
In het lab heb ik de api.key en api.pem van PDM vervangen zoals een vernieuwing dat doet, door nieuwe bestanden te schrijven en die over de oude te hernoemen.
Direct na het hernoemen had api.key geen ACL-regel voor caddy.
Binnen vier seconden was de path-unit afgegaan, stond de ACL weer op beide bestanden, en had Caddy op zijn plek herladen met hetzelfde proces-ID.
De tokennaam en het secret worden gescheiden gehouden.
De naam gaat in de Caddyfile, waar hij gelogd wordt.
Het secret gaat in een environmentbestand dat alleen root kan lezen, en Caddy leest het via {env.*} op het moment van het request.
Log welk token gebruikt werd, nooit het secret
De eerste versie hiervan in mijn lab lekte.
De meegeleverde unit van Caddy start hem als caddy run --environ, en --environ print de hele environment voordat hij start28,27.
Elke herstart zette het sitetoken in platte tekst in het journal: acht regels ervan op het moment dat ik ze telde.
Niets in de accesslog, niets in de eigen logs van PDM, alles van die ene vlag.
Dus bevat het secretsbestand secrets en verder niets:
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
en de unit-override laat --environ vallen en geeft de admin-socket een plek:
# 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
De + draait die regels als root, voordat Caddy als zijn eigen gebruiker start.
De logregels doen ertoe zodra iets anders aan /var/log/caddy komt: een log die door root is geroteerd, of een bestand dat tijdens het testen met de hand is aangemaakt, laat Caddy zonder toegang tot zijn eigen accesslog.
In het lab heb ik pdm.log op root:root gezet en Caddy herstart; hij kwam terug als caddy:caddy en Caddy logde geen permissiefouten.
Herstart in plaats van herladen na het bewerken, want een reload leest de environment niet opnieuw in.
Gebruik {env.NAME}, nooit {$NAME}: de tweede vorm wordt ingevuld wanneer de Caddyfile wordt geadapteerd, zodat het secret in de JSON-config belandt, in het autosave-bestand en in alles wat die dumpt29.
Ook hier doet de admin-socket ertoe.
{env.*} lost alles in de procesenvironment op zonder allowlist, dus iedereen die een config kan laden via een admin-API op TCP, kan Caddy een secret aan hem laten teruglezen30.
Elke antwoordregel in de accesslog noemt nu de site en het token, met de header zelf geredigeerd. Uit het lab, waar beide siteA heten:
{"site":"siteA","pdm_token":"siteA","pdm_status":400,"status":400,
"request":{"headers":{"Authorization":["REDACTED"]}}}
Ik heb het gecontroleerd in plaats van erop te vertrouwen.
Op een podman-kopie van deze config met dummy-secrets, en daarna in het lab, stuurde ik antwoorden via elk pad dat ik kon bedenken: normaal, debuglogging aan, upstream plat voor een 502, een reload en een herstart.
Daarna telde ik elk secret in het journal, de accesslog, de config- en datamappen van Caddy en de geadapteerde JSON.
Elke telling was nul.
De controlerun, dezelfde config gestart met --environ, printte het secret twee keer.
Twee kanttekeningen bij die test.
Met debug aan logt Caddy zijn hele geadapteerde config, dus het catch-token en de {env.*}-placeholders duiken daar op; de waarden erachter niet.
Houd debug in productie hoe dan ook uit.
En als een secret al door een journal is gegaan, zoals het mijne, genereer het dan opnieuw in PDM en werk het bestand bij; logregels verwijderen is niet de oplossing.
De firewall: 8443 verlaat de machine nooit
Mijn lab had helemaal geen firewall, dus 8443 antwoordde elke host op het provisioningnetwerk en elke regel in de Caddyfile was te omzeilen. Doe hem dicht. nftables, met drop als standaard:
#!/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;
}
}
De verbindingen van PDM naar zijn remotes zijn uitgaand, dus 8443 inkomend dichtzetten verandert daar niets. Laad een nieuwe ruleset de eerste keer met een getimede rollback, zodat een fout je negentig seconden kost in plaats van een tocht naar de 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
In het lab, na dit alles:
| Controle | Resultaat |
|---|---|
| 8443 vanaf het beheernetwerk | geen verbinding; de drop-teller loopt op |
| PDM-web-UI via Caddy op 443, vanaf beheer | logt in en werkt |
| PDM-web-UI vanaf een siteadres | afgebroken |
| antwoord-POST vanaf een site met het catch-token | bereikt PDM met het sitetoken: een bewust niet-matchende probe kreeg 400 no answer file found |
| dezelfde probe rechtstreeks naar PDM met een verzonnen token | 401 |
antwoord-POST zonder het catch-token, of als /api2/extjs/… | afgebroken |
bestanden waarin de secrets van PDM staan, gelezen als caddy | geen enkele leesbaar |
| het sitesecret in het journal, de accesslog of de data van Caddy sinds de wijziging | 0 |
DHCP, iPXE en de lus
Serverfirmware kan zijn eerste bestand alleen over TFTP ophalen, dus TFTP serveert de kleine iPXE-loader en iPXE haalt al het andere over HTTP op.
De valkuil is dat iPXE zijn eigen DHCP-request stuurt, en een server die daarop opnieuw ipxe.efi antwoordt, stuurt de machine eeuwig in een kringetje rond.
De DHCP-server moet de firmware en iPXE uit elkaar kunnen houden.
| DHCP-server | Hoe hij de lus doorbreekt |
|---|---|
| dnsmasq | tag requests met optie 175, die alleen iPXE stuurt, en geef die de HTTP-URL31 |
| ISC dhcpd | if exists user-class and option user-class = "iPXE"32 |
| FortiGate | het veld filename neemt één waarde, dus bouw een iPXE-loader met de URL erin en serveer die aan alles |
dnsmasq, dat zijn eigen netwerk bedient en de andere twee via relays, met alleen reserveringen:
# /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 betekent geen pool: alleen gereserveerde machines krijgen een adres, en dat houdt vreemden van het installerpad af.
Het adres van elke reservering moet binnen de eigen /24 van zijn site liggen, want dat adres gebruikt Caddy om het token te kiezen.
Een node die in het verkeerde subnet is gereserveerd, krijgt het antwoord van de verkeerde site, of niets.
Reserveringen zijn een lijst met MAC-adressen, en dat is boekhouding per machine van precies het soort dat de klant niet wilde.
Is inprikken en booten echt de eis, geef elk provisioning-VLAN dan in plaats daarvan een kleine dynamische range, bijvoorbeeld dhcp-range=set:site1,10.10.50.100,10.10.50.150,255.255.255.0,1h.
De site komt nog steeds uit het subnet, dus Caddy kiest nog steeds het juiste token, en de FQDN-template met installation_nr geeft de node zijn naam.
De prijs is de eerste rij van “Wat er nog te beslissen valt”: alles wat in dat VLAN wordt geprikt, kan nu het antwoord van de site ophalen, dus het VLAN zelf wordt de poort.
dnsmasq onderhandelt de TFTP-blokgrootte met de client.
Laat je het met rust, dan begrenst het die op de MTU van zijn eigen interface31, en dat werkt tot het pad naar een site kleiner is dan de link waar dnsmasq op zit.
Daarom legt de config het plafond vast als tftp-mtu=1500 in plaats van het over te nemen.
Zo staat het getal voor de neus van wie een site achter een IPsec-tunnel beheert, of achter iets anders dat van het pakket afknabbelt, en die verlaagt het tot wat het pad echt draagt in plaats van het te ontdekken via een transfer die halverwege vastloopt.
Als één NIC-ROM zich misdraagt zodra de optie überhaupt wordt toegekend, zet tftp-no-blocksize de onderhandeling uit.
Kopieer ipxe.efi uit het Debian-pakket ipxe naar /srv/tftp, met de gebruiker dnsmasq als eigenaar, want tftp-secure weigert al het andere.
Kies bij voorkeur UEFI.
Secure Boot heeft een gesigneerde iPXE nodig, dus richt dat in of zet Secure Boot uit voor de installatie en daarna weer aan.
Booten, kijken en wat er stukgaat
Test met één virtuele machine op een sitenetwerk voordat je aan hardware komt: UEFI-firmware, 8 GiB geheugen en een NIC op dat VLAN reproduceren de hele keten.
En het geheel op volgorde, van aanzetten tot een installatie die PDM als Finished markeert:
| Stap | Wat er gebeurt | Waar je kijkt |
|---|---|---|
| 1 | firmware haalt ipxe.efi over TFTP, iPXE haalt boot.ipxe over HTTP | journalctl -u dnsmasq, de log van Caddy |
| 2 | na tien seconden boot het menu de geautomatiseerde optie; kernel, initrd en ISO worden gedownload | vier GET’s, waarvan één van ongeveer 1.6 GB |
| 3 | de installer neemt een lease, leest het zoekdomein en vraagt beide TXT-records op | /tmp/fetch_answer.log op het doel |
| 4 | POST naar Caddy, token verwisseld, PDM kiest een antwoord | de log van Caddy noemt het token; PDM toont een nieuwe installatie |
| 5 | de installatie draait | /tmp/auto_installer.log, /tmp/install-low-level-start-session.log |
| 6 | nog in de installer belt de webhook terug via Caddy, daarna reboot de machine33 | de installatie gaat naar Finished met schijven, NIC’s en SSH-hostkeys erbij |
Bij een fout valt de installer terug op een shell, tenzij het antwoord Reboot on Error zet, dus laat dat uit terwijl je test.
| Symptoom | Waarschijnlijke oorzaak |
|---|---|
Could not find search domain in resolv.conf. | DHCP stuurde geen domein |
| een TXT-query voor een naam met een spatie erin | meer dan één zoekdomein |
Got empty response. | record ontbreekt, verkeerde zone, of optie 6 wijst naar een andere DNS-server |
| een URL met een spatie in het midden | TXT-waarde in meerdere strings gesplitst |
| de log zegt dat de URL uit DHCP kwam | optie 250 staat ergens op het netwerk |
| TLS-fout op de POST | de DNS-fingerprint is niet het huidige certificaat van Caddy |
| verbinding gesloten zonder antwoord | Caddy brak hem af: verkeerd subnet, geen catch-token, of een route die niet een van de twee is |
400 no answer file found | het token werkte; geen filter matchte en er is geen standaardantwoord op dat token |
| het verkeerde antwoord | overlappende filters; de eerste in het bestand wint |
| status blijft in uitvoering | de basis-URL van de webhook wijst nog naar 8443 |
| iPXE laadt zichzelf eindeloos opnieuw | de DHCP-server houdt iPXE en firmware niet uit elkaar |
PXE-E32 TFTP-timeout | next-server verkeerd, of UDP 69 geblokkeerd |
Proxmox Backup Server toevoegen
Niets aan het pad is specifiek voor Proxmox VE.
Bouw een tweede set bootbestanden uit de ISO van PBS 4 met hetzelfde catch-token, in /srv/www/pbs-4/, en voeg per site één PBS-antwoord toe met het filter /product/product=pbs, toegestaan op het token van die site.
De DNS-records, Caddy, de tokens en de firewall veranderen niet.
Voor een site met allebei moet iets beslissen welk product een machine krijgt, en een menu is het verkeerde ding om dat te laten beslissen.
De choose van iPXE zonder --timeout wacht eeuwig aan de console op een toetsaanslag, en dat is het tegenovergestelde van unattended.
Laat de DHCP-reservering het beslissen.
Je reserveert elke node al op MAC-adres, dus tag de PBS-nodes en stuur elke tag zijn eigen 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 op een reservering tagt die ene machine31, en tag:!pbs houdt de twee bootregels wederzijds uitsluitend, zodat er geen vraag is welke wint.
De eigen boot.ipxe van elk product boot al na tien seconden de geautomatiseerde optie, zonder iemand aan het toetsenbord15.
Deze regels komen door dnsmasq --test op 2.91, de versie in mijn lab, en volgen de gedocumenteerde syntaxis van dhcp-host en tags.
Ik heb nog geen PBS-node via deze regels geboot met PXE, dus probeer één getagde machine voordat je erop vertrouwt.
Vanaf daar is het hetzelfde pad als bij Proxmox VE.
De installer van PBS post "product": "pbs", Caddy zet het token van de site erin, en PDM kiest het PBS-antwoord van die site.
Nergens wordt iets met de hand gekozen, en welk product een node wordt, staat op één plek, de reservering, naast zijn adres.
| Wat beslist | Waar het staat |
|---|---|
| welke ISO boot | de tag set:pbs op de DHCP-reservering |
| welke antwoorden van welke site gelden | het bronsubnet, via de tokenwissel van Caddy |
| welk antwoord binnen die site | het filter /product/product=pbs in PDM |
De installer legt alleen het besturingssysteem neer. De datastore, op storage los van de OS-schijf, is weer een taak voor wat de node afmaakt, en dat brengt ons terug bij de ontbrekende first-boot-hook.
Wat er nog te beslissen valt
Geen van deze punten houdt je tegen om dit ontwerp te gebruiken. Elk ervan heeft wel een eigenaar nodig.
| Afweging | Waarom het ertoe doet | Wat je doet |
|---|---|---|
| het bronadres is de poort | alles op een provisioning-VLAN, of wat daar een adres kan spoofen, kan het antwoord van die site ophalen: hash van het root-wachtwoord, SSH-keys, subscription key | isoleer het VLAN, port security, alleen reserveringen, vervaldatum op tokens, tokens uitzetten tussen uitrollen |
| het catch-token is geen geheim | het zit in elk image en staat op deze pagina | het filtert alleen verdwaalde requests weg; het subnet en het echte token in Caddy zijn de poort |
| Caddy kan de key van PDM lezen | het certificaat van PDM serveren betekent dat de proxy die ene private key leest | een ACL op alleen api.pem en api.key, verder niets van PDM leesbaar, de host dichtgetimmerd; de key verlaat hem nooit |
| DNS wordt vertrouwd | een vervalst TXT-record stuurt de installer ergens anders heen, en als de fingerprint ook uit DNS komt, beheerst de vervalser allebei | bescherm de provisioning-DNS, of bak de fingerprint van PDM in met --cert-fingerprint en bouw opnieuw als die verandert |
| bootbestanden gaan over gewone HTTP | ze kunnen onderweg gelezen of verwisseld worden | houd het netwerk geïsoleerd; iPXE over HTTPS als je build dat ondersteunt |
| het webhooktoken | 16 willekeurige bytes, eenmalig, gewist na de callback, maar zonder tijdslimiet4 | ruim oude installatierecords op |
| de subscription key | één keer geactiveerd nadat het netwerk op is; bij een fout wordt de key verwijderd en faalt de unit, zonder nieuwe poging34 | controleer systemctl --failed op nieuwe nodes |
| PDM is één enkel punt | het is vandaag niet high-available | een storing stopt nieuwe installaties, geen draaiende clusters |
| één beheer-NIC | het antwoord heeft geen opties voor bond, VLAN of bridge9 | dat is werk na de installatie, met hetzelfde first-boot-probleem |
Een eerste versie, degelijk gebouwd
Dit is een eerste release van de functie, en voor een eerste release is hij goed. Het antwoordformulier is verstandig, de selectieregels zijn helder zodra je ze leest, de webhook werkt, en de code is open, en dat is de enige reden dat alles hierboven bewezen kon worden in plaats van geraden. De patchserie ging vier rondes over de openbare mailinglijst voordat hij landde35, en dat zie je.
De gaten zijn precies de twee dingen die van een installatieserver een provisioningsysteem maken. Een site is de eenheid waarin een MSP denkt, en de installer staat erop als hij de vraag stelt. Een first-boot-hook is het verschil tussen een node die geïnstalleerd is en een node die af is. Beide zijn al half aanwezig: de installer heeft de hook en PDM heeft het token. Ik verwacht dat ze allebei komen, en als dat gebeurt, wordt de Caddy in het midden van dit ontwerp eenvoudiger in plaats van te verdwijnen, want een proxy die de secrets van PDM buiten bereik houdt en 8443 van het netwerk, is hoe dan ook de moeite waard.
De reden om het in de tussentijd zo te doen, is die van bovenaan. Tien PXE-servers zijn tien dingen die elk in hun eigen tempo verouderen, en elk ervan wordt pas bekeken als het faalt op de dag dat je het nodig hebt. Eén omgeving, één keer bijgehouden, waarbij het netwerk de site bepaalt en de secrets op één plek liggen, is minder om te beheren. Minder om te beheren is minder om fout te doen, en dat is het grootste deel van het werk.
Proxmox — persbericht Proxmox Datacenter Manager 1.1 — geautomatiseerde installaties, met een nieuw tabblad onder Remotes dat de voortgang van installaties bijhoudt. ↩︎
Documentatie Proxmox Datacenter Manager — Automated Installations — voorbereide antwoorden, doelfilters, templating, en “a dedicated token mechanism, separate from the normal API tokens”. ↩︎ ↩︎
proxmox-datacenter-manager,
lib/pdm-api-types/src/auto_installer.rs— sleutels van doelfilters “are JSON Pointers as per [RFC6901], the values globs”; slechts één standaardantwoord. ↩︎proxmox-datacenter-manager,
server/src/api/auto_installer/mod.rs— tokencontrole vóór het standaardantwoord, eerste match in bestandsvolgorde,first_boot: None, ophogen van de teller, het eenmalige post-hook-token, de twee routes die zonder PDM-login bereikbaar zijn, en de rechtenSys.Audit/Sys.Modifyop/system/auto-installation. ↩︎ ↩︎ ↩︎ ↩︎ ↩︎ ↩︎ ↩︎Proxmox VE Roadmap — Proxmox VE 9.2 — voorbereiding voor PXE en iPXE, “at least 6 GiB of available memory on the host”, en het bekende probleem met
megaraid_sasen NVMe-JBOD. ↩︎ ↩︎ ↩︎Proxmox Bugzilla 8078 — “prepared answer disk setup does not allow to specify multiple udev filters with same name”, geopend op 26 september 2026, status NEW op 7 november 2026. ↩︎
Proxmox Bugzilla 7699 — “Add on-first-boot parameter to the PDM remote installation”, geopend op 12 juni 2026, status NEW op 7 november 2026. ↩︎ ↩︎
proxmox,
proxmox-installer-types/src/lib.rs—SystemInfo: product, ISO, DMI en netwerkinterfaces. ↩︎Proxmox VE wiki — Automated Installation — de opties
[first-boot]en[network], de systeeminformatie die wordt verstuurd, en dat het auth-token “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”, geopend op 31 oktober 2026, status NEW op 7 november 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—AutoInstallerConfigbevatfirst_boot: Option<FirstBootHookInfo>. ↩︎pve-installer,
proxmox-auto-installer/src/bin/proxmox-auto-installer.rs— de first-boot-executable wordt alleen opgezet als het antwoord een sectiefirst_bootheeft. ↩︎pve-installer,
proxmox-fetch-answer/src/fetch_plugins/http.rs— eerst de URL in de ISO, dan DHCP, dan DNS; de TXT-namen; het parsen vanresolv.conf;dig txt +shortmet de aanhalingstekens eruit; de Bearer-header. ↩︎ ↩︎pve-installer,
proxmox-auto-install-assistant/src/main.rs—--pxe-loaderimpliceert--pxe, de outputmap, de gzip-initrd,/bootverwijderd uit de ISO, en de gegenereerdeboot.ipxe. ↩︎ ↩︎ ↩︎proxmox-datacenter-manager,
server/src/bin/proxmox-datacenter-api/main.rs—PDM_LISTEN_ADDRis het ongespecificeerde IPv6-adres opPDM_PORT, 8443, vastgelegd bij het bouwen. ↩︎Caddy releases — 2.11.1 repareerde CVE-2026-27587, een omzeiling van de path-matcher via escape-reeksen. ↩︎
Debian sources — caddy —
2.6.2-12+deb13u1in trixie,2.11.2-1~bpo13+1in trixie-backports. ↩︎Caddy —
request_header— “With no prefix, the field is set (overwritten).” ↩︎Caddy — Request matchers —
remote_ipis de directe peer; padmatches zijn hoofdletterongevoelig, URL-gedecodeerd, opgeschoond, met herhaalde slashes samengevoegd. ↩︎ ↩︎Caddy —
handle— “only the first matchinghandleblock will be evaluated”. ↩︎Caddy — Global options — admin staat standaard op
localhost:2019;protocolsstaat standaard oph1 h2 h3. ↩︎ ↩︎Caddy —
file_server—hidemoet niet als beveiligingsgrens worden beschouwd. ↩︎Caddy —
log—Cookie,Set-Cookie,AuthorizationenProxy-Authorizationworden gelogd alsREDACTED. ↩︎systemd.service —
ExecCondition=— bij een exitcode van 1 tot en met 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 —
--environprint de environment voordat hij start. ↩︎Caddy — Caddyfile-concepten, environmentvariabelen —
{$NAME}wordt ingevuld voordat de Caddyfile wordt geparsed. ↩︎caddyserver/caddy,
replacer.go— elke sleutel die metenv.begint, wordt aanos.Getenvdoorgegeven. ↩︎manpagina van dnsmasq —
dhcp-match,dhcp-boot,dhcp-hostmetset:<tag>,enable-tftp,tftp-secure,tftp-mtu,tftp-no-blocksizeenstatic-ranges. ↩︎ ↩︎ ↩︎iPXE — Configuring ISC dhcpd —
if exists user-class and option user-class = "iPXE"geeft iPXE het script en de firmware de loader. ↩︎pve-installer,
unconfigured.sh—proxmox-post-hookdraait direct nadatproxmox-auto-installerslaagt, vóór de reboot. ↩︎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 — de serie die de functie toevoegde, met de wijzigingsnotities van v3 naar v4. ↩︎