Una consola, muchos clientes

Si te ocupas de Proxmox para otros, cada nodo nuevo es el mismo trabajo hecho de forma un poco distinta. Cada cliente tiene su país, su zona horaria, sus claves de root, su disposición de discos, su forma de nombrar los hosts y su clave de suscripción. Si te equivocas en una de ellas en un nodo recién instalado, te enteras la primera vez que alguien inicia sesión, o la primera vez que una auditoría pregunta por qué el reloj va una hora desfasado.

Monté esto para un cliente con exactamente ese problema, y lo que pidió fue de lo más sencillo. Cada sede se instala con su propia forma de nombrar, su propia contraseña de root y sus propias claves SSH. Nadie en la sede hace nada ingenioso: montan un servidor en el rack, lo conectan a la VLAN correcta y lo encienden. Nadie recopila números de serie, y nadie escribe un archivo de respuesta por máquina. Y un servidor añadido así acaba en el clúster que la sede ya tiene.

Lo que queríanLo que lo resuelve aquí
el nombre, la contraseña y las claves propios de la sedeuna respuesta preparada por sede, elegida por el token de esa sede
conectar a la VLAN correcta, encender y marcharsedescubrimiento por DNS, iPXE, y Caddy eligiendo el token según la subred de origen
sin números de serie, sin archivos de respuesta por máquinalas respuestas filtran solo por el producto; la sede sale de la red
el servidor nuevo se une al clúster existenteno en este artículo: PDM no tiene gancho de primer arranque, mira más abajo

Este artículo cubre las tres primeras: llevar un servidor vacío desde el encendido hasta un nodo Proxmox instalado con los ajustes de la sede correcta. Esa mitad es la que le toca a casi cualquiera con más de una sede, sea MSP o no, así que merece la pena tenerla por separado. Unirse al clúster es otro trabajo, y la sección sobre el gancho de primer arranque que falta explica por qué PDM todavía no puede hacerlo por ti.

Proxmox Datacenter Manager (PDM) 1.1 añadió la pieza que faltaba1. Ahora puede hacer de servidor de respuestas para el instalador automatizado de Proxmox, así que una máquina vacía arranca, envía lo que es, recibe una respuesta preparada y se instala sola sin nadie delante de la consola. Para un proveedor de servicios gestionados (MSP) que ya usa PDM como consola de gestión, ese es el sitio obvio donde guardar los ajustes de instalación de cada cliente.

Lo que no quieres es un servidor PXE por cliente. Cada uno es un servidor Trivial File Transfer Protocol (TFTP), una configuración DHCP, un juego de archivos de arranque y una ISO que reconstruir cada vez que Proxmox saca una versión menor. Diez clientes son diez de esos que mantener al día. Eso no escala. El objetivo aquí es un entorno PXE, una imagen de arranque, un registro DNS y un PDM, y que cada sede siga recibiendo su propia respuesta.

Un entorno PXE para todas las sedessite110.10.50.0/24site210.20.50.0/24site310.30.50.0/24Caddy en el 443entra el token comodínsale el de la sede,elegido por subredPDMrespuestas acotadaspor token8443 en loopbackUna imagen de arranque, un registro DNS y un servicio DHCP para todas las sedes
El diseño terminado. Todas las sedes arrancan la misma imagen. Caddy averigua de qué sede viene una petición por su subred y entrega a PDM el token de esa sede, y PDM solo ofrece las respuestas asociadas a ese token.

PDM 1.1 llega casi hasta el final por sí solo. Se queda corto en dos sitios, y conviene conocer los dos antes de planificar un despliegue a su alrededor: no tiene forma de casar una respuesta con una sede, y no tiene forma de dar a un nodo nuevo un script de primer arranque. Este artículo demuestra las dos cosas con el propio código fuente de PDM, rodea la primera y monta todo el conjunto sobre el PDM de mi laboratorio con la versión 1.1.9, con el proxy endurecido para que las partes que guardan secretos sigan cerradas.

Lo que te da PDM 1.1

La función está en Remotes → Automated Installations y tiene tres partes: instalaciones, respuestas preparadas y tokens de autenticación2. Una respuesta preparada es la versión de PDM del answer.toml del instalador, rellenada con un formulario en lugar de escrita a mano.

ParteQué haceEl detalle que importa
Respuestas preparadaspaís, teclado, zona horaria, hash de la contraseña de root, claves SSH, red, disposición de discos, clave de suscripción, acción tras la instalaciónse generan de nuevo en cada petición
Filtros de destinodeciden qué máquina recibe qué respuestaJSON Pointer dentro de la información del sistema que envía el instalador, glob para el valor3
PlantillasMiniJinja en los campos de FQDN, correo del administrador, IP, puerta de enlace y DNScontadores como installation_nr suben cada vez que se sirve una respuesta4
Tokens de autenticaciónel instalador envía Authorization: Bearer <name>:<secret>un tipo de token distinto de los tokens de API de PDM; cada respuesta enumera los tokens que acepta2
Instalacionesun registro por petición, con la información completa del sistemapasa de en curso a terminada cuando llama el webhook de postinstalación
PermisosSys.Audit para mirar, Sys.Modify para cambiarlos dos sobre /system/auto-installation4

La parte del instalador lleva tiempo ahí. El comando proxmox-auto-install-assistant prepare-iso construye una ISO que va a buscar su respuesta por HTTP, y desde la ISO de Proxmox VE 9.2 también puede generar el kernel, el initrd, la ISO y el boot.ipxe que iPXE necesita para arrancarlo todo por la red5.

Los contadores son la parte que un answer.toml independiente no tiene. Una sola respuesta preparada numera cada host que construye, donde un archivo estático habría que copiarlo por máquina. Es un paso adelante de verdad, y sigue siendo una primera versión. El formulario aún no está a la par del archivo de respuestas independiente6, y hay un área que falta por completo: en la 1.1 no existe un script de primer arranque, de ninguna forma7. Esa carencia, y la ubicación por la que no puede filtrar, son las dos secciones siguientes.

No puede casar con la sede

PDM elige una respuesta comparando filtros con la información del sistema que envía el instalador. Así que la pregunta es qué hay en ese documento, porque lo que no está en él no se puede casar.

ClaveQué contiene¿Podría nombrar la sede?
productfullname, product (pve, pbs, pmg), enable_btrfsno
isorelease, isoreleaseno
dmisistema, placa base y chasis: nombre, número de serie, UUID, SKU, etiqueta de inventariosolo si alguien escribió la sede en la etiqueta de inventario
network_interfacesuna lista de {link, mac}solo a través de un inventario que lleves tú

