--- title: "Terminación SSL en HAProxy con Let's Encrypt en Ubuntu 24.04" description: "Configura HAProxy en Ubuntu 24.04 para terminar TLS con Let's Encrypt, servir varios dominios por SNI, enrutar con ACL y mapas y limitar peticiones por IP." category: "Proxy y Equilibrio de Carga" language: es source: https://cubepath.com/es/docs/proxy-y-equilibrio-de-carga/configuracion-avanzada-de-haproxy-terminacion-ssl updated: 2026-09-25 --- # Terminación SSL en HAProxy con Let's Encrypt en Ubuntu 24.04 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 `A` apuntando a la IP pública del balanceador. En esta guía se usan `your_domain` y `api.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) y `10.0.0.21` (API), todas escuchando en el puerto `8080` y 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: ```bash sudo apt update sudo apt install haproxy ``` Comprueba la versión y que el servicio está activo: ```bash haproxy -v | head -n 1 systemctl status haproxy --no-pager ``` ```output 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: ```bash 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: ```bash sudo cp /etc/haproxy/haproxy.cfg /etc/haproxy/haproxy.cfg.orig ``` Abre el archivo de configuración: ```bash sudo nano /etc/haproxy/haproxy.cfg ``` Sustituye su contenido por lo siguiente: ```haproxy 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 los `bind ... ssl`. - `defaults`: modo HTTP, logs detallados y la cabecera `X-Forwarded-For` para 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_web` y `be_api`: backends con comprobación de salud HTTP contra `/health`. Valida la sintaxis y recarga el servicio: ```bash sudo haproxy -c -f /etc/haproxy/haproxy.cfg sudo systemctl reload haproxy ``` ```output Configuration file is valid ``` ## Paso 3: Obtener los certificados con Certbot Instala Certbot: ```bash 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: ```bash sudo certbot certonly --standalone \ --http-01-address 127.0.0.1 --http-01-port 8888 \ -d your_domain -d www.your_domain ``` ```bash 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//` y recuerda estas opciones para las renovaciones automáticas: ```output 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: ```bash sudo install -d -m 700 /etc/haproxy/certs ``` Crea el hook: ```bash sudo nano /etc/letsencrypt/renewal-hooks/deploy/haproxy.sh ``` ```bash #!/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: ```bash 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/ ``` ```output -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: ```bash sudo nano /etc/haproxy/haproxy.cfg ``` Sustituye el bloque `frontend http_in` completo por estos dos frontends: ```haproxy 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.1` negocia 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-Security` indica al navegador que use siempre HTTPS. Actívalo solo cuando todo el dominio funcione bien por HTTPS. Valida y recarga: ```bash sudo haproxy -c -f /etc/haproxy/haproxy.cfg sudo systemctl reload haproxy ``` Comprueba desde tu equipo que la redirección y el certificado funcionan: ```bash curl -I http://your_domain curl -I https://your_domain ``` ```output 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: ```bash openssl s_client -connect your_domain:443 -servername api.your_domain /dev/null \ | openssl x509 -noout -subject -dates ``` ```output 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: ```haproxy 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: ```bash sudo install -d /etc/haproxy/maps sudo nano /etc/haproxy/maps/hosts.map ``` ```text 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: ```haproxy 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: ```bash 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 ``` ```output 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`: ```haproxy 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: ```bash 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: ```bash sudo apt install socat echo "show table https_in" | sudo socat stdio /run/haproxy/admin.sock ``` ```output # 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: ```bash 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`: ```haproxy 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: ```bash 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: ```bash sudo certbot renew --dry-run systemctl list-timers certbot.timer --no-pager ``` ```output 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 file` o `no certificate found`**: el directorio `/etc/haproxy/certs/` está vacío o un PEM no contiene la clave. Revisa con `sudo openssl x509 -in /etc/haproxy/certs/your_domain.pem -noout -subject`. - **Certbot falla con `Connection refused` o `Timeout during connect`**: comprueba que el DNS apunta al balanceador, que el puerto 80 está abierto y que la redirección a HTTPS contiene `unless is_acme`. - **Todos los servidores aparecen como `DOWN`**: la comprobación de salud no recibe un 2xx o 3xx. Prueba desde el balanceador con `curl -i -H "Host: your_domain" http://10.0.0.11:8080/health`. - **La aplicación genera enlaces `http://`**: configúrala para confiar en la cabecera `X-Forwarded-Proto` que envía HAProxy. - **Revisar qué ocurre**: `sudo tail -f /var/log/haproxy.log` muestra 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.