HAProxy es un balanceador de carga y proxy inverso de alto rendimiento. Cuando termina TLS, descifra el tráfico HTTPS en el propio balanceador y reenvía las peticiones en HTTP a los servidores backend, de modo que los certificados se gestionan en un único punto. En este tutorial configurarás HAProxy en Ubuntu 24.04 con certificados de Let's Encrypt renovados automáticamente, varios dominios en el mismo puerto 443 mediante SNI, enrutamiento por ACL y por archivo de mapa, cabeceras de seguridad y un límite de peticiones por IP.
Requisitos previos
- Un servidor con Ubuntu 24.04 LTS que hará de balanceador, por ejemplo un VPS de CubePath.
- Un usuario no root con privilegios
sudo. - Uno o más dominios con registros DNS
Aapuntando a la IP pública del balanceador. En esta guía se usanyour_domainyapi.your_domain: sustitúyelos por los tuyos. - Al menos un servidor backend con una aplicación HTTP. Los ejemplos usan las IP privadas
10.0.0.11,10.0.0.12(web) y10.0.0.21(API), todas escuchando en el puerto8080y respondiendo en/health. - Los puertos 80 y 443 abiertos hacia el balanceador.
Paso 1: Instalar HAProxy
Ubuntu 24.04 incluye HAProxy 2.8, una versión LTS con soporte hasta 2028 y todo lo necesario para esta guía:
sudo apt update
sudo apt install haproxy
Comprueba la versión y que el servicio está activo:
haproxy -v | head -n 1
systemctl status haproxy --no-pager
HAProxy version 2.8.5-1ubuntu3 2024/04/01 - https://haproxy.org/
● haproxy.service - HAProxy Load Balancer
Loaded: loaded (/usr/lib/systemd/system/haproxy.service; enabled; preset: enabled)
Active: active (running)
El paquete ya deja configurado rsyslog para escribir los logs de HAProxy en /var/log/haproxy.log.
Si usas UFW, permite el tráfico web:
sudo ufw allow 80/tcp
sudo ufw allow 443/tcp
Paso 2: Crear una configuración inicial solo con HTTP
Para emitir los certificados, Let's Encrypt necesita acceder por HTTP a /.well-known/acme-challenge/. En lugar de parar HAProxy en cada renovación, HAProxy reenviará esas rutas a Certbot, que escuchará en 127.0.0.1:8888. Todo lo demás se redirigirá a HTTPS más adelante.
Haz una copia de la configuración original:
sudo cp /etc/haproxy/haproxy.cfg /etc/haproxy/haproxy.cfg.orig
Abre el archivo de configuración:
sudo nano /etc/haproxy/haproxy.cfg
Sustituye su contenido por lo siguiente:
global
log /dev/log local0
log /dev/log local1 notice
chroot /var/lib/haproxy
stats socket /run/haproxy/admin.sock mode 660 level admin
stats timeout 30s
user haproxy
group haproxy
daemon
# Perfil "intermediate" de Mozilla: TLS 1.2 y 1.3
ssl-default-bind-ciphers ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305
ssl-default-bind-ciphersuites TLS_AES_128_GCM_SHA256:TLS_AES_256_GCM_SHA384:TLS_CHACHA20_POLY1305_SHA256
ssl-default-bind-options ssl-min-ver TLSv1.2 no-tls-tickets
defaults
log global
mode http
option httplog
option dontlognull
option forwardfor
timeout connect 5s
timeout client 30s
timeout server 30s
frontend http_in
bind :80
acl is_acme path_beg /.well-known/acme-challenge/
use_backend be_acme if is_acme
default_backend be_web
backend be_acme
server certbot 127.0.0.1:8888
backend be_web
balance roundrobin
option httpchk
http-check send meth GET uri /health ver HTTP/1.1 hdr Host your_domain
server web1 10.0.0.11:8080 check
server web2 10.0.0.12:8080 check
backend be_api
balance roundrobin
option httpchk
http-check send meth GET uri /health ver HTTP/1.1 hdr Host api.your_domain
server api1 10.0.0.21:8080 check
Qué hace cada bloque:
global: proceso, socket de administración (lo usarás en el paso 7) y parámetros TLS por defecto para todos losbind ... ssl.defaults: modo HTTP, logs detallados y la cabeceraX-Forwarded-Forpara que los backends vean la IP real del cliente.be_acme: backend sin comprobación de salud; solo recibe tráfico mientras Certbot está validando.be_webybe_api: backends con comprobación de salud HTTP contra/health.
Valida la sintaxis y recarga el servicio:
sudo haproxy -c -f /etc/haproxy/haproxy.cfg
sudo systemctl reload haproxy
Configuration file is valid
Paso 3: Obtener los certificados con Certbot
Instala Certbot:
sudo apt install certbot
Solicita un certificado por dominio. Certbot levanta un servidor temporal en 127.0.0.1:8888 y HAProxy le pasa el desafío:
sudo certbot certonly --standalone \
--http-01-address 127.0.0.1 --http-01-port 8888 \
-d your_domain -d www.your_domain
sudo certbot certonly --standalone \
--http-01-address 127.0.0.1 --http-01-port 8888 \
-d api.your_domain
Certbot guarda cada certificado en /etc/letsencrypt/live/<dominio>/ y recuerda estas opciones para las renovaciones automáticas:
Successfully received certificate.
Certificate is saved at: /etc/letsencrypt/live/your_domain/fullchain.pem
Key is saved at: /etc/letsencrypt/live/your_domain/privkey.pem
Paso 4: Preparar los certificados para HAProxy
HAProxy espera un único archivo PEM con la cadena completa y la clave privada. Un hook de despliegue de Certbot genera ese archivo en cada emisión o renovación y recarga HAProxy.
Crea el directorio de certificados, legible solo por root:
sudo install -d -m 700 /etc/haproxy/certs
Crea el hook:
sudo nano /etc/letsencrypt/renewal-hooks/deploy/haproxy.sh
#!/usr/bin/env bash
# Combina fullchain + clave para HAProxy y recarga el servicio.
set -euo pipefail
domain="$(basename "$RENEWED_LINEAGE")"
target="/etc/haproxy/certs/${domain}.pem"
umask 077
cat "$RENEWED_LINEAGE/fullchain.pem" "$RENEWED_LINEAGE/privkey.pem" > "${target}.tmp"
mv "${target}.tmp" "$target"
systemctl reload haproxy
Hazlo ejecutable y ejecútalo una vez por cada certificado ya emitido, pasando la variable que Certbot define en las renovaciones:
sudo chmod 755 /etc/letsencrypt/renewal-hooks/deploy/haproxy.sh
sudo RENEWED_LINEAGE=/etc/letsencrypt/live/your_domain /etc/letsencrypt/renewal-hooks/deploy/haproxy.sh
sudo RENEWED_LINEAGE=/etc/letsencrypt/live/api.your_domain /etc/letsencrypt/renewal-hooks/deploy/haproxy.sh
sudo ls -l /etc/haproxy/certs/
-rw------- 1 root root 5412 Sep 25 10:12 api.your_domain.pem
-rw------- 1 root root 5420 Sep 25 10:12 your_domain.pem
HAProxy lee los certificados como root antes de soltar privilegios, así que el permiso 600 es suficiente.
Paso 5: Activar HTTPS con varios certificados (SNI)
Si en crt indicas un directorio en lugar de un archivo, HAProxy carga todos los PEM que contiene y elige el correcto según el nombre que pide el cliente (SNI). Añadir un dominio nuevo solo requiere dejar su PEM en /etc/haproxy/certs/ y recargar.
Abre de nuevo la configuración:
sudo nano /etc/haproxy/haproxy.cfg
Sustituye el bloque frontend http_in completo por estos dos frontends:
frontend http_in
bind :80
acl is_acme path_beg /.well-known/acme-challenge/
http-request redirect scheme https code 301 unless is_acme
use_backend be_acme if is_acme
frontend https_in
bind :443 ssl crt /etc/haproxy/certs/ alpn h2,http/1.1
# Informa al backend de que la conexión original era HTTPS
http-request set-header X-Forwarded-Proto https
# Cabeceras de seguridad en todas las respuestas
http-response set-header Strict-Transport-Security "max-age=31536000"
http-response set-header X-Content-Type-Options nosniff
use_backend be_api if { req.hdr(host) -i api.your_domain }
default_backend be_web
alpn h2,http/1.1negocia HTTP/2 con los clientes que lo soportan; hacia los backends se sigue usando HTTP/1.1.- La redirección a HTTPS excluye el desafío ACME para que las renovaciones sigan funcionando.
Strict-Transport-Securityindica al navegador que use siempre HTTPS. Actívalo solo cuando todo el dominio funcione bien por HTTPS.
Valida y recarga:
sudo haproxy -c -f /etc/haproxy/haproxy.cfg
sudo systemctl reload haproxy
Comprueba desde tu equipo que la redirección y el certificado funcionan:
curl -I http://your_domain
curl -I https://your_domain
HTTP/1.1 301 Moved Permanently
location: https://your_domain/
HTTP/2 200
strict-transport-security: max-age=31536000
x-content-type-options: nosniff
Verifica que cada dominio recibe su propio certificado:
openssl s_client -connect your_domain:443 -servername api.your_domain </dev/null 2>/dev/null \
| openssl x509 -noout -subject -dates
subject=CN = api.your_domain
notBefore=Sep 25 09:10:00 2026 GMT
notAfter=Dec 24 09:09:59 2026 GMT
Paso 6: Enrutar con ACL y archivos de mapa
Las ACL permiten decidir el backend según cualquier propiedad de la petición. Las reglas use_backend se evalúan en orden y gana la primera que coincide.
Un ejemplo habitual es enviar /api/ al backend de API independientemente del dominio, y proteger una ruta de administración por IP. Añade estas líneas en frontend https_in, justo antes de las líneas use_backend existentes:
acl is_admin path_beg /admin
acl from_office src 203.0.113.0/24
http-request deny deny_status 403 if is_admin !from_office
use_backend be_api if { path_beg /api/ }
Sustituye 203.0.113.0/24 por la red desde la que se administra la aplicación.
Cuando el número de dominios crece, una lista de ACL se vuelve difícil de mantener. Un archivo de mapa asocia cada Host con un backend. Créalo:
sudo install -d /etc/haproxy/maps
sudo nano /etc/haproxy/maps/hosts.map
your_domain be_web
www.your_domain be_web
api.your_domain be_api
En frontend https_in, sustituye las dos últimas líneas (use_backend be_api if { req.hdr(host) ... } y default_backend be_web) por:
use_backend %[req.hdr(host),lower,map_str(/etc/haproxy/maps/hosts.map,be_web)]
El segundo argumento de map_str es el backend por defecto cuando el dominio no aparece en el mapa. Valida, recarga y prueba el enrutamiento:
sudo haproxy -c -f /etc/haproxy/haproxy.cfg
sudo systemctl reload haproxy
curl -s -o /dev/null -w '%{http_code}\n' https://api.your_domain/health
curl -s -o /dev/null -w '%{http_code}\n' https://your_domain/admin
200
403
Paso 7: Limitar la tasa de peticiones por IP
Una stick table guarda contadores por clave, en este caso la IP de origen. Con ella puedes rechazar a los clientes que superan un umbral de peticiones. Añade estas líneas al principio de frontend https_in, justo después de la línea bind:
stick-table type ip size 100k expire 30s store http_req_rate(10s)
http-request track-sc0 src
http-request deny deny_status 429 if { sc_http_req_rate(0) gt 100 }
Cada IP puede hacer hasta 100 peticiones en una ventana deslizante de 10 segundos; a partir de ahí recibe 429 Too Many Requests. Ajusta el umbral a tu tráfico real: detrás de un NAT corporativo muchos usuarios comparten IP.
Valida y recarga:
sudo haproxy -c -f /etc/haproxy/haproxy.cfg
sudo systemctl reload haproxy
Consulta el contenido de la tabla a través del socket de administración. Instala socat primero:
sudo apt install socat
echo "show table https_in" | sudo socat stdio /run/haproxy/admin.sock
# table: https_in, type: ip, size:102400, used:1
0x55d0c8a1b2c0: key=198.51.100.7 use=0 exp=28512 http_req_rate(10000)=12
El mismo socket permite cambiar entradas del mapa sin recargar. El cambio solo vive en memoria, así que añádelo también a hosts.map para que sobreviva a un reinicio:
echo "add map /etc/haproxy/maps/hosts.map blog.your_domain be_web" | sudo socat stdio /run/haproxy/admin.sock
echo "show map /etc/haproxy/maps/hosts.map" | sudo socat stdio /run/haproxy/admin.sock
Paso 8: Consultar la página de estadísticas
La página de estadísticas muestra el estado de cada backend y servidor. Publícala solo en localhost y accede mediante un túnel SSH. Añade al final de /etc/haproxy/haproxy.cfg:
frontend stats
bind 127.0.0.1:8404
stats enable
stats uri /stats
stats refresh 10s
Valida y recarga la configuración. Después, desde tu equipo, abre un túnel:
ssh -L 8404:127.0.0.1:8404 your_user@your_server_ip
Con el túnel abierto, visita http://localhost:8404/stats en el navegador. Los servidores en verde están pasando la comprobación de salud.
Paso 9: Comprobar la renovación automática
El paquete de Certbot instala un temporizador de systemd que intenta renovar dos veces al día. Haz una prueba en seco para confirmar que el desafío llega a Certbot a través de HAProxy:
sudo certbot renew --dry-run
systemctl list-timers certbot.timer --no-pager
Congratulations, all simulated renewals succeeded:
/etc/letsencrypt/live/api.your_domain/fullchain.pem (success)
/etc/letsencrypt/live/your_domain/fullchain.pem (success)
En las renovaciones reales, el hook del paso 4 regenerará el PEM y recargará HAProxy sin cortar conexiones.
Solución de problemas
unable to stat SSL certificate from fileono certificate found: el directorio/etc/haproxy/certs/está vacío o un PEM no contiene la clave. Revisa consudo openssl x509 -in /etc/haproxy/certs/your_domain.pem -noout -subject.- Certbot falla con
Connection refusedoTimeout during connect: comprueba que el DNS apunta al balanceador, que el puerto 80 está abierto y que la redirección a HTTPS contieneunless is_acme. - Todos los servidores aparecen como
DOWN: la comprobación de salud no recibe un 2xx o 3xx. Prueba desde el balanceador concurl -i -H "Host: your_domain" http://10.0.0.11:8080/health. - La aplicación genera enlaces
http://: configúrala para confiar en la cabeceraX-Forwarded-Protoque envía HAProxy. - Revisar qué ocurre:
sudo tail -f /var/log/haproxy.logmuestra cada petición con el frontend, el backend y el servidor que la atendió.
Conclusión
HAProxy ya termina TLS para varios dominios con certificados de Let's Encrypt que se renuevan solos, redirige HTTP a HTTPS, reparte el tráfico según el dominio y la ruta y frena a los clientes que abusan. Como siguientes pasos, puedes afinar las comprobaciones de salud y la conmutación por error de los backends, cifrar también el tramo hasta los backends con ssl verify required ca-file en las líneas server, o poner dos balanceadores en alta disponibilidad con keepalived y una IP flotante.