Eso es todo8,9. No hay dirección, ni nombre de host, ni opción DHCP, nada sobre dónde está enchufada la máquina.

Los campos de Desktop Management Interface (DMI) parecen prometedores hasta que lees unos de verdad. Todas las instalaciones de mi laboratorio volvieron con el nombre de sistema Standard PC (Q35 + ICH9, 2009) y el número de serie y la etiqueta de inventario vacíos. Los servidores reales sí los rellenan, pero con lo que puso el fabricante y no con el nombre de tu cliente, y casar por números de serie o direcciones MAC solo funciona si llevas una lista de cada chasis que vayas a instalar en tu vida, que es justo el papeleo que esto tenía que quitar. Por host, sí. Por sede, no.

Entonces, ¿a qué red está enchufada la máquina? PDM nunca se entera. El instalador tiene su concesión DHCP en la mano cuando envía la petición, y el único dato que un MSP conoce con certeza sobre un nodo nuevo es justo el que se deja atrás. Filtrar por la dirección de origen, IPv4 o IPv6, es una petición de funcionalidad abierta, y la abrí yo mismo como el bug 826310.

Hay un segundo límite escondido en las reglas de selección, y mi laboratorio se metió de lleno en él.

Cómo elige PDM una respuesta1. ¿Token válido?activo, sin caducarno401 tras medio segundo o asíno se registra2. Respuestas de ese tokenen el orden del archivo, nada más3. ¿Casan todos los filtros?solo cadenas, con globsla primerarespuesta generadacada contador suyo sube 1ninguna4. ¿Por defecto en este token?una por defecto en todo PDMno400: no answer file foundregistrado en Installations
La selección de PDM, sacada del código fuente. El token se comprueba primero y acota todo lo que viene después, incluida la respuesta por defecto.

El token se comprueba primero, y solo se tienen en cuenta las respuestas autorizadas para ese token, incluida la respuesta por defecto4. PDM permite exactamente una respuesta por defecto en todo el sistema. Así que no puedes dar a cada sede su propia respuesta de reserva con la marca de respuesta por defecto. Mi laboratorio tenía una respuesta por defecto llamada fallback con su propio token, y no se ha servido ni una vez, porque nada envía nunca ese token. Cada sede necesita en su lugar un filtro que lo case todo con su propio token, como /product/product=*, colocado el último en el archivo.

Dos cosas más del mismo código que conviene tener en la cabeza cuando depuras:

  • Gana la primera coincidencia, en el orden del archivo, y una segunda coincidencia solo deja un aviso en el log. Esa es la regla desde la 1.1.111.
  • Un filtro solo casa con cadenas. /product/enable_btrfs es un booleano y no casa con nada, y un array necesita un índice, como en /network_interfaces/0/mac.

En la 1.1.9 un token desconocido o caducado recibe un 401 tras un retardo deliberado de más o menos medio segundo y no deja registro. Un token bueno sin nada que case recibe un 400 con el cuerpo no answer file found, y ese intento sí queda registrado, con la información completa del sistema. Ese registro es la forma más rápida de leer los números de serie y las direcciones MAC reales de un chasis nuevo antes de escribirle un filtro.

No puede ejecutar un script de primer arranque

El instalador en sí admite un gancho de primer arranque. Una respuesta puede llevar una sección [first-boot] con source = "from-iso" o "from-url", un orden de before-network, network-online o fully-up, y una huella de certificado opcional9. prepare-iso --on-first-boot mete un script en la ISO para el caso from-iso.

PDM nunca envía esa sección. Ni una vez. El propio tipo de respuesta del instalador tiene el campo, first_boot: Option<FirstBootHookInfo>12. La respuesta preparada de PDM no tiene ningún campo para eso, y el código que construye la respuesta a partir de ella escribe first_boot: None sin condición alguna4. No hay nada en el formulario, nada en la API y nada en el archivo de configuración que puedas añadir a mano. Es una petición de funcionalidad abierta, el bug 7699, abierta en junio de 2026 y sin asignar cuando escribo esto7. Casi cinco meses después no ha tenido respuesta de Proxmox, y el 8263 tampoco. Ese silencio es lo preocupante. Quien se toma el tiempo de escribir cómo podría mejorar tu producto merece una respuesta, aunque la respuesta sea que no, y eso es simple cortesía profesional. Con más razón cuando quienes escriben son clientes de pago y partners, que ya ponen dinero en el producto antes de poner también su tiempo.

Y el script de la ISO no te salva. El instalador solo configura el servicio de primer arranque if let Some(first_boot) = &answer.first_boot, y ese es el único camino que lee alguna vez el archivo incrustado13. prepare-iso no avisa cuando incrustas un script que ninguna respuesta va a pedir nunca. Se queda en la ISO y no hace nada.

El hueco del primer arranqueScript en la ISOprepare-iso --on-first-bootRespuesta de PDMfirst_boot: None, siempreInstaladorconfigura el gancho de primer arranquesolo si la respuesta tiene[first-boot]el script se queda en la ISOy nunca se ejecuta, sin avisar
Las dos mitades llegan al instalador, y la mitad que viene de PDM nunca pide el gancho. El script de la ISO es peso muerto.

Eso importa más a un MSP que a nadie. Un nodo recién salido del instalador es un sistema pelado con un hash de contraseña de root y unas cuantas claves SSH. No es un remoto de PDM, no está en un clúster, y no tiene nada de tu monitorización, tus repositorios ni tu almacenamiento. En mi laboratorio los tres nodos terminaron y avisaron de vuelta, y aun así el remoto SITEA de PDM hubo que añadirlo a mano después.

Así que lo que uses para terminar un nodo, sea Ansible o cualquier otra cosa, tiene que dispararse desde fuera: desde el webhook de postinstalación, desde una tarea que vigile la lista de Installations, o desde una persona. Lo dejo ahí. Este artículo trata de conseguir que un solo entorno PXE sirva a todas las sedes, y el hueco del primer arranque es un límite con el que hay que planificar, no uno que vaya a tapar aquí.

El token es la sede

PDM no puede ver la sede, pero sí acota las respuestas por token, y un token no es más que una cabecera. Quien pone la cabecera elige qué conjunto de respuestas puede recibir el instalador. Ese es todo el truco.

Así que pon delante de PDM algo que sepa de dónde vino la petición:

  1. La imagen de arranque lleva un token comodín, catch-all:not-a-secret, que es el mismo en todas las imágenes y está publicado aquí mismo. No es un token de PDM y por sí solo no abre nada.
  2. Un proxy inverso Caddy delante de PDM mira la subred de origen de una petición de respuesta que lleva el token comodín, y sustituye la cabecera por el token real de esa sede.
  3. PDM ve site2-2026q4:<secret> y solo ofrece las respuestas de site2.

El secreto nunca se acerca a la imagen de arranque, así que un pendrive perdido o una ISO copiada no le dan nada a nadie. Rotar el token de una sede es un cambio en Caddy, no reconstruir una imagen. Y añadir una sede es una línea de configuración de Caddy y un token nuevo en PDM.

Esto se apoya en que la dirección de origen sea honesta, y eso es una propiedad de tu red, no del proxy. Ese compromiso y los demás están más abajo, en «Lo que queda por decidir».

Cómo encuentra el instalador a PDM sin una URL incrustada

Una imagen para todas las sedes solo funciona si la imagen tampoco lleva la dirección de PDM. El instalador busca la URL de su respuesta en tres sitios, por este orden14:

Dónde encuentra el instalador la URL de respuesta1. ¿URL incrustada en la ISO?prepare-iso --urlsíse usa, sin preguntar a DHCP ni DNSuna imagen por PDM: aquí se evitano2. ¿Opción DHCP 250?leída de la concesión del instaladorsíse usa, con la opción 251 para lahuella: aquí no se envíano, o cualquier error de DHCP3. Consulta TXT de DNSdominio de búsqueda de resolv.confproxmox-auto-installer.example.docURL y huella, las dos de DNS
El orden que sigue el instalador. Deja la URL fuera de la imagen y no envíes la opción 250, y todas las sedes acaban cayendo en DNS.
ReglaQué hace el códigoQué significa para ti
Ordengana una URL en la ISO; si no, DHCP; cualquier fallo de DHCP cae en DNS14construye la imagen sin --url, y no envíes la opción 250 en ningún sitio
Nombres de registroproxmox-auto-installer.<search domain> para la URL, proxmox-auto-installer-cert-fingerprint.<search domain> para la huellauna zona, dos registros TXT
Dominio de búsquedala primera línea search de /etc/resolv.conf, partida por un solo espacioentrega exactamente un dominio; dos se convierten en un solo nombre no válido
Sin dominiofalla con Could not find search domain in resolv.conf.comprueba que se envía la opción DHCP 15 o 119
Valor TXTdig txt +short, quitadas todas las ", y luego recortadoun valor TXT partido en varias cadenas deja un espacio en la URL; mantenlo en una sola cadena
Huellase toma de DNS solo cuando la URL también vino de DNS y no hay ninguna incrustadapublica la huella del certificado de PDM, que Caddy también sirve
Tokense envía como Authorization: Bearer <token>, guardado en texto plano en la ISO9por eso la imagen solo lleva el token comodín

El certificado que comprueba el instalador es el que conteste en el 443. En este diseño Caddy sirve ahí el propio certificado de PDM, así que un cliente ve en el 443 el mismo certificado que PDM tiene en el 8443, y hay una sola huella que publicar.

El montaje: PDM, archivos de arranque y DNS

Los ejemplos usan el dominio example.doc y tres sedes, 10.10.50.0/24, 10.20.50.0/24 y 10.30.50.0/24. .doc no es un dominio de primer nivel real, así que pon el tuyo. PDM, Caddy, dnsmasq y los archivos de arranque viven todos en un host, pdm.example.doc en 10.10.50.10, y las sedes remotas llegan a él a través de un relé DHCP en sus puertas de enlace. La gestión vive en 10.99.0.0/24 y, como tal, es la única red que llega a ver la interfaz web de PDM.

Tokens y respuestas en PDM

En Remotes → Automated Installations → Authentication Tokens, añade un token por sede y dale una caducidad que encaje con tu ventana de despliegue. El nombre no es secreto y lo vas a ver en los logs, así que haz que diga algo: site1-2026q4. Copia el secreto cuando aparezca.

Luego añade una respuesta preparada por sede y por producto, autorizada solo para el token de esa sede:

Camposite1, Proxmox VE
Token autorizadosite1-2026q4
Filtro de destino/product/product=pve
País, teclado, zona horariagb, en-gb, Europe/London
Acceso de rootclaves SSH, más una contraseña puesta con el formulario, que PDM guarda y envía solo como hash4
RedDHCP, nombres de interfaz fijados
FQDN{{ product.product }}-site1-{{ installation_nr }}.example.doc
Discotu disposición, por ejemplo un espejo ZFS sobre dos discos con nombre
Webhook de postinstalaciónURL base https://pdm.example.doc, con la huella del certificado de PDM

El contador sube cada vez que se sirve una respuesta, reintentos incluidos, así que los nombres pueden saltarse números. El contador de mi laboratorio está en 4 tras tres instalaciones terminadas. Si necesitas nombres predecibles, usa FQDN from DHCP y pon el nombre de host en la reserva DHCP.

La URL base del webhook es el único ajuste que tiene que cambiar para este diseño. Lo natural es apuntarla al propio PDM en el puerto 8443, que es como la tenía mi laboratorio, y el 8443 está a punto de cerrarse. Apúntala a Caddy.

Los archivos de arranque

En cualquier host Proxmox VE o Debian con el repositorio de 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/

Sin --url, porque la pone DNS. --pxe-loader ipxe implica --pxe, y con --pxe la salida tiene que ser un directorio que ya exista15. Obtienes cuatro archivos: vmlinuz, initrd.img recomprimido de zstd a gzip porque iPXE no puede cargar un initrd en zstd, la ISO preparada sin /boot, y un menú boot.ipxe que arranca por defecto la instalación automatizada a los diez segundos y se refiere a los demás por su nombre relativo15. Guarda los cuatro en un mismo directorio.

Cada máquina de destino necesita memoria suficiente para el kernel, el initrd y la ISO entera. Las notas de Proxmox VE 9.2 lo cifran en «at least 6 GiB of available memory on the host»5. Las mismas notas recogen un problema conocido del kernel de la ISO 9.2 con controladoras megaraid_sas y algunas unidades NVMe en modo JBOD (just a bunch of disks), con el consejo de instalar desde la ISO 9.1 y actualizar5. Prueba tu hardware antes de confiarle la ruta de red.

