HAProxy 3.4: balanceo de carga y certificados TLS en tu homelab
Si tienes varios servicios corriendo en tu homelab y todos apuntan al mismo nombre de dominio, alguien tiene que decidir a qué máquina va cada conexión. Nginx puede hacerlo, Traefik puede hacerlo, y HAProxy también, pero con una diferencia clave: HAProxy lleva más de dos décadas afinando el algoritmo de balanceo y el manejo de TLS a nivel de proxy, y desde la rama 3.2 incluye un cliente ACME nativo que pide y renueva los certificados sin que tengas que montar certbot, un cron y un hook de reload.
En esta guía montamos un balanceador de carga completo sobre HAProxy 3.4 en un homelab: dos backends con verificación de salud, terminación TLS con SNI para servir varios dominios desde un solo puerto 443, rotación de certificados en caliente por la API de administración y renovación automática con ACME nativo. Todo lo que aparece aquí está validado contra la documentación oficial de la rama 3.4 y comprobado con el binario 3.4.7, no solo escrito con buena intención.
TL;DR — Lo que vas a tener al final
- Tiempo de setup: 25 minutos con el contenedor
haproxy:3.4; más si compilas 3.4 desde el código, porque Debian 13 y Ubuntu 24.04 empaquetan 3.0 y 2.8 respectivamente. - Requisitos: un servidor Linux con puertos 80 y 443 libres, un dominio apuntando a él y dos servicios con un endpoint
/healthzque devuelva 200. - Resultado: un
frontendTLS en el 443 con SNI para varios dominios, dos backends con health check cada 2 segundos y balanceorandom(2). - Troubleshooting: si el certificado no aparece en
show ssl sni, casi siempre es que la ruta del cert no coincide con la de la crt-list; y si el ACME no arranca, faltaexpose-experimental-directivesenglobal.
Qué versión de HAProxy vas a encontrar en tu distribución
Antes de escribir una línea de configuración conviene tener claro qué versión vas a ejecutar, porque las funciones que usa esta guía tienen requisitos de versión distintos y no todas están empaquetadas igual en cada distro.
HAProxy publica ramas paralelas con ciclos de soporte distintos. La tabla oficial de haproxy.org muestra el estado real de cada rama a noviembre de 2026:
| Rama | Tipo | Fecha de lanzamiento | Fin de soporte | Última versión |
|---|---|---|---|---|
| 3.4 | LTS | 2026-06-03 | 2031-Q2 | 3.4.7 (2026-10-14) |
| 3.3 | Estable | 2025-11-26 | 2027-Q1 | 3.3.17 |
| 3.2 | LTS | 2025-05-28 | 2030-Q2 | 3.2.26 |
| 3.1 | Sin mantenimiento | 2024-11-26 | 2026-Q1 | 3.1.17 |
| 3.0 | LTS | 2024-05-29 | 2029-Q2 | 3.0.30 |
| 2.8 | Solo críticos | 2023-05-31 | 2028-Q2 | 2.8.31 |
| 2.6 | Solo críticos | 2022-05-31 | 2027-Q2 | 2.6.35 |
El punto importante es otro: el cliente ACME nativo no llegó en la 3.4, sino en la 3.2. Los cambios de esa rama ya quedaron documentados en el CHANGELOG de la versión 3.2, con las primeras entradas de soporte ACME en el desarrollo de 3.2-dev a mediados de 2025. La 3.4 aporta encima el resto del trabajo de la rama y el API de backends dinámicos.
Ahora el detalle que más sorprende a quien viene de una distro estable: el paquete de tu distribución puede estar muy por debajo de la rama que necesitas.
- Debian 13 (trixie) trae
haproxy 3.0.11-1+deb13u4. Es la rama 3.0: estable y con soporte hasta 2029, pero sin ACME nativo y sin el API de backends dinámicos. - Ubuntu 24.04 (noble) trae
haproxy 2.8.30-0ubuntu0.24.04.2. Rama 2.8, que solo recibe correcciones críticas hasta 2028-Q2.
Es decir: en Debian 13 y Ubuntu 24.04 no hay ACME nativo, y la 3.0 no llega ni a la 3.2 que lo introdujo. Si tu homelab usa los paquetes de la distro, la parte de ACME de esta guía te va a exigir compilar desde el código fuente o usar un contenedor. Veremos las dos opciones.
Preparar la máquina: directorios, usuario y configuración base
HAProxy separa la configuración en secciones: global para ajustes del proceso, defaults para los valores por defecto de los proxies, frontend para el punto de entrada, backend para cada grupo de servidores y listen para combinaciones de frontend y backend.
Lo primero es crear la estructura de archivos con los permisos correctos. El proceso corre como usuario haproxy y no puede escribir en su directorio de configuración, solo leerlo:
# Debian/Ubuntu: instalar HAProxy y el paquete de socat para el socket de administracion
sudo apt update
sudo apt install -y haproxy socat
# Directorios de trabajo
sudo mkdir -p /etc/haproxy/certs
sudo mkdir -p /etc/haproxy/acme
sudo chown -R haproxy:haproxy /etc/haproxy/acme
sudo chmod 700 /etc/haproxy/acme
# Verificar con que version estas trabajando
haproxy -v
El comando haproxy -v es el punto de partida: si tu salida dice 3.0.11 o 2.8.30, ya sabes que las secciones de ACME de esta guía no van a funcionar y necesitas o actualizar el binario o construirlo desde el código.
Para la versión 3.4 sobre una distro que no la empaqueta, el camino más limpio es un contenedor. La imagen oficial de Docker Hub publica la misma versión que la rama upstream:
# Descargar la imagen oficial y comprobar la version
docker pull haproxy:3.4
docker run --rm haproxy:3.4 haproxy -v
# Validar una configuracion SIN arrancar el servicio
docker run --rm -v "$PWD:/work:ro" haproxy:3.4 haproxy -c -f /work/haproxy.cfg
El flag -c es el que te va a ahorrar dolor de cabeza: parsea la configuración y sale. Si hay un error de sintaxis o una directiva mal colocada, lo dice con número de línea y no toca nada del sistema. Usa ese chequeo siempre antes de reiniciar el servicio, y también en el servidor por si acaso:
# Validacion local en la maquina donde editas
haproxy -c -f /etc/haproxy/haproxy.cfg
# Si el socket de administracion esta activo, muestra el estado de los backends
echo "show stat" | socat /run/haproxy/admin.sock stdio | head -20
El frontend TLS y los dos backends con health check
Este es el corazón de la guía. Un frontend que acepta conexiones TLS en el 443 y dos backend que reparten el tráfico entre los servicios. Lo importante es que el bloque completo parsea con el binario 3.4.7 real, no con un ejemplo de internet.
global
log stdout format raw local0 info
maxconn 4096
stats socket /run/haproxy/admin.sock mode 660 level admin expose-fd listeners
user haproxy
group haproxy
daemon
defaults
mode http
log global
option httplog
timeout connect 5s
timeout client 30s
timeout server 30s
frontend fe_https
bind *:443 ssl crt /etc/haproxy/certs/lab.pem
bind *:80
http-request redirect scheme https if !{ ssl_fc }
default_backend be_app
backend be_app
balance random(2)
option httpchk GET /healthz
http-check expect status 200
server app1 192.168.5.20:8080 check inter 2s fall 3 rise 2
server app2 192.168.5.21:8080 check inter 2s fall 3 rise 2
Cinco decisiones de diseño en ese bloque, y ninguna es arbitraria:
El socket de administración (stats socket /run/haproxy/admin.sock mode 660 level admin) es lo que habilita toda la parte de rotación de certificados en caliente que veremos más adelante. Sin él, cambiar un certificado obliga a recargar el proceso. El nivel admin es el que permite ejecutar comandos como dump ssl cert.
http-request redirect scheme https if !{ ssl_fc } convierte el puerto 80 en un redirector a HTTPS. El ssl_fc significa “la conexión de frontend es TLS”, así que la condición solo se cumple cuando el cliente llegó sin cifrar.
El health check usa option httpchk GET /healthz con http-check expect status 200. HAProxy no espera que el puerto esté abierto: le hace la petición HTTP y mira el código de respuesta. Un servicio que escucha en 8080 pero devuelve 500 en su endpoint de salud queda marcado como caído, que es justo lo que quieres. Los parámetros inter 2s fall 3 rise 2 significan que comprueba cada 2 segundos, necesita 3 fallos seguidos para sacar el servidor de rotación y 2 aciertos seguidos para volver a meterlo. Eso evita el efecto látigo de sacar y reponer un backend en cada comprobación.
balance random(2) deserves atención especial, y aquí hay un cambio de comportamiento que mucha gente no conoce. Hasta la rama 3.2 el algoritmo por defecto era roundrobin. El CHANGELOG de la 3.3 registra el cambio como MAJOR: “backend: switch the default balancing algo to ‘random'”. La razón es que el balanceo round-robin ignora el peso real de la conexión: con servidores lentos y rápidos mezclados, round-robin reparte las peticiones a ciegas. El algoritmo random sortea entre los servidores disponibles y, con el parámetro (2), cada elección se hace entre dos backends sorteados al azar, lo que produce una distribución más equilibrada en la práctica.
Servir varios dominios en el mismo 443 con SNI
Un homelab real no tiene un solo dominio: tienes lab.example.com para el panel, media.example.com para el servidor de medios y git.example.com para el repositorio. Con SNI (Server Name Indication) un solo proceso y un solo puerto 443 pueden atenderlos todos, eligiendo el certificado correcto según el nombre que el cliente envía en el handshake.
La forma más limpia en 3.4 es la crt-list: un archivo de texto donde cada línea es un nombre de certificado seguido de sus opciones, y que se asigna a los bind mediante crt-list:
# Crear la crt-list con los dominios del homelab
cat > /tmp/certs.list <<'EOF'
/etc/haproxy/certs/lab.example.com.pem [alpn h2,http/1.1 ssl-min-ver TLSv1.2]
/etc/haproxy/certs/media.example.com.pem [alpn h2,http/1.1]
/etc/haproxy/certs/git.example.com.pem [alpn h2,http/1.1 ssl-min-ver TLSv1.2]
EOF
# Ver las lineas antes de aplicarlas
cat /tmp/certs.list
Usa rutas absolutas en la crt-list. Este detalle no está en la documentación y te va a gastar una tarde: HAProxy resuelve los nombres de archivo de la crt-list contra el directorio de trabajo del proceso, no contra el directorio donde está la propia crt-list. Si pones lab.example.com.pem a secas, el arranque falla con unable to stat SSL certificate from file 'lab.example.com.pem' aunque el archivo esté justo al lado de la crt-list. Solo funciona si el servicio arranca con ese directorio como working directory, cosa que no da por sentada.
El formato de cada línea es <certificado> [opción opción ...], con un único grupo de corchetes por línea. Si abres un segundo grupo, HAProxy rechaza el archivo entero con el error too many '['. Dentro de ese grupo, las opciones se separan por espacios y son las mismas keywords que acepta ssl-f-use: alpn, ciphers, curves, ssl-min-ver, ca-file, verify, ocsp y compañía. Las dos listas no son idénticas palabra por palabra, pero todo lo que uso en esta guía es válido en las dos. La segunda parte opcional de la línea, fuera de los corchetes, es el filtro SNI.
Esa segunda parte merece un comentario, porque es lo que permite servir un solo certificado para varios dominios. El manual define el formato completo como <crtfile> [<sslbindconf> ...] [[!]<snifilter> ...]: a la derecha de los corchetes van los dominios que activan ese certificado, y el prefijo ! excluye uno. El caso más común es un comodín con una excepción:
# El comodin cubre todos los subdominios, excepto secure.example.com
cat > /tmp/certs.list <<'EOF'
/etc/haproxy/certs/default.pem.rsa *
/etc/haproxy/certs/wildcard.pem *.example.com !secure.example.com
/etc/haproxy/certs/secure.pem [curves X25519:P-256 ciphers ECDHE-ECDSA-AES256-GCM-SHA384] secure.example.com
EOF
Sin ese filtro SNI, HAProxy decide qué certificado servir por el CN y los SAN del propio certificado. Con el filtro, decides tú. El primer certificado declarado en el bind se usa como certificado por defecto cuando el cliente no envía SNI o cuando ningún dominio coincide, salvo que actives strict-sni para desactivar ese comportamiento.
Con esa lista declarada, el bind queda así:
frontend fe_https
bind *:443 ssl crt-list /etc/haproxy/certs/certs.list
bind *:80
http-request redirect scheme https if !{ ssl_fc }
default_backend be_app
ssl-f-use: opciones TLS compartidas sin repetirlas
ssl-f-use es una de las novedades de la rama 3.2. En la documentación oficial de 3.4 aparece con un grupo de keywords que se aplican en bloque a los certificados del frontend (alpn, ciphers, curves, ssl-min-ver, ca-file, verify, ocsp, entre otras) y asigna esa configuración a todas las líneas bind con ssl del frontend actual. En lugar de repetir la misma lista de opciones de cifrado en cada bind, la defines una vez y se aplica a todos.
La forma de usarla es dejar el bind sin crt y dejar que ssl-f-use cargue el certificado con sus opciones:
frontend fe_https
bind *:443 ssl
bind *:8443 ssl
ssl-f-use crt /etc/haproxy/certs/lab.pem ssl-min-ver TLSv1.2
default_backend be_app
Ese patrón es el que evita un error fácil de cometer: no pongas el mismo certificado en bind y en ssl-f-use a la vez. Si escribes bind *:443 ssl crt /etc/haproxy/certs/lab.pem y además ssl-f-use crt /etc/haproxy/certs/lab.pem, HAProxy carga el certificado dos veces y el dominio aparece duplicado en el mapeo SNI. No es un error fatal: el arranque es normal y el servicio funciona. Pero el duplicado se ve con show ssl sni, que es justamente la herramienta para detectar esa situación, y en la práctica significa que el mismo nombre sirve dos certificados idénticos con la misma fecha de expiración.
La forma limpia es la que se ve arriba: bind con ssl a secas y el certificado cargado una sola vez por ssl-f-use.
Y para comprobar qué certificados están cargados y para cuáles hay SNI configurado, la API de administración responde con dos comandos:
# Listar los certificados cargados en memoria
echo "show ssl cert" | socat /run/haproxy/admin.sock stdio
# Ver el mapeo SNI por frontend (columnas separadas por tabulador)
echo "show ssl sni" | socat /run/haproxy/admin.sock stdio
# Solo los certificados que ya expiraron, o los que expiran en 30 dias
echo "show ssl sni -A -t 30d" | socat /run/haproxy/admin.sock stdio
El comando show ssl sni acepta dos flags útiles: -A filtra para mostrar solo los certificados cuyo notAfter ya pasó, y -t acepta un offset temporal. La combinación -A -t 30d que ves arriba te muestra los que expirarán dentro de 30 días, que es exactamente la consulta que quieres hacer antes de que Let’s Encrypt cambie la duración por defecto.
Rotar certificados en caliente sin reiniciar el proceso
Este es el punto donde HAProxy se separa de la mayoría de setups caseros. El ciclo clásico de nginx es: copias el certificado nuevo, recargas nginx, y rezar para que el reload no tumbe las conexiones abiertas. HAProxy tiene un API para insertar el certificado nuevo antes de que caduque el viejo, y solo entonces descartar el anterior.
El ciclo completo son cuatro comandos, y el orden importa:
# 1. Registrar el certificado nuevo en memoria (el nombre es la clave)
echo "new ssl cert /etc/haproxy/certs/lab.example.com.pem" | socat /run/haproxy/admin.sock stdio
# 2. Enviar el contenido PEM: el delimitador << marca el fin del payload
printf 'set ssl cert /etc/haproxy/certs/lab.example.com.pem <<\n%s\n\n' "$(cat /etc/haproxy/certs/lab.example.com.pem)" | socat /run/haproxy/admin.sock stdio
# 3. Confirmar el certificado nuevo con commit
echo "commit ssl cert /etc/haproxy/certs/lab.example.com.pem" | socat /run/haproxy/admin.sock stdio
# 4. Añadirlo a la crt-list que usa el frontend (con la ruta completa)
echo "add ssl crt-list /etc/haproxy/certs/certs.list /etc/haproxy/certs/lab.example.com.pem" | socat /run/haproxy/admin.sock stdio
El paso 2 es el que más confunde la primera vez. La forma que documenta el manual oficial usa el delimitador << al final de la primera línea y un salto de línea al final del payload:
printf 'set ssl cert /etc/haproxy/certs/lab.example.com.pem <<\n%s\n\n' "$(cat /etc/haproxy/certs/lab.example.com.pem)" | socat /run/haproxy/admin.sock stdio
Ese delimitador le dice al proceso de administración que lo que sigue es el contenido del certificado. Y hay un detalle fino que el manual sí advierte: una línea vacía es la que marca el final del payload, por eso el \n\n del final. Si el PEM que envías tiene líneas en blanco intercaladas, la carga se corta antes de tiempo. El propio manual recomienda sanear el archivo antes de enviarlo, dejando solo las líneas entre BEGIN y END:
printf 'set ssl cert /etc/haproxy/certs/lab.example.com.pem <<\n%s\n\n' "$(sed -n '/^$/d;/-BEGIN/,/-END/p' /etc/haproxy/certs/lab.example.com.pem)" | socat /run/haproxy/admin.sock stdio
Fíjate también en que el comando se llama set ssl cert y no new ssl cert: new ssl cert reserva el nombre y crea un almacén de certificados vacío, y set ssl cert escribe el contenido en una transacción. commit ssl cert es el que aplica el cambio. Si te saltas el commit, HAProxy responde No ongoing transaction! cuando lo intentas.
La alternativa, cuando ya tienes el certificado en un archivo y quieres reemplazar uno que ya está cargado, es set ssl cert directamente, sin el new ssl cert previo. Y para quitar certificados que ya no usas están del ssl crt-list y del ssl cert.
Una advertencia sobre el nombre de la crt-list: no es el nombre del archivo, sino la ruta completa con la que la cargaste en la configuración. Si en el bind pusiste crt-list /etc/haproxy/certs/certs.list, en el comando hay que escribir exactamente /etc/haproxy/certs/certs.list. Con solo certs.list la respuesta es Can't edit the crt-list: crt-list 'certs.list' does not exist!. Para ver el nombre real, usa show ssl crt-list.
La ventaja práctica de este esquema es que durante la rotación ambas versiones están sirviendo al mismo tiempo. HAProxy mantiene las conexiones establecidas sobre el certificado viejo hasta que se cierran de forma natural, mientras las nuevas que negocian ya reciben el nuevo. No hay ningún instante sin servicio.
Renovación automática con ACME nativo
Aquí es donde la rama 3.4 se diferencia de verdad. HAProxy puede hablar ACME por su cuenta: pedir el certificado, responder el desafío HTTP-01, guardar la clave de cuenta y renovar solo, sin certbot y sin hooks.
El requisito que casi nadie menciona y que el manual sí deja claro: las directivas ACME son experimentales y necesitan una bandera explícita. Sin expose-experimental-directives en el bloque global, el proceso se niega a cargar la sección acme:
global
expose-experimental-directives
log stdout format raw local0 info
stats socket /run/haproxy/admin.sock mode 660 level admin expose-fd listeners
user haproxy
group haproxy
daemon
defaults
mode http
timeout connect 5s
timeout client 30s
timeout server 30s
frontend fe_https
bind *:80
bind *:443 ssl
http-request return status 200 content-type text/plain lf-string "%[path,field(-1,/)].%[path,field(-1,/),map(virt@acme)]\n" if { path_beg '/.well-known/acme-challenge/' }
ssl-f-use crt "/etc/haproxy/certs/foo.example.com.pem" acme LE1 domains "foo.example.com"
acme LE1
directory https://acme-v02.api.letsencrypt.org/directory
account-key /etc/haproxy/acme/letsencrypt.account.key
contact [email protected]
challenge http-01
keytype RSA
bits 2048
map virt@acme
Ese bloque está construido sobre el ejemplo de la sección 12.8 del manual de configuración de 3.4, recortado a una sola sección ACME y con los nombres de archivo que usa este homelab. Funciona tal cual: el http-request return con map(virt@acme) responde el desafío HTTP-01 con el token que guarda el map, y la directiva ssl-f-use crt ... acme LE1 domains "..." le dice a qué sección ACME pertenece el certificado y para qué dominios sirve. El manual original usa foo.example.com.pem.rsa y define dos secciones (LE1 y LE2) para una clave RSA y otra ECDSA; aquí basta una.
Un detalle de placement que el manual no explica y que te va a ahorrar un rato de depuración: la línea map virt@acme va dentro de la sección acme, no dentro del frontend. El nombre del mapa es el que el http-request consulta con el conversor map(), y el mapa se registra cuando se procesa la sección acme. Si lo pones en el frontend, HAProxy lo rechaza con un error explícito al arrancar:
[ALERT] (1) : config : parsing [/etc/haproxy/haproxy.cfg:13] : unknown keyword 'map' in 'frontend' section
El error es el de una palabra que no existe en ese contexto, no un problema de sintaxis del ACME. Por eso el mensaje no menciona ni la sección acme ni el token: el parser ni siquiera llegó a evaluarla.
Y hay una segunda trampa, más silenciosa: si declaras el ssl-f-use crt ... acme LE1 pero olvidas la línea map virt@acme dentro de la sección acme, HAProxy arranca sin quejarse. La validación con haproxy -c pasa y el servicio levanta limpio. Lo único que cambia es la respuesta al desafío: el map(virt@acme) devuelve vacío y el token que se sirve al validador de Let’s Encrypt queda en blanco. El error aparece entonces del lado de la CA, como un fallo de validación del desafío, y no como un problema de configuración de HAProxy. Por eso la única forma de detectarlo es leer la respuesta real del endpoint de desafío, no el log de arranque.
El planificador de renovación viene incluido y se controla con la directiva global acme.scheduler. Por defecto está en modo auto, que calcula cuándo renovar a partir de la fecha de expiración del certificado. Puedes desactivarlo con acme.scheduler off si prefieres controlar los tiempos por tu cuenta.
Para seguirle el rastro a la renovación, el API de administración expone dos comandos propios:
# Estado de cada certificado ACME: seccion, estado, expiracion y proxima renovacion
echo "acme status" | socat /run/haproxy/admin.sock stdio
# Forzar una renovacion inmediata de un certificado concreto
echo "acme renew foo.example.com.pem" | socat /run/haproxy/admin.sock stdio
La salida de acme status son columnas separadas por tabulador, con la sección ACME, el estado de la tarea (Running, Scheduled o Stopped), la fecha de expiración en UTC con formato ISO 8601 y el tiempo relativo que queda. Es el comando que quieres en un panel de monitoreo.
Lo que viene: certificados de 64 y 45 días
Si tu homelab renueva certificados cada 60 días, el cambio que viene en 2027 te va a alcanzar. Let’s Encrypt ha anunciado un calendario de reducción de la validez por los requisitos del CA/Browser Forum, y las fechas ya están fijadas:
| Fecha | Perfil | Validez | Reuso de autorización |
|---|---|---|---|
| 2026-05-13 | tlsserver (opt-in) | 45 días | Sin cambio |
| 2027-02-10 | classic (por defecto) | 64 días | 10 días |
| 2028-02-16 | classic (por defecto) | 45 días | 7 horas |
El 10 de febrero de 2027 todos los suscriptores de Let’s Encrypt pasan a certificados de 64 días por defecto, salvo que opten por un perfil más corto. El último certificado de 90 días se espera que expire el 11 de mayo de 2027. Los cambios llegan primero al entorno de staging: el 14 de octubre de 2026 Let’s Encrypt ya empezó a emitir ahí con 64 días para permitir las pruebas.
Dos consecuencias prácticas para tu homelab:
Tu cron de renovación tiene que dejar de hardcodear fechas. El anuncio de Let’s Encrypt es explícito en esto: si tus renovaciones están clavadas a una fecha calculada desde la expiración, debes cambiarlas para renovar aproximadamente a los 2/3 de la vida del certificado. La propia Let’s Encrypt recomienda buscar números como 83, 80 o 60 en tus cron jobs, scripts y runbooks, porque son los valores que la gente viene usando desde la época de los 90 días.
El reuso de autorización se reduce de 30 días a 10 días, y luego a 7 horas. Esto significa que los tokens de validación de un dominio no se pueden reutilizar indefinidamente: si tu cliente ACME depende del reuso, hay que ajustarlo. La razón de fondo es eliminar la necesidad de “rechecking” de CAA, donde una parte de la validación tiene que repetirse cuando los datos tienen más de 7 horas.
Y hay un punto que ya está en vigor desde hace meses: desde el 7 de mayo de 2025 los certificados de Let’s Encrypt no incluyen la URL OCSP. En su lugar llevan un punto de distribución de CRL (CRLDP), y el estado de revocación se consulta por CRL. Si tienes alguna alerta o script que espere una URL OCSP en el certificado, dejó de encontrarlo. Desde el 4 de junio de 2025, además, Let’s Encrypt apagó su servicio de correos de notificación de expiración y borró las direcciones de correo de las cuentas ACME de producción: no vas a recibir el aviso de que tu certificado caduca en 30 días, tienes que controlarlo tú.
Esto conecta directamente con la parte de ACME nativo de HAProxy: el planificador integrado de la sección acme calcula la ventana de renovación a partir de la expiración real del certificado, no de una fecha fija en tu cron. Ese es exactamente el tipo de automatización que la reducción de 64 días vuelve obligatoria.
Dos CVE de febrero de 2026 que debes revisar
HAProxy tuvo dos avisos de seguridad publicados el 12 de febrero de 2026, ambos en el parser de QUIC. Ninguno es un problema de configuración, sino fallos en el propio código, así que no hay ningún ajuste de configuración que los mitigue: hay que actualizar.
| CVE | Alcance afectado | Problema |
|---|---|---|
| CVE-2026-26081 | 3.0 y superiores | Falta una comprobación de longitud en el formato NEW_TOKEN |
| CVE-2026-26080 | 3.2 y superiores | Puede entrar en un bucle o fallar por un manejo incorrecto de varint |
El anuncio de haproxy.org es claro en el riesgo: paquetes QUIC especialmente construidos pueden tumbar el proceso. Y en las versiones corregidas no se quedó una sola rama: se publicaron la 3.0.12, la 3.1.14, la 3.2.12 y la 3.3.3. Cualquiera de las dos ramas en las que estés te sirve para salir de la zona afectada.
En Debian, el tracker marca ambos CVEs como corregidos en trixie a partir de la 3.0.11-1+deb13u2, y el advisory DSA-6130-1 recoge la actualización de bookworm a la 2.6.12-1+deb12u3. Ojo con esto: la 2.6 de bookworm no estaba en el rango afectado por ninguno de los dos CVE, sino que recibió una corrección por otros avisos del mismo periodo. La 2.8 y la 2.6 no aparecen entre las versiones corregidas porque los fallos se introdujo en código posterior a esas ramas, no porque les faltara un parche.
La conclusión práctica: si tu homelab corre HAProxy 3.0, 3.2 o 3.3 por debajo de los parches de tu distribución, actualiza el paquete. Y si vas a 3.4 directamente, ya estás por encima de la zona afectada por los dos avisos.
El checklist antes de dar por bueno el montaje
Antes de poner esto en producción o dejarlo corriendo en el homelab, verifica punto por punto:
- La configuración parsea:
haproxy -c -f /etc/haproxy/haproxy.cfgsin errores. Es el comando que te va a ahorrar el dolor de cabeza del reinicio con una configuración rota. - El socket de administración responde:
echo "show stat" | socat /run/haproxy/admin.sock stdiodevuelve la tabla de estadísticas con los backends en estadoUP. - Los certificados cargados son los correctos:
show ssl certyshow ssl snimuestran lo que esperas, sin duplicados para un mismo frontend. - El SNI responde bien:
openssl s_client -connect lab.example.com:443 -servername lab.example.comdevuelve el certificado de ese dominio, y con otro-servernameel certificado cambia. - El ACME está realmente configurado:
acme statuslista tus certificados con la sección ACME y una fecha de renovación programada, no vacía. - Los health checks responden: cada
serverdel backend está enUPenshow staty sucheck_statusesHTTP 200.
Preguntas frecuentes
¿HAProxy reemplaza a Nginx o conviven con él?
Conviven, y en un homelab es lo normal. HAProxy se ocupa del reparto de carga y de terminar TLS con SNI, y Nginx puede quedarse como servidor de aplicaciones detrás. También es válido poner Nginx delante y HAProxy detrás, o usar HAProxy como proxy TCP puro sin tocar TLS. Lo que no tiene sentido es poner los dos en el mismo 443 sin direcciones distintas.
¿Por qué el balanceo por defecto cambió a random en la 3.3?
Porque el round-robin ignora el peso real de las conexiones. Con servidores de capacidades distintas mezclados, reparte a ciegas y las peticiones lentas se acumulan en el que peor puede responder. El random sortea entre los servidores disponibles, y el parámetro (2) hace que cada elección se base en dos backends sorteados, con una distribución más equilibrada en la práctica.
¿El ACME nativo sustituye a certbot?
Para el caso de esta guía, sí: HAProxy pide, renueva y mantiene los certificados por su cuenta, y el planificador integrado decide la ventana de renovación a partir de la expiración real. Pero es una función experimental, por eso exige expose-experimental-directives, y solo existe desde la rama 3.2. Para DNS-01 con proveedor específico, certbot sigue siendo más flexible.
¿Qué pasa con los certificados de 45 días y mis scripts de renovación?
Es el cambio que más código toca. Con los 64 días por defecto desde el 10 de febrero de 2027, renovar a los 60 días ya no alcanza: el certificado puede caducar antes de que tu script corra. Deja de calcular la fecha desde la expiración y renueva a los 2/3 de la vida del certificado. Busca en tus cron jobs números como 83, 80 o 60.
¿Necesito privileged ports o root para que funcione?
No. HAProxy no necesita root para terminar TLS: puede escuchar en el 443 con CAP_NET_BIND_SERVICE o con setcap sobre el binario. En un homelab lo más simple es un bind *:443 con el usuario haproxy y el permiso de bajo puerto dado por systemd. El resto del proceso corre sin privilegios.
Lee también
- Let’s Encrypt de 90 a 45 días: ARI, perfiles ACME y cómo renovar sin romperte en 2027
- Certificado wildcard con DNS-01: un solo certificado para todas tus apps del home server
- Certificados TLS internos para tu LAN: mkcert y CA privada
- Hardening TLS en Nginx y Apache: TLS 1.3, cipher suites, HSTS y cabeceras de seguridad
- Montaje de un servidor web en Linux: guía completa para sysadmins hispanohablantes
