Blog ·
El argumento contra abrir un puerto al panel
Un agente que llama hacia fuera necesita dos reglas salientes y ningún puerto entrante. Qué aporta eso, qué cuesta y los cuatro problemas que solo desplaza.
Por Provibr
Si una plataforma de gestión va a gobernar los hipervisores propios, algo tiene que cruzar la frontera entre la red de ella y la de uno. Hay dos direcciones entre las que elegir, y la elección determina casi todo lo demás del modelo de seguridad.
La dirección tradicional es hacia dentro. La plataforma guarda las credenciales del clúster Proxmox, de la máquina DirectAdmin y del panel de juego, y se conecta a cada uno de ellos por internet. Eso significa una API que responde a internet, una regla de cortafuegos o una VPN, y un plano de control alcanzable por cualquiera que sepa encontrarlo.
La otra dirección es hacia fuera. Se ejecuta un agente pequeño dentro de la propia red. Abre una conexión hacia la plataforma y la mantiene abierta, los comandos bajan por esa conexión y los resultados suben por ella. Del lado propio no escucha nada.
Esta página trata de lo que cuesta la segunda opción y de lo que aporta, a partir de haber construido una.
El cambio en el cortafuegos: dos reglas, las dos salientes
Todo el requisito de red de un agente saliente cabe en una tabla:
| Dirección | Regla | Cuándo |
|---|---|---|
| Saliente | TCP 50051 a la pasarela de la plataforma | Una vez, en el alta |
| Saliente | TCP 50052 al mismo host | De forma continua: es la sesión |
| Entrante | Ninguna |
Dos puertos en lugar de uno es una separación deliberada. El alta ocurre antes de que el agente tenga un certificado de cliente, así que ese puerto solo puede ofrecer TLS del lado del servidor. El puerto de sesión exige TLS mutuo y rechaza cualquier cosa que no traiga un certificado de cliente válido. Un solo puerto a la escucha para los dos significaría un puerto en el que el certificado de cliente es opcional y la decisión de autorización es condicional. Dos hacen que la frontera sea estructural: en el 50052 no hay camino de código alguno en el que un par sin autenticar pase del saludo inicial.
Si el filtrado de salida es estricto, esas dos reglas son toda la petición. No hay regla entrante que escribir, ni entrada de NAT, ni concentrador de VPN, ni nada que renovar cuando cambia la dirección pública.
Qué significa «saliente» cuando la red es hostil
Una conexión TCP saliente a través de NAT no es gratis: el estado que la hace funcionar vive en la tabla de otro, y esa tabla tiene un tiempo de espera. El NAT del operador, un cortafuegos corporativo y un balanceador de carga en la nube olvidan en silencio una conexión inactiva, y el modo de fallo es el peor de todos, porque los dos extremos siguen creyendo que el socket está bien.
Dos mecanismos se ocupan de eso, y hacen falta los dos.
El primero es el keepalive de HTTP/2, a intervalos de 30 segundos con un tiempo de espera de 10. Esa es la capa que vence a los tiempos de espera del NAT y del balanceador, y la única que se entera de una conexión TCP que murió en silencio por debajo del flujo.
El segundo es un latido de aplicación cada 20 segundos, con un vigilante que termina la sesión si no llega ninguna confirmación en 45 segundos. El keepalive demuestra que el socket sigue vivo; el latido demuestra que el otro extremo sigue procesando.
Cuando la sesión termina de verdad, el agente se reconecta con retroceso exponencial y jitter completo: la espera se sortea de manera uniforme entre cero y el tope actual, que se duplica desde un segundo hasta sesenta. El jitter no es un adorno. Sin él, todos los agentes de todos los clientes se reconectan en el mismo instante después de reiniciar la pasarela, y la recuperación se convierte en una estampida autoinfligida.
Un detalle que solo aparece en producción: el contador de retroceso se reinicia únicamente después de que una sesión haya estado sana durante un minuto entero. Sin esa regla, un agente con una conexión medio rota conecta, muere, pone su contador en un segundo y machaca la pasarela una vez por segundo para siempre.
Identidad: un certificado, no una contraseña
Un modelo entrante autentica la plataforma frente al panel, normalmente con una clave de API que se pega en un formulario. Un modelo saliente tiene que autenticar el agente frente a la plataforma, y puede hacerlo con algo mejor que un secreto compartido.
El agente se da de alta una sola vez con un token de un solo uso: 36 bytes aleatorios, 48 caracteres base64url, almacenado del lado del servidor solo como un hash SHA-256 y válido durante 72 horas. Se consume dentro de una transacción que vuelve a comprobar las condiciones por las que fue seleccionado, porque entre la lectura y la escritura es por donde se cuela un alta duplicada.
Lo que vuelve es un certificado de cliente. A partir de ahí la sesión está autenticada por ambos lados, y la pasarela fija el par exacto: toma la huella SHA-256 del certificado presentado y busca al agente por esa huella, antes de que el agente haya dicho quién cree que es. La identidad la fija el certificado; el primer mensaje del flujo solo puede confirmarla.
La rotación va con el reloj del propio agente: los certificados viven 90 días y la renovación empieza a mitad de camino, el día 45. Eso deja 45 días de reintentos antes de que nada esté en riesgo, y el panel avisa a una persona solo cuando quedan 21 días, así que la renovación tiene que llevar tres semanas fallando antes de molestar a nadie. Cada rotación genera un par de claves nuevo: renovar sobre la misma clave alarga la vida de algo que quizá ya se filtró.
La revocación merece su propia frase, porque la versión honesta es menos elegante que el diagrama. No hay CRL ni OCSP. Revocar un agente es una actualización en la base de datos, y una sesión conectada se contrasta con ella una vez por minuto. Esa comprobación falla cerrada, con un presupuesto de tres fallos consecutivos, de modo que un tropiezo momentáneo de la base de datos no puede mantener vivo en silencio a un agente revocado, y el peor caso está acotado y se puede calcular: sesenta segundos por tres.
Exactamente una vez, sobre una conexión que se va a caer
En cuanto se acepta que el enlace es un flujo de larga vida sobre un camino poco fiable, la semántica de entrega deja de ser una preocupación teórica. La plataforma volverá a entregar un comando después de una reconexión, porque desde su lado la diferencia entre «el agente no lo recibió» y «el resultado no volvió» es invisible.
Así que el agente recuerda. Los comandos terminados se guardan en disco con sus resultados, indexados por identificador de comando, en una LRU acotada. La repetición de un comando terminado devuelve el resultado en caché y no ejecuta nada. La repetición de un comando que todavía se está ejecutando se ignora, porque la respuesta ya viene de camino.
En disco, concretamente, y ese es el punto: una caché en memoria está vacía justo cuando la plataforma vuelve a ofrecerlo todo, que es justo después de un reinicio.
La dirección inversa encaja igual. Los eventos salen por un buzón de salida con números de secuencia; al reconectar, la plataforma declara el número de secuencia que tiene y el agente reproduce desde ahí. El buzón está acotado, y cuando algo se cae de él el agente lo dice en la siguiente conexión, declarando la secuencia más baja que todavía conserva menos uno, para que la plataforma deje de esperar eventos que ya no existen.
Hay una grieta honesta en la promesa, y vale más decirla que esconderla: si falla la escritura en disco del registro de deduplicación, el resultado se envía igualmente. La alternativa sería descartar el resultado de un comando terminado para proteger una garantía, y en el peor de los casos una reentrega posterior lo ejecuta una segunda vez.
Las credenciales se quedan del lado propio
Esta es la parte que cambia la forma de una brecha.
En el modelo entrante, la plataforma almacena credenciales que dan control de la infraestructura ajena, y ese almacén es el premio. En el modelo saliente, las credenciales de los hipervisores y de los paneles viven en una caja fuerte cifrada en el host propio, empujadas ahí una sola vez al añadir una integración.
Nuestra caja fuerte usa XChaCha20-Poly1305 con una clave derivada mediante HKDF-SHA256 a partir de un fichero de clave local y del identificador de la máquina, con el identificador del agente mezclado en la información de derivación. Una consecuencia que conviene deletrear: copiar vault.enc a otra máquina deja un fichero que no se descifrará, porque la máquina a la que estaba ligado forma parte de la clave.
Y el límite, que escribimos en el código fuente al lado de la implementación, es que esto no protege contra root en el host. No hace falta que lo haga. El cliente es dueño del host y dueño de las credenciales. La caja fuerte protege contra un fichero filtrado, no contra el propietario.
El rastro de auditoría tiene esa misma forma por la misma regla. Cada comando es una fila que nunca se borra de verdad, y el contenido de un envío de credenciales se guarda censurado. Una tabla de auditoría que contiene los secretos que audita es una segunda copia del problema.
Cuatro cosas que esto no resuelve
Una página de arquitectura que solo enumera ventajas es publicidad. Esto es lo que se desplaza en lugar de desaparecer.
Software en el host propio. El intercambio es real: ya no se expone un puerto y, a cambio, se ejecuta un binario que hay que instalar, mantener actualizado y en el que hay que confiar. Eso mueve la pregunta de «quién puede alcanzar mi panel» a «quién firma el código que ejecuto», que es una pregunta mejor, pero no una pregunta gratis.
Privilegios amplios. Un agente que gestiona un hipervisor guarda credenciales que gestionan un hipervisor. La dirección de la conexión no cambia nada de lo que esas credenciales pueden hacer, solo dónde viven y quién más puede alcanzarlas.
Salir sigue siendo salir. Una conexión que abandona la red propia puede sacar datos de ella. El destino es fijo, el transporte está autenticado por ambos lados y el protocolo es estrecho, pero «dos reglas salientes» es una afirmación más pequeña que «no sale nada».
Respuestas en tiempo real. Esta aparece de inmediato en la API. Como la plataforma no puede llamar al agente, una escritura no puede responder con su resultado: la respuesta correcta a «crea esta máquina» es un acuse de recibo y algo que sondear. Las lecturas no se ven afectadas, porque salen de la propia base de datos de la plataforma. Quien diseñe una API encima de un agente saliente tiene que aceptar esa asimetría en lugar de disimularla.
Hay además exactamente un caso en el que el agente sí escucha, y fingir lo contrario sería deshonesto. Instalar bare metal por la red significa responder a DHCP y servir ficheros, y eso no se puede hacer desde un socket saliente. Así que el agente enlaza esos servicios, en una única interfaz nombrada, solo para máquinas con un trabajo en curso, y solo mientras haya trabajo que hacer. «Activado» significa «permitido aquí», no «escuchando ahora».
Cuándo la dirección entrante sigue siendo lo correcto
Con un solo hipervisor gestionado por uno mismo, nada de esto aplica: un agente no aporta nada frente a una regla de cortafuegos y una contraseña fuerte.
El caso se afila con la distancia y con el número. Varias sedes, redes que no son propias, direcciones que cambian, clientes cuyos cortafuegos no se pueden editar, y un plano de control que más vale no tener en la internet pública en absoluto. En ese punto el agente no es un componente extra; es el componente que permite borrar el resto.
Así funciona Provibr: una conexión saliente por agente, ningún puerto entrante y credenciales que nunca salen de la red del cliente. La documentación del agente cubre la instalación, el dimensionado y la revocación, los primeros pasos recorren la primera conexión de principio a fin, y la API documenta la forma de acusar y sondear que impone el diseño saliente.