Los registros DNS

En la zona que corresponde al único dominio de búsqueda que entrega 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"

La huella es la del propio certificado de PDM:

openssl x509 -in /etc/proxmox-datacenter-manager/auth/api.pem -noout -fingerprint -sha256

Compruébalos como lo hará el instalador:

dig txt +short proxmox-auto-installer.example.doc @10.10.50.10
# one quoted string, nothing split

Caddy, la única puerta a PDM

La API de PDM escucha en [::]:8443, y eso es una constante compilada en el binario, no un ajuste16. No puedes atarla al loopback. Así que Caddy pasa a ser la única forma de entrar, y el cortafuegos se asegura de ello.

Las rutas de Caddy delante de PDMPETICIÓNQUÉ PASAPOST de respuesta, subred de sede, token comodínreescrita al token de esa sedePOST de post-hook, subred de sedeAuthorization quitada, pasainterfaz web o API, subred de gestiónpasa intacta, login de PDMcualquier otra cosa, desde donde seaabortada, nunca llega a PDMpuerto 8443, desde fuera de la máquinadescartada por nftables
Cada petición que ve Caddy acaba en uno de cinco sitios. Solo los tres primeros llegan a PDM.

PDM tiene exactamente dos rutas que funcionan sin iniciar sesión en PDM: POST /api2/json/auto-install/answer con un token de instalador, y POST …/installations/{uuid}/post-hook con el token de un solo uso en el cuerpo4. Son las dos únicas a las que debería llegar nunca una red de aprovisionamiento.

Usa Caddy 2.11.1 o posterior. La 2.11.1 corrigió una forma de saltarse el path matcher, CVE-2026-27587, que es exactamente el tipo de filtrado por ruta en el que se apoya esto17. El paquete propio de Debian 13 es la 2.6.2, así que tómalo de trixie-backports, que lleva la 2.11.2, o del repositorio propio de Caddy18.

El 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
	}
}

Las partes que hacen trabajo de seguridad, y por qué está cada una:

LíneaPor qué
header Authorization "Bearer catch-all:not-a-secret" en el matcher de la sedesin ella, cualquier POST desde la subred de una sede recibe el token real; una petición tiene que venir de la subred correcta y llevar el token comodín
request_header Authorization …pone la cabecera y sobrescribe lo que enviara el cliente, así que un token falsificado se sustituye, nunca se reenvía19
remote_ipel par inmediato; no pongas NAT ni balanceador de carga entre los instaladores y Caddy20
bloques handle con nombrese mantienen en el orden en que se escriben y son mutuamente excluyentes, así que el handle { abort } desnudo tiene la última palabra21
@mgmttoda la interfaz y la API de PDM, sin tocar, así que el inicio de sesión, los tokens de API y las cookies de PDM funcionan con normalidad; la subred de una sede no recibe nada de eso
admin unix//…|0600la API de administración usa por defecto TCP localhost:2019, sin autenticación22
protocols h1 h2Caddy escucha por defecto en UDP 443 para HTTP/322; esto lo apaga en lugar de dejar abierto un puerto que nadie quería abrir
@hiddenfile_server sirve todos los archivos bajo su raíz, archivos ocultos incluidos, y hide está documentado como que no es una barrera de seguridad23
Csrfpreventiontoken replace REDACTEDCaddy oculta por defecto Authorization y Cookie en su log de acceso, pero no la cabecera CSRF de PDM24

El path matcher es más tolerante que PDM, que es como tiene que ser. Caddy limpia la ruta, decodifica la codificación por porcentaje, junta las barras repetidas e ignora mayúsculas y minúsculas antes de casar20. Así que //answer, %61nswer y ANSWER cuentan todas como la ruta de respuesta, y una variante /api2/extjs/… o una barra final no casan con ninguna ruta de sede y caen en abort.

Hay dos decisiones ahí dentro que necesitan explicación.

Caddy sirve el certificado de PDM, directamente desde los archivos de PDM. Caddy lee api.pem y api.key donde los guarda PDM, así que los instaladores, el webhook y un navegador ven todos el certificado que PDM presenta en el 8443, y no hay una segunda copia que se desfase. La forma obvia de darle ese acceso es añadir el usuario caddy a www-data. En mi laboratorio comprobé hasta dónde llega de verdad esa pertenencia al grupo, probando cada archivo como el usuario caddy:

Archivo bajo /etc/proxmox-datacenter-manager/Legible por caddy en www-data
remotes.shadow, las credenciales de cada remoto gestionadosí
autoinst/tokens.shadow, todos los secretos de los tokens de instaladorsí
auth/api.key, auth/csrf.keysí
access/user.cfg, subscriptions/sí

Nada de esa lista tiene que estar ni cerca de un proxy web. Un fallo en el proxy sería entonces el compromiso de todos los clústeres que gestiona PDM.

Así que Caddy se queda fuera de www-data y recibe una ACL sobre exactamente los dos archivos que necesita, más permiso para atravesar los dos directorios que hay por encima sin poder listarlos:

#!/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"

Probado como el usuario caddy en el laboratorio después de ejecutarlo:

Rutacaddy
auth/api.pem, auth/api.keypuede leer
auth/csrf.key, auth/authkey.key, auth/authkey.pubno puede leer
remotes.shadow, remotes.cfg, autoinst/tokens.shadow, access/user.cfgno puede leer
listar auth/denegado

La pega es la renovación. PDM escribe un certificado renovado en un archivo nuevo y lo renombra encima del antiguo, y un archivo nuevo no lleva la ACL del anterior. Así que cuando PDM renueva, systemd vuelve a poner la ACL y recarga Caddy, sin que intervenga nadie.

Una comprobación de que el certificado y la clave van juntos, porque una renovación los escribe de uno en uno:

#!/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" ]

Una unidad path que vigila los dos archivos, y el servicio que arranca:

# /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

El override de más abajo ejecuta el mismo script de ACL antes de cada arranque de Caddy, así que nunca se pierde una renovación hecha mientras Caddy estaba parado.

Los detalles que lo hacen funcionar, comprobados uno a uno:

