Cuando pones un proxy inverso o un balanceador delante de tus aplicaciones tienes que decidir dónde se descifra el tráfico TLS. Hay tres estrategias: descifrar en el proxy y hablar HTTP con los backends (offloading), dejar pasar el tráfico cifrado sin tocarlo (passthrough) o descifrar en el proxy y volver a cifrar hacia los backends (re-cifrado). En esta guía verás qué implica cada una, con configuraciones funcionales de HAProxy y Nginx en Ubuntu 24.04, cómo comprobar que funcionan y qué criterio seguir para elegir.
Requisitos previos
- Un servidor con Ubuntu 24.04 LTS que actúe como proxy, por ejemplo un VPS de CubePath, y al menos un backend accesible por red privada.
- Un usuario no root con privilegios
sudo. - Un dominio (en los ejemplos,
your_domain) con un registro DNS A apuntando a la IP pública del proxy y un certificado válido para él (por ejemplo de Let's Encrypt). - HAProxy (
sudo apt install haproxy, versión 2.8 en Ubuntu 24.04) o Nginx (sudo apt install nginx, versión 1.24). - Los puertos 80 y 443 TCP abiertos en el proxy:
sudo ufw allow 80,443/tcp.
En los ejemplos los backends son 10.0.0.11 y 10.0.0.12. Sustitúyelos por las IP privadas de tus servidores.
Resumen de las tres estrategias
| Offloading | Passthrough | Re-cifrado | |
|---|---|---|---|
| Dónde se descifra | En el proxy | En el backend | En el proxy y otra vez en el backend |
| Tráfico proxy a backend | HTTP en claro | TLS original del cliente | TLS nuevo |
| Certificado público en | Proxy | Cada backend | Proxy (y uno interno en cada backend) |
| El proxy ve HTTP (rutas, cabeceras, cookies) | Sí | No, solo el SNI | Sí |
| Coste de CPU en el proxy | Un handshake por conexión de cliente | Casi nulo | Dos handshakes (se mitiga con keepalive) |
| Caso típico | Red privada de confianza | El backend debe ver el TLS del cliente (mTLS extremo a extremo, cumplimiento) | Red no confiable entre proxy y backend, requisitos de cifrado extremo a extremo |
Offloading: terminar TLS en el proxy
Con offloading el proxy tiene el certificado, descifra la petición, puede aplicar reglas de capa 7 (enrutado por ruta, cabeceras, compresión, caché) y reenvía HTTP en claro al backend. Es la opción más sencilla de operar: solo hay un sitio donde renovar certificados. Úsala cuando la red entre proxy y backends sea privada y de confianza, como una red privada entre VPS.
HAProxy
HAProxy espera el certificado y la clave concatenados en un único archivo PEM. Con un certificado de Let's Encrypt:
sudo mkdir -p /etc/haproxy/certs
sudo sh -c 'cat /etc/letsencrypt/live/your_domain/fullchain.pem /etc/letsencrypt/live/your_domain/privkey.pem > /etc/haproxy/certs/your_domain.pem'
sudo chmod 600 /etc/haproxy/certs/your_domain.pem
Añade el frontend y el backend al final de /etc/haproxy/haproxy.cfg:
sudo nano /etc/haproxy/haproxy.cfg
frontend fe_https
bind :80
bind :443 ssl crt /etc/haproxy/certs/your_domain.pem alpn h2,http/1.1
mode http
http-request redirect scheme https code 301 unless { ssl_fc }
option forwardfor
http-request set-header X-Forwarded-Proto https if { ssl_fc }
default_backend be_app
backend be_app
mode http
balance roundrobin
server app1 10.0.0.11:8080 check
server app2 10.0.0.12:8080 check
X-Forwarded-Proto y X-Forwarded-For (añadida por option forwardfor) permiten a la aplicación saber que el cliente usó HTTPS y cuál es su IP real. Valida la configuración y recarga:
sudo haproxy -c -f /etc/haproxy/haproxy.cfg
sudo systemctl reload haproxy
Configuration file is valid
Nginx
En Nginx, el bloque server termina TLS y proxy_pass apunta a un upstream HTTP:
sudo nano /etc/nginx/sites-available/your_domain
upstream app_backend {
server 10.0.0.11:8080;
server 10.0.0.12:8080;
keepalive 32;
}
server {
listen 443 ssl http2;
server_name your_domain;
ssl_certificate /etc/letsencrypt/live/your_domain/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/your_domain/privkey.pem;
ssl_protocols TLSv1.2 TLSv1.3;
location / {
proxy_pass http://app_backend;
proxy_http_version 1.1;
proxy_set_header Connection "";
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
}
server {
listen 80;
server_name your_domain;
return 301 https://$host$request_uri;
}
Actívalo y recarga:
sudo ln -s /etc/nginx/sites-available/your_domain /etc/nginx/sites-enabled/
sudo nginx -t
sudo systemctl reload nginx
Comprobar el offloading
Desde tu equipo, comprueba que el certificado lo presenta el proxy y que la respuesta llega del backend:
curl -sI https://your_domain
HTTP/2 200
server: nginx/1.24.0 (Ubuntu)
...
En el backend, una captura en el puerto de la aplicación debe mostrar HTTP legible, lo que confirma que el tramo interno va en claro:
sudo tcpdump -i any -A -c 20 'tcp port 8080'
Passthrough: el proxy no descifra nada
Con passthrough el proxy trabaja en capa 4: recibe la conexión TCP, lee el nombre del servidor (SNI) del mensaje ClientHello, que va sin cifrar, y reenvía los bytes tal cual al backend elegido. El certificado y la clave privada solo existen en los backends.
La contrapartida es que el proxy no puede ver rutas, cabeceras ni cookies, así que no puede enrutar por URL, añadir X-Forwarded-For ni hacer sesiones persistentes por cookie. La IP del cliente solo llega al backend si ambos lados hablan el protocolo PROXY.
HAProxy
Este frontend sustituye al de offloading: dos frontends no pueden escuchar en el mismo puerto.
frontend fe_tls_passthrough
bind :443
mode tcp
option tcplog
tcp-request inspect-delay 5s
tcp-request content accept if { req_ssl_hello_type 1 }
use_backend be_app1_tls if { req_ssl_sni -i app1.your_domain }
use_backend be_app2_tls if { req_ssl_sni -i app2.your_domain }
backend be_app1_tls
mode tcp
server app1 10.0.0.11:443 check
backend be_app2_tls
mode tcp
server app2 10.0.0.12:443 check
tcp-request inspect-delay hace que HAProxy espere hasta recibir el ClientHello completo antes de decidir el backend. Si tus backends aceptan el protocolo PROXY (Nginx con listen 443 ssl proxy_protocol;), añade send-proxy-v2 a cada línea server para que reciban la IP real del cliente.
Nginx
En Nginx el passthrough se hace con el módulo stream y ssl_preread. En Ubuntu 24.04 el módulo viene en un paquete aparte:
sudo apt install libnginx-mod-stream
El bloque stream debe estar al nivel superior de nginx.conf, fuera de http. Crea un directorio para su configuración:
sudo mkdir -p /etc/nginx/stream.d
Añade este bloque al final de /etc/nginx/nginx.conf, fuera del bloque http:
stream {
include /etc/nginx/stream.d/*.conf;
}
Crea /etc/nginx/stream.d/passthrough.conf:
sudo nano /etc/nginx/stream.d/passthrough.conf
map $ssl_preread_server_name $tls_backend {
app1.your_domain 10.0.0.11:443;
app2.your_domain 10.0.0.12:443;
default 10.0.0.11:443;
}
server {
listen 443;
ssl_preread on;
proxy_pass $tls_backend;
}
Ningún bloque server del contexto http puede escuchar en el 443 a la vez, o Nginx no arrancará. Comprueba y recarga:
sudo nginx -t
sudo systemctl reload nginx
Comprobar el passthrough
El certificado que recibe el cliente debe ser el del backend, no uno instalado en el proxy:
openssl s_client -connect your_server_ip:443 -servername app1.your_domain </dev/null 2>/dev/null | openssl x509 -noout -subject -issuer
subject=CN = app1.your_domain
issuer=C = US, O = Let's Encrypt, CN = R11
Re-cifrado: descifrar y volver a cifrar
El re-cifrado (también llamado SSL bridging) combina ambas: el proxy termina el TLS del cliente para poder aplicar reglas de capa 7 y abre una conexión TLS nueva hacia el backend. El tramo interno queda cifrado, a cambio de gestionar certificados también en los backends y de un segundo handshake. Ese coste se reduce mucho reutilizando conexiones (keepalive) y sesiones TLS.
Los backends pueden usar certificados de una CA interna. Lo importante es que el proxy verifique ese certificado: re-cifrar sin verificar protege frente a escuchas pasivas, pero no frente a alguien que suplante al backend. En los ejemplos, la CA interna está en /etc/ssl/internal/ca.crt y los backends tienen certificados para app1.internal y app2.internal.
HAProxy
El frontend es el mismo que en offloading. Solo cambia el backend:
backend be_app_reencrypt
mode http
balance roundrobin
server app1 10.0.0.11:443 ssl verify required ca-file /etc/ssl/internal/ca.crt verifyhost app1.internal sni str(app1.internal) check check-sni app1.internal
server app2 10.0.0.12:443 ssl verify required ca-file /etc/ssl/internal/ca.crt verifyhost app2.internal sni str(app2.internal) check check-sni app2.internal
ssl verify required ca-file: cifra hacia el backend y exige un certificado firmado por tu CA.verifyhost: comprueba que el certificado corresponde a ese nombre.sniycheck-sni: envían el nombre en elClientHellodel tráfico y de las comprobaciones de salud.
Nginx
upstream app_backend_tls {
server 10.0.0.11:443;
keepalive 32;
}
server {
listen 443 ssl http2;
server_name your_domain;
ssl_certificate /etc/letsencrypt/live/your_domain/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/your_domain/privkey.pem;
location / {
proxy_pass https://app_backend_tls;
proxy_http_version 1.1;
proxy_set_header Connection "";
proxy_set_header Host $host;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_ssl_verify on;
proxy_ssl_trusted_certificate /etc/ssl/internal/ca.crt;
proxy_ssl_name app1.internal;
proxy_ssl_server_name on;
proxy_ssl_protocols TLSv1.2 TLSv1.3;
proxy_ssl_session_reuse on;
}
}
Por defecto Nginx no verifica el certificado del backend: sin proxy_ssl_verify on aceptaría cualquiera. proxy_ssl_name fija el nombre con el que se compara el certificado. Con varios backends que tienen nombres distintos, emite un certificado interno con un nombre común para todos (por ejemplo app.internal como SAN en cada uno).
Comprobar el re-cifrado
Desde el proxy, verifica que el backend presenta un certificado válido para tu CA y el nombre esperado:
openssl s_client -connect 10.0.0.11:443 -servername app1.internal -verify_hostname app1.internal -CAfile /etc/ssl/internal/ca.crt </dev/null 2>/dev/null | grep "Verify return code"
Verify return code: 0 (ok)
Después haz una petición al dominio público. Si el proxy no puede verificar el backend, Nginx devuelve 502 Bad Gateway y registra upstream SSL certificate verify error en /var/log/nginx/error.log.
Rendimiento
Las diferencias reales dependen del hardware, del tamaño de las respuestas y, sobre todo, de cuántas conexiones nuevas se abren. Algunas pautas:
- Con TLS 1.3 y CPUs modernas con AES-NI, el cifrado simétrico apenas consume CPU. El coste está en los handshakes, sobre todo con claves RSA grandes. Los certificados ECDSA reducen ese coste.
- El passthrough es el más ligero para el proxy porque no hace criptografía, pero pierde todas las funciones de capa 7.
- En re-cifrado,
keepaliveen el upstream (Nginx) y la reutilización de sesiones hacen que el segundo handshake se produzca pocas veces. Sin ellos, cada petición paga dos handshakes.
Mide con tu propia carga antes de decidir por rendimiento. Herramientas como wrk o h2load contra el proxy, comparando CPU con top o pidstat, darán cifras más útiles que cualquier benchmark genérico.
Consideraciones de seguridad
- Offloading: el tráfico interno va en claro. Limita el acceso al puerto de los backends para que solo el proxy pueda conectar, por ejemplo con
sudo ufw allow from 10.0.0.10 to any port 8080 proto tcpen cada backend. Haz que la aplicación confíe enX-Forwarded-Forsolo cuando la petición venga de la IP del proxy. - Passthrough: cada backend guarda la clave privada del certificado público y debe mantener su propia configuración TLS actualizada. El proxy no puede aplicar un WAF ni limitar peticiones por ruta.
- Re-cifrado: activa siempre la verificación del certificado del backend y protege la clave de la CA interna. Los certificados internos también caducan: incluye su renovación en tu automatización.
¿Qué estrategia elegir?
- Offloading si tus backends están en una red privada que controlas. Es la opción más habitual y la más fácil de mantener.
- Passthrough si el backend necesita el TLS original del cliente, por ejemplo para validar certificados de cliente (mTLS) o porque una norma exige que la clave no salga del servidor de aplicación, y no necesitas reglas de capa 7 en el proxy.
- Re-cifrado si el tráfico entre proxy y backend cruza redes que no controlas (otro centro de datos, Internet) o si tu política de seguridad exige cifrado en todos los tramos, y a la vez necesitas funciones de capa 7.
Puedes combinarlas en el mismo proxy: offloading para la web pública, passthrough por SNI para un servicio con mTLS y re-cifrado para un backend remoto.
Conclusión
Has visto cómo configurar offloading, passthrough y re-cifrado en HAProxy y Nginx, y cómo comprobar en cada caso quién presenta el certificado y si el tramo interno va cifrado. Como siguientes pasos, automatiza la renovación de certificados con Certbot y un hook que regenere el PEM de HAProxy, restringe con UFW el acceso a los backends y, si eliges passthrough, activa el protocolo PROXY para conservar la IP real del cliente.
