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

OffloadingPassthroughRe-cifrado
Dónde se descifraEn el proxyEn el backendEn el proxy y otra vez en el backend
Tráfico proxy a backendHTTP en claroTLS original del clienteTLS nuevo
Certificado público enProxyCada backendProxy (y uno interno en cada backend)
El proxy ve HTTP (rutas, cabeceras, cookies)SíNo, solo el SNISí
Coste de CPU en el proxyUn handshake por conexión de clienteCasi nuloDos handshakes (se mitiga con keepalive)
Caso típicoRed privada de confianzaEl 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.
  • sni y check-sni: envían el nombre en el ClientHello del 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, keepalive en 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 tcp en cada backend. Haz que la aplicación confíe en X-Forwarded-For solo 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.