PiezaPor qué
los dos archivos vigiladosla primera escritura dispara la unidad; la condición la salta mientras el par no casa, y la segunda escritura la vuelve a disparar con un par que sí casa
ExecConditionuna condición fallida salta la ejecución en lugar de hacerla fallar, así que una renovación a medio escribir nunca llega a Caddy25
PathChangedse dispara cuando se cierra un archivo vigilado que estaba abierto para escritura26; en el laboratorio también se disparó al renombrar un archivo nuevo encima del antiguo
el script de ACL en cada ejecuciónun archivo renombrado no tiene ACL, así que Caddy no podría leerlo hasta que el script vuelva a ejecutarse
try-reload-or-restartrecarga Caddy si está en marcha y no hace nada si no lo está
reload --force en la unidad de Caddysin --force, Caddy ve la misma configuración y sigue sirviendo el certificado antiguo

Esa última fila no es una suposición. En un contenedor desechable con Caddy 2.11.7 cambié el archivo del certificado por debajo de un servidor en marcha: un caddy reload a secas siguió sirviendo el certificado antiguo, y caddy reload --force sirvió el nuevo. La unidad empaquetada ya recarga con --force27, así que conserva esa línea si sobrescribes ExecReload.

En el laboratorio sustituí api.key y api.pem de PDM como lo hace una renovación, escribiendo archivos nuevos y renombrándolos encima de los antiguos. Justo después del renombrado, api.key no tenía entrada de ACL para caddy. En cuatro segundos la unidad path se había disparado, la ACL estaba de vuelta en los dos archivos, y Caddy se había recargado en su sitio con el mismo ID de proceso.

El nombre del token y el secreto se guardan por separado. El nombre va en el Caddyfile, donde queda en el log. El secreto va en un archivo de entorno que solo puede leer root, y Caddy lo lee mediante {env.*} en el momento de la petición.

Registra qué token se usó, nunca el secreto

La primera versión de esto en mi laboratorio tenía una fuga. La unidad empaquetada de Caddy lo arranca como caddy run --environ, y --environ imprime todo el entorno antes de arrancar28,27. Cada reinicio dejaba el token de la sede en el journal en texto claro: ocho líneas cuando lo conté. Nada en el log de acceso, nada en los propios logs de PDM, todo por ese único parámetro.

Adónde van el secreto y el nombre del tokenEL SECRETOpdm-secrets.envsolo root, 0600entorno de Caddy{env.PDM_SECRET_SITE1}cabecera Authorizationsolo el salto por loopback a PDMEL NOMBRElínea import enel Caddyfilelog de acceso, cada petición de respuesta"pdm_token":"site1-2026q4"NUNCA SE ESCRIBE ENel journal--environ quitadola API de administraciónsocket unix, 0600un volcado de configuraciónsolo el marcador
El secreto tiene un trabajo y un camino. El nombre es lo que registras.

Así que el archivo de secretos guarda secretos y nada más:

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

y el override de la unidad quita --environ y le da un sitio al socket de administración:

# 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

El + ejecuta esas líneas como root, antes de que Caddy arranque con su propio usuario. Las líneas del log importan en cuanto cualquier otra cosa toca /var/log/caddy: un log rotado por root, o un archivo creado a mano mientras pruebas, deja a Caddy sin poder abrir su propio log de acceso. En el laboratorio puse pdm.log como root:root y reinicié Caddy; volvió como caddy:caddy y Caddy no registró ningún error de permisos.

Reinicia en lugar de recargar después de editarlo, porque una recarga no vuelve a leer el entorno. Usa {env.NAME}, nunca {$NAME}: la segunda forma se sustituye cuando se adapta el Caddyfile, así que el secreto acaba en la configuración JSON, en el archivo de autosave y en cualquier cosa que los vuelque29. El socket de administración también importa aquí. {env.*} resuelve cualquier cosa del entorno del proceso sin lista de permitidos, así que cualquiera que pueda cargar una configuración a través de una API de administración por TCP puede hacer que Caddy le devuelva un secreto30.

Ahora cada línea de respuesta del log de acceso nombra la sede y el token, con la cabecera en sí oculta. Del laboratorio, donde los dos se llaman siteA:

{"site":"siteA","pdm_token":"siteA","pdm_status":400,"status":400,
 "request":{"headers":{"Authorization":["REDACTED"]}}}

Lo comprobé en lugar de fiarme. En una copia de esta configuración en podman con secretos de prueba, y luego en el laboratorio, envié respuestas por todos los caminos que se me ocurrieron: normal, con el log de depuración activado, con el upstream caído para un 502, una recarga y un reinicio. Luego conté cada secreto en el journal, el log de acceso, los directorios de configuración y de datos de Caddy y el JSON adaptado. Todos los recuentos dieron cero. La pasada de control, la misma configuración arrancada con --environ, imprimió el secreto dos veces.

Dos salvedades de esa prueba. Con debug activado, Caddy registra toda su configuración adaptada, así que el token comodín y los marcadores {env.*} aparecen ahí; los valores que hay detrás no aparecieron. Aun así, mantén debug desactivado en producción. Y si un secreto ya ha pasado por un journal, como el mío, regenéralo en PDM y actualiza el archivo; borrar líneas del log no es la solución.

El cortafuegos: el 8443 nunca sale de la máquina

Mi laboratorio no tenía ningún cortafuegos, así que el 8443 contestaba a cualquier host de la red de aprovisionamiento y todas las reglas del Caddyfile se podían rodear. Ciérralo. nftables, con drop por defecto:

#!/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;
	}
}

Las conexiones de PDM con sus remotos son salientes, así que cerrar el 8443 de entrada no cambia nada ahí. Carga un conjunto de reglas nuevo con una vuelta atrás temporizada la primera vez, para que un error te cueste noventa segundos y no un viaje a la consola:

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

En el laboratorio, después de todo eso:

ComprobaciónResultado
el 8443 desde la red de gestiónsin conexión; el contador de drop subiendo
la interfaz web de PDM a través de Caddy en el 443, desde gestióninicia sesión y funciona
la interfaz web de PDM desde una dirección de sedeabortada
POST de respuesta desde una sede con el token comodínllega a PDM con el token de la sede: una prueba deliberada sin coincidencia recibió 400 no answer file found
la misma prueba directa a PDM con un token inventado401
POST de respuesta sin el token comodín, o como /api2/extjs/…abortado
los archivos donde viven los secretos de PDM, leídos como caddyninguno legible
el secreto de la sede en el journal, el log de acceso o los datos de Caddy desde el cambio0

DHCP, iPXE y el bucle

El firmware de un servidor solo puede traer su primer archivo por TFTP, así que TFTP sirve el pequeño cargador iPXE y iPXE trae todo lo demás por HTTP. La trampa es que iPXE envía su propia petición DHCP, y un servidor que le contesta otra vez con ipxe.efi manda la máquina a dar vueltas en círculo para siempre. El servidor DHCP tiene que distinguir el firmware de iPXE.

Por qué una respuesta DHCP simple da vueltasRespuesta simple: bucleel firmware pide un archivo de arranquerespuesta: ipxe.efiiPXE arranca y vuelve a pedirmisma respuesta: ipxe.efiel servidor no distingue el firmwarede iPXE, así que nunca avanzaDistingue iPXE: funcionael firmware pide un archivo de arranqueno es iPXE: ipxe.efi por TFTPiPXE vuelve a pedir, con la opción 175respuesta: la URL de boot.ipxe, por HTTPkernel, initrd e ISO, luego instala
La misma petición dos veces recibe la misma respuesta dos veces. La opción 175, que solo envía iPXE, es como el servidor distingue la segunda petición de la primera.
Servidor DHCPCómo rompe el bucle
dnsmasqetiqueta las peticiones que llevan la opción 175, que solo envía iPXE, y a esas les da la URL HTTP31
ISC dhcpdif exists user-class and option user-class = "iPXE"32
FortiGateel campo filename admite un solo valor, así que construye un cargador iPXE con la URL incrustada y sírveselo a todo

dnsmasq, sirviendo su propia red y las otras dos a través de relés, solo con reservas:

# /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 significa que no hay pool: solo las máquinas reservadas reciben dirección, lo que mantiene a los extraños fuera del camino del instalador. La dirección de cada reserva tiene que estar dentro del /24 de su propia sede, porque esa dirección es la que usa Caddy para elegir el token. Un nodo reservado en la subred equivocada recibe la respuesta de la sede equivocada, o nada.

Las reservas son una lista de direcciones MAC, y eso es contabilidad por máquina, justo la que el cliente no quería. Si de verdad el requisito es conectar y arrancar, dale a cada VLAN de aprovisionamiento un rango dinámico pequeño, por ejemplo dhcp-range=set:site1,10.10.50.100,10.10.50.150,255.255.255.0,1h. La sede sigue saliendo de la subred, así que Caddy sigue eligiendo el token correcto, y la plantilla de FQDN con installation_nr da nombre al nodo. El precio es la primera fila de «Lo que queda por decidir»: cualquier cosa conectada a esa VLAN puede ahora obtener la respuesta de la sede, así que la propia VLAN pasa a ser la puerta.

dnsmasq negocia con el cliente el tamaño de bloque de TFTP. Si no lo tocas, lo limita a la MTU de su propia interfaz31, lo que funciona hasta que el camino hacia una sede es más pequeño que el enlace en el que está dnsmasq. Por eso la configuración fija el techo como tftp-mtu=1500 en lugar de heredarlo. Así el número queda delante de quien se ocupe de una sede detrás de un túnel IPsec o de cualquier otra cosa que le coma espacio al paquete, y lo baja a lo que el camino lleva de verdad en lugar de enterarse por una transferencia que se queda colgada a medias. Si una ROM de tarjeta de red se porta mal cuando se concede la opción, tftp-no-blocksize desactiva la negociación.

Copia ipxe.efi del paquete ipxe de Debian a /srv/tftp, con el usuario dnsmasq como propietario, porque tftp-secure rechaza cualquier otra cosa. Mejor UEFI. Secure Boot necesita un iPXE firmado, así que o lo preparas o desactivas Secure Boot para la instalación y lo vuelves a activar después.

Arrancar, mirar y lo que se rompe

Prueba con una máquina virtual en la red de una sede antes de tocar hardware: firmware UEFI, 8 GiB de memoria y una tarjeta de red en esa VLAN reproducen toda la cadena.

Qué pasa en el arranqueServidor de destinoarranque UEFI por red6 GiB o más1. DHCP y TFTPuna concesión, un dominio de búsqueda, luego ipxe.efi2. HTTP en el puerto 80boot.ipxe, vmlinuz, initrd.img y la ISO, a memoria3. DNSdos registros TXT: la URL de respuesta y la huella de PDM4. Caddy en el 443, luego PDMla respuesta ahora, el webhook tras el reinicio
Cuatro conversaciones separadas, cada una con su propio sitio donde mirar cuando falla.

Y todo junto, por orden, desde el encendido hasta una instalación que PDM marca como Finished:

Del encendido a la instalación completaDestinodnsmasq, :80Caddy :443PDMDHCP: una concesiónTFTP: ipxe.efiiPXE vuelve a pedirHTTP: kernel, initrd, ISOinstalador: un dominioDNS: dos registros TXTPOST, token comodíntoken de la sedeen cursola respuesta generada, vía Caddyinstala en discowebhook, token de un usoAuthorization quitadaFinishedreinicia; primer arranqueactiva la clave
Cada paso, por orden. El webhook lo envía el instalador antes del reinicio, así que Finished significa instalado, no arrancado; la clave de suscripción se activa después, en el primer arranque.
PasoQué pasaDónde mirar
1el firmware recibe ipxe.efi por TFTP, iPXE recibe boot.ipxe por HTTPjournalctl -u dnsmasq, el log de Caddy
2a los diez segundos el menú arranca la entrada automatizada; se descargan el kernel, el initrd y la ISOcuatro GET, uno de ellos de unos 1,6 GB
3el instalador toma una concesión, lee el dominio de búsqueda y consulta los dos registros TXT/tmp/fetch_answer.log en el destino
4POST a Caddy, cambio de token, PDM elige una respuestael log de Caddy nombra el token; PDM muestra una instalación nueva
5se ejecuta la instalación/tmp/auto_installer.log, /tmp/install-low-level-start-session.log
6todavía en el instalador, el webhook llama de vuelta a través de Caddy, y luego la máquina se reinicia33la instalación pasa a Finished con los discos, las tarjetas de red y las claves SSH del host adjuntas

Ante un fallo el instalador cae a una shell, salvo que la respuesta active Reboot on Error, así que déjalo desactivado mientras pruebas.

SíntomaCausa probable
Could not find search domain in resolv.conf.DHCP no envió ningún dominio
una consulta TXT de un nombre con un espacio dentromás de un dominio de búsqueda
Got empty response.falta el registro, zona equivocada, o la opción 6 apunta a otro servidor DNS
una URL con un espacio en mediovalor TXT partido en varias cadenas
el log dice que la URL vino de DHCPla opción 250 está en algún sitio de la red
error TLS en el POSTla huella de DNS no es la del certificado actual de Caddy
conexión cerrada sin respuestaCaddy la abortó: subred equivocada, sin token comodín, o una ruta que no es ninguna de las dos
400 no answer file foundel token funcionó; ningún filtro casó y no hay respuesta por defecto en ese token
la respuesta equivocadafiltros solapados; gana el primero del archivo
el estado se queda en cursola URL base del webhook sigue apuntando al 8443
iPXE se recarga a sí mismo sin pararel servidor DHCP no distingue iPXE del firmware
tiempo de espera TFTP PXE-E32next-server equivocado, o UDP 69 bloqueado

Añadir Proxmox Backup Server

Nada del camino es propio de Proxmox VE. Construye un segundo juego de archivos de arranque desde la ISO de PBS 4 con el mismo token comodín, en /srv/www/pbs-4/, y añade una respuesta de PBS por sede con el filtro /product/product=pbs, autorizada para el token de esa sede. Los registros DNS, Caddy, los tokens y el cortafuegos no cambian.

En una sede con los dos, algo tiene que decidir qué producto recibe cada máquina, y un menú es lo que no debe decidirlo. El choose de iPXE sin --timeout espera en la consola a que alguien pulse una tecla para siempre, que es lo contrario de desatendido.

Deja que decida la reserva DHCP. Ya reservas cada nodo por su dirección MAC, así que etiqueta los de PBS y manda a cada etiqueta su propio 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 en una reserva etiqueta esa máquina concreta31, y tag:!pbs mantiene las dos líneas de arranque mutuamente excluyentes, así que no hay duda de cuál gana. El boot.ipxe de cada producto ya arranca su entrada automatizada a los diez segundos sin nadie al teclado15.

Estas líneas pasan dnsmasq --test en la 2.91, la versión de mi laboratorio, y siguen la sintaxis documentada de dhcp-host y de las etiquetas. Todavía no he arrancado por PXE un nodo PBS a través de ellas, así que prueba con una máquina etiquetada antes de fiarte.

A partir de ahí es el mismo camino que Proxmox VE. El instalador de PBS envía "product": "pbs", Caddy pone el token de la sede, y PDM elige la respuesta de PBS de esa sede. No se elige nada a mano en ningún momento, y el producto en que se convierte un nodo queda escrito en un solo sitio, la reserva, junto a su dirección.

Qué decideDónde vive
qué ISO arrancala etiqueta set:pbs en la reserva DHCP
qué respuestas de qué sede se aplicanla subred de origen, mediante el cambio de token de Caddy
qué respuesta dentro de esa sedeel filtro /product/product=pbs en PDM

El instalador solo pone el sistema operativo. El datastore, en un almacenamiento separado del disco del sistema, es otro trabajo para lo que termine el nodo, y eso nos devuelve al gancho de primer arranque que falta.

Lo que queda por decidir

Nada de esto te impide usar este diseño. Cada punto necesita a alguien que se haga cargo de él.

CompromisoPor qué importaQué hacer
la dirección de origen es la puertacualquier cosa en una VLAN de aprovisionamiento, o capaz de falsificar una dirección en ella, puede obtener la respuesta de esa sede: hash de la contraseña de root, claves SSH, clave de suscripciónaislar la VLAN, seguridad de puerto, solo reservas, caducidad de los tokens, desactivar los tokens entre despliegues
el token comodín no es un secretoestá en todas las imágenes y en esta páginasolo filtra peticiones perdidas; la puerta es la subred junto con el token real que guarda Caddy
Caddy puede leer la clave de PDMservir el certificado de PDM implica que el proxy lee esa clave privadauna ACL solo sobre api.pem y api.key, nada más de PDM legible, el host cerrado a cal y canto; la clave nunca sale de él
se confía en DNSun registro TXT falsificado manda al instalador a otro sitio, y si la huella también viene de DNS el falsificador controla las dos cosasproteger el DNS de aprovisionamiento, o incrustar la huella de PDM con --cert-fingerprint y reconstruir cuando cambie
los archivos de arranque van por HTTP sin cifrarse pueden leer o cambiar por el caminomantener la red aislada; iPXE sobre HTTPS si tu compilación lo admite
el token del webhook16 bytes aleatorios, de un solo uso, borrado tras la llamada, pero sin límite de tiempo4purgar los registros de instalación antiguos
la clave de suscripciónse activa una vez cuando sube la red; si falla, la clave se borra y la unidad falla, sin reintento34revisar systemctl --failed en los nodos nuevos
PDM es un punto únicohoy no tiene alta disponibilidaduna caída para las instalaciones nuevas, no los clústeres en marcha
una sola tarjeta de gestiónla respuesta no tiene opciones de bond, VLAN ni bridge9eso es trabajo de postinstalación, con el mismo problema del primer arranque

Una primera versión, bien aprovechada

Esta es la primera versión de la función, y para ser la primera es buena. El formulario de respuestas es sensato, las reglas de selección quedan claras en cuanto las lees, el webhook funciona, y el código es abierto, que es la única razón por la que todo lo anterior se ha podido demostrar en lugar de suponer. La serie de parches pasó por cuatro rondas en la lista de correo pública antes de entrar35, y se nota.

Los huecos son las dos cosas que convierten un servidor de instalación en un sistema de aprovisionamiento. La sede es la unidad con la que piensa un MSP, y el instalador está pisándola cuando pregunta. Un gancho de primer arranque es la diferencia entre un nodo instalado y un nodo terminado. Los dos ya están a medias: el instalador tiene el gancho y PDM tiene el token. Espero que lleguen los dos, y cuando lleguen, el Caddy en medio de este diseño se simplificará en lugar de desaparecer, porque un proxy que mantiene los secretos de PDM fuera de alcance y el 8443 fuera de la red merece la pena de todas formas.

La razón para hacerlo así mientras tanto es la del principio. Diez servidores PXE son diez cosas desfasándose a diez velocidades distintas, y cada una solo se mira cuando falla el día que la necesitas. Un solo entorno, puesto al día una vez, con la sede deducida por la red y los secretos guardados en un solo sitio, es menos que mantener. Menos que mantener es menos en lo que equivocarse, y eso es casi todo el trabajo.


  1. Proxmox — Proxmox Datacenter Manager 1.1 press release — instalaciones automatizadas, con una pestaña nueva bajo Remotes que sigue el progreso de la instalación. ↩︎

  2. Proxmox Datacenter Manager documentation — Automated Installations — respuestas preparadas, filtros de destino, plantillas, y «a dedicated token mechanism, separate from the normal API tokens». ↩︎ ↩︎

  3. proxmox-datacenter-manager, lib/pdm-api-types/src/auto_installer.rs — las claves de los filtros de destino «are JSON Pointers as per [RFC6901], the values globs»; una sola respuesta por defecto. ↩︎

  4. proxmox-datacenter-manager, server/src/api/auto_installer/mod.rs — la comprobación del token antes de la respuesta por defecto, la primera coincidencia en el orden del archivo, first_boot: None, los incrementos de los contadores, el token de un solo uso del post-hook, las dos rutas accesibles sin iniciar sesión en PDM, y los privilegios Sys.Audit/Sys.Modify sobre /system/auto-installation. ↩︎ ↩︎ ↩︎ ↩︎ ↩︎ ↩︎ ↩︎

  5. Proxmox VE Roadmap — Proxmox VE 9.2 — preparación para PXE e iPXE, «at least 6 GiB of available memory on the host», y el problema conocido con megaraid_sas y NVMe en JBOD. ↩︎ ↩︎ ↩︎

  6. Proxmox Bugzilla 8078 — «prepared answer disk setup does not allow to specify multiple udev filters with same name», abierto el 26 de septiembre de 2026, estado NEW a 7 de noviembre de 2026. ↩︎

  7. Proxmox Bugzilla 7699 — «Add on-first-boot parameter to the PDM remote installation», abierto el 12 de junio de 2026, estado NEW a 7 de noviembre de 2026. ↩︎ ↩︎

  8. proxmox, proxmox-installer-types/src/lib.rs — SystemInfo: producto, ISO, DMI e interfaces de red. ↩︎

  9. Proxmox VE wiki — Automated Installation — las opciones [first-boot] y [network], la información del sistema que se envía, y que el token de autenticación «is stored in plain text inside the prepared ISO». ↩︎ ↩︎ ↩︎ ↩︎

  10. Proxmox Bugzilla 8263 — «[Feature Request] auto-installer: client source IP (or X-Forwarded-For) for answer selection and templating», abierto el 31 de octubre de 2026, estado NEW a 7 de noviembre de 2026. ↩︎

  11. proxmox-datacenter-manager, debian/changelog — 1.1.1: «match prepared answers in the configuration’s stable order and let the first match win». ↩︎

  12. proxmox, proxmox-installer-types/src/answer.rs — AutoInstallerConfig lleva first_boot: Option<FirstBootHookInfo>. ↩︎

  13. pve-installer, proxmox-auto-installer/src/bin/proxmox-auto-installer.rs — el ejecutable de primer arranque solo se configura cuando la respuesta tiene una sección first_boot. ↩︎

  14. pve-installer, proxmox-fetch-answer/src/fetch_plugins/http.rs — primero la URL de la ISO, luego DHCP, luego DNS; los nombres TXT; el análisis de resolv.conf; dig txt +short con las comillas quitadas; la cabecera Bearer. ↩︎ ↩︎

  15. pve-installer, proxmox-auto-install-assistant/src/main.rs — --pxe-loader implica --pxe, el directorio de salida, el initrd en gzip, /boot quitado de la ISO, y el boot.ipxe generado. ↩︎ ↩︎ ↩︎

  16. proxmox-datacenter-manager, server/src/bin/proxmox-datacenter-api/main.rs — PDM_LISTEN_ADDR es la dirección IPv6 no especificada en PDM_PORT, 8443, fijada al compilar. ↩︎

  17. Caddy releases — la 2.11.1 corrigió CVE-2026-27587, una forma de saltarse el path matcher mediante secuencias de escape. ↩︎

  18. Debian sources — caddy — 2.6.2-12+deb13u1 en trixie, 2.11.2-1~bpo13+1 en trixie-backports. ↩︎

  19. Caddy — request_header — «With no prefix, the field is set (overwritten).» ↩︎

  20. Caddy — Request matchers — remote_ip es el par inmediato; las rutas casan sin distinguir mayúsculas, decodificadas, limpias y con las barras repetidas juntadas. ↩︎ ↩︎

  21. Caddy — handle — «only the first matching handle block will be evaluated». ↩︎

  22. Caddy — Global options — admin usa por defecto localhost:2019; protocols vale por defecto h1 h2 h3. ↩︎ ↩︎

  23. Caddy — file_server — hide no debe tratarse como una barrera de seguridad. ↩︎

  24. Caddy — log — Cookie, Set-Cookie, Authorization y Proxy-Authorization se registran como REDACTED. ↩︎

  25. systemd.service — ExecCondition= — con un código de salida de 1 a 254 «the remaining commands are skipped and the unit is not marked as failed». ↩︎

  26. systemd.path — PathChanged= — «activated if the file which was open for writing gets closed». ↩︎

  27. caddyserver/dist, init/caddy.service — ExecStart=/usr/bin/caddy run --environ --config /etc/caddy/Caddyfile. ↩︎ ↩︎

  28. Caddy — Command line — --environ imprime el entorno antes de arrancar. ↩︎

  29. Caddy — Caddyfile concepts, environment variables — {$NAME} se sustituye antes de analizar el Caddyfile. ↩︎

  30. caddyserver/caddy, replacer.go — cualquier clave que empiece por env. se pasa a os.Getenv. ↩︎

  31. dnsmasq manual page — dhcp-match, dhcp-boot, dhcp-host con set:<tag>, enable-tftp, tftp-secure, tftp-mtu, tftp-no-blocksize y los rangos static. ↩︎ ↩︎ ↩︎

  32. iPXE — Configuring ISC dhcpd — if exists user-class and option user-class = "iPXE" da a iPXE el script y al firmware el cargador. ↩︎

  33. pve-installer, unconfigured.sh — proxmox-post-hook se ejecuta justo después de que proxmox-auto-installer termine bien, antes del reinicio. ↩︎

  34. pve-installer, proxmox-first-boot/proxmox-first-boot-subscription — «always drop the staged key, regardless of whether activation succeeded». ↩︎

  35. pdm-devel — “add auto-installer integration”, v4, 40 patches — la serie que añadió la función, con sus notas de cambios de la v3 a la v4. ↩︎