Qué protege TLS realmente en una conexión HTTPS

Carga una página por http:// sin cifrar en una red que no controlas, un aeropuerto o el router de una cafetería, y todo lo que envías viaja como texto legible. La ruta de la URL, las cookies, los campos del formulario, la contraseña en un POST de login: cualquiera en esa red con un sniffer de paquetes lo lee al vuelo. TLS, Transport Layer Security, es el protocolo que corta eso. Se sitúa entre TCP y el protocolo de aplicación, así que cuando HTTP empieza a hablar, la conexión ya está cifrada y ligada a una identidad de servidor verificada. HTTPS es HTTP corriendo sobre esa conexión en lugar de una en texto plano.

TLS agrupa tres garantías, y solo valen juntas. Cifrado significa que un observador de la red ve texto cifrado y nada más. Integridad significa que un bit alterado en tránsito se detecta y la conexión se cae en lugar de entregar datos modificados. Autenticación significa que el cliente puede confirmar que llegó a example.com y no a quien respondió primero en esa IP. Pierde una de las tres y las otras dos dejan de valer gran cosa: un canal cifrado hacia un impostor no es un canal seguro.

Cómo el handshake de TLS establece un canal cifrado

Antes de que salga ninguna petición HTTP, cliente y servidor ejecutan un handshake que acuerda una clave secreta compartida sin ponerla nunca en la red. TLS 1.3, la versión actual y la que deberías usar, lo hace en un solo round trip, frente a los dos que necesitaba TLS 1.2.

1
2
3
4
5
6
7
8
9
Client                                           Server
  |--- ClientHello + key_share ------------------->|
  |    (versiones soportadas, cipher suites,       |
  |     una suposición del grupo de intercambio)    |
  |<-- ServerHello + key_share --------------------|
  |<-- EncryptedExtensions, Certificate,           |
  |    CertificateVerify, Finished ----------------|
  |--- Finished ---------------------------------->|
  |======== datos de aplicación, cifrados =========|

El cliente envía un ClientHello: las versiones de TLS y las cipher suites que soporta, un nonce aleatorio y una suposición sobre qué grupo de intercambio de claves elegirá el servidor, enviada como key_share en el mismo mensaje. Esa suposición es el atajo de TLS 1.3. El servidor responde con su propio key_share en el ServerHello, y ambos lados ejecutan un intercambio Diffie-Hellman sobre esas shares para derivar de forma independiente la misma clave de sesión simétrica. La clave en sí nunca cruza la red.

Desde ese punto todo lo demás ya va cifrado bajo esa clave de sesión: el certificado del servidor, su prueba de tener la clave privada correspondiente, los mensajes Finished que hacen el checksum de todo el handshake. Un round trip, y la conexión está lista.

TLS 1.2 necesitaba un segundo round trip porque el intercambio de claves y la negociación del cifrado corrían como pasos separados en lugar de solaparse. Ese viaje extra es latencia pura. En un enlace con 150ms de RTT añade 150ms a cada nueva conexión HTTPS, por eso TLS 1.3 se notó en los tiempos de carga de página y no solo en la postura de seguridad.

TLS 1.2TLS 1.3
Round trips hasta el primer byte cifrado21
Intercambio de clavesNegociado después de la cipher suiteAdivinado y enviado con ClientHello
Renegociación a mitad de sesiónPermitidaEliminada
Intercambio de claves RSA estáticoPermitidoEliminado (forward secrecy obligatorio)
Cifrados débiles (RC4, CBC legacy)PermitidosEliminados

Qué demuestra un certificado y cómo funciona la cadena de confianza

El intercambio de claves se encarga del cifrado. Demostrar que el servidor es de verdad example.com es trabajo del certificado. Un certificado ata una clave pública a un nombre de dominio y lleva la firma de una Certificate Authority (CA), una organización cuya propia clave pública ya viene incluida en los almacenes de confianza de tu sistema operativo y navegador.

En la práctica la cadena tiene tres niveles. Un certificado root CA, autofirmado y preinstalado en todas partes, firma un certificado intermediate CA. El intermedio firma el certificado hoja, el de tu dominio. Los servidores casi nunca presentan un certificado firmado directamente por una root: sirven la hoja más el intermedio, y el cliente reconstruye la cadena hasta una root de la que ya se fía.

Olvidar servir el intermedio produce un fallo que parece un fantasma. Los navegadores con una copia en caché de ese intermedio se conectan sin problema, así que el sitio funciona en tu portátil; todos los demás reciben un error de confianza. Es uno de los errores de configuración de TLS más comunes en producción, y nunca se reproduce en la máquina desde la que probaste.

Un certificado también lleva una ventana de validez (notBefore / notAfter) y los hostnames que cubre, listados en la extensión Subject Alternative Name (SAN). SAN es el campo que los clientes leen de verdad hoy; el antiguo campo Common Name es vestigial. Un certificado para example.com no cubre api.example.com a menos que ese nombre también esté en la lista SAN, o el certificado sea un wildcard para *.example.com.

Cómo inspeccionar un certificado con openssl s_client

No necesitas un navegador para ver nada de esto. openssl s_client abre una conexión TLS en crudo y vuelca lo que se negoció:

1
openssl s_client -connect example.com:443 -servername example.com </dev/null

-servername fija SNI (Server Name Indication), el hostname enviado sin cifrar en el ClientHello para que un servidor que aloja varios dominios en una IP sepa qué certificado presentar. Quítalo y un servidor multi-tenant no tiene forma de elegir.

Para solo la fecha de caducidad en lugar del volcado completo del handshake, pásalo por openssl x509:

1
2
echo | openssl s_client -connect example.com:443 -servername example.com 2>/dev/null \
  | openssl x509 -noout -dates -subject -issuer

Eso imprime notBefore, notAfter, el subject (el dominio para el que se emitió el certificado) y el issuer (la CA que lo firmó). Para un script de monitorización que solo necesita un pass/fail, -checkend acepta una ventana en segundos y fija el exit code:

1
2
3
4
openssl s_client -connect example.com:443 -servername example.com 2>/dev/null \
  | openssl x509 -noout -checkend 2592000 \
  && echo "OK: válido al menos 30 días más" \
  || echo "AVISO: caduca en menos de 30 días"

curl -v es la vía más rápida para una comprobación puntual, porque imprime la versión de TLS negociada y la cadena de certificados como parte de su traza detallada del handshake:

1
curl -v https://example.com 2>&1 | grep -E "SSL connection|subject:|expire date"

Cómo conseguir HTTPS en localhost con mkcert

Un certificado autofirmado en crudo, uno que generas y firmas tú mismo en lugar de conseguirlo de una CA, te da cifrado pero no autenticación. Nadie lo respalda, así que todos los navegadores muestran un aviso. Vale para una prueba rápida con openssl. Molesto para el desarrollo local, donde quieres que https://myapp.local cargue sin tener que saltarte el aviso a mano.

mkcert resuelve esto generando una CA local, instalándola en los almacenes de confianza del sistema operativo y del navegador, y emitiendo certificados firmados por ella:

1
2
mkcert -install                          # genera y da confianza a una CA local
mkcert localhost 127.0.0.1 myapp.local   # emite un certificado para estos nombres

Obtienes un par certificado/clave .pem en el directorio actual. Cada navegador de la máquina confía en él porque ahora confía en la CA que instaló mkcert. Apunta la configuración TLS de tu servidor de desarrollo a esos dos archivos y el aviso desaparece.

Mantén rootCA-key.pem fuera del control de versiones. Cualquiera que tenga la clave privada de la CA local de mkcert puede generar un certificado para cualquier dominio en el que tu máquina confíe, la misma clase de exposición que ves en variables de entorno y secrets en Docker: una clave privada que se filtra es una credencial filtrada.

Cómo conseguir un certificado de producción con Let’s Encrypt y certbot

En producción necesitas un certificado firmado por una CA en la que ya confía el navegador de cada visitante, y Let’s Encrypt es la opción gratuita y automatizada que usa la mayoría de sitios. certbot habla el protocolo ACME con los servidores de Let’s Encrypt y, con un plugin para el servidor web, configura Nginx o Apache por ti:

1
sudo certbot --nginx -d example.com -d www.example.com

Eso demuestra que controlas el dominio (sirviendo un token que certbot coloca, o mediante un registro DNS, según el tipo de challenge), obtiene el certificado y escribe las directivas ssl_certificate y ssl_certificate_key en tu configuración de Nginx.

Los certificados de Let’s Encrypt duran 90 días. La ventana corta es deliberada: hace que la renovación automática sea el único camino viable. Certbot instala un timer de systemd, o una tarea cron en sistemas más antiguos, que corre dos veces al día y renueva todo lo que esté a menos de 30 días de caducar:

1
sudo certbot renew --deploy-hook "systemctl reload nginx"

--deploy-hook se dispara solo en una renovación real, no en cada ejecución del timer, así que Nginx no se recarga dos veces al día por nada. Pruébalo antes de confiarle esto sin supervisión:

1
sudo certbot renew --dry-run

Cómo funciona la terminación TLS en un reverse proxy o una CDN

La mayoría de los servicios nunca terminan TLS dentro del proceso de la aplicación. Un reverse proxy delante de tu app es el sitio habitual para esto: Nginx o Caddy guarda el certificado, descifra la conexión HTTPS entrante y reenvía la petición al backend por HTTP plano en la red interna. El código de tu aplicación nunca ve un certificado.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
server {
    listen 443 ssl;
    server_name example.com;

    ssl_certificate     /etc/letsencrypt/live/example.com/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/example.com/privkey.pem;

    location / {
        proxy_pass http://127.0.0.1:8080;
    }
}

fullchain.pem es el certificado hoja con el intermedio ya añadido, el archivo con la cadena de confianza listo para servir tal cual. Usa este en lugar de cert.pem, que es solo la hoja y reintroduce el fallo de intermedio que falta visto antes.

Una CDN hace el mismo trabajo un salto más allá. Termina TLS en el PoP edge más cercano al visitante, y luego abre una segunda conexión TLS de vuelta a tu origen. Dos conexiones, dos handshakes, y el certificado que valida el navegador del visitante es el certificado edge de la CDN. Tu certificado de origen solo tiene que convencer a la CDN.

Esa división decide dónde depurar. Si curl directo contra el origen muestra un certificado válido pero el dominio público no, el problema está en la capa del proxy o de la CDN y ninguna cantidad de logs de aplicación lo va a encontrar.

Qué añade HSTS por encima de TLS

TLS asegura una conexión una vez que es HTTPS. No hace nada con la primera petición, que todavía puede salir por http:// plano cuando un usuario teclea un dominio a secas o sigue un enlace antiguo, y esa primera petición es exactamente donde un atacante en la ruta puede interceptar y forzar el downgrade. Strict-Transport-Security cierra ese hueco: tras una primera visita HTTPS con éxito, el navegador reescribe cada petición posterior a ese dominio a HTTPS antes de que salga de la máquina.

1
Strict-Transport-Security: max-age=63072000; includeSubDomains; preload

max-age va en segundos, y 63072000 son dos años, cómodamente por encima del mínimo de un año que exige el registro en la preload list. includeSubDomains extiende la política a todos los subdominios. preload marca el dominio como elegible para la lista integrada en Chrome, Firefox y Safari, cuyas entradas obtienen la reescritura solo-HTTPS desde la primerísima petición, sin necesidad de una visita previa.

Añade preload solo cuando cada subdominio sirva HTTPS de verdad. Salir de la preload list tarda meses en propagarse por las versiones de los navegadores, y hasta que lo hace, cualquier subdominio que no pueda usar HTTPS queda inalcanzable.

Cuándo TLS no basta, y qué revisar primero

TLS verifica el servidor ante el cliente. No dice nada sobre lo que ese servidor hace con tu petición después, y no impide que un usuario víctima de phishing teclee una contraseña en un dominio falso convincente que tiene un certificado perfectamente válido. Let’s Encrypt emite para examp1e.com con la misma facilidad que para example.com. Un candado significa que la conexión es privada, no que el otro extremo sea honesto.

Tres fallos explican la mayoría de los avisos de “el sitio está caído” que resultan ser una mala configuración de TLS: un certificado caducado, un intermedio que falta y una lista SAN que no cubre el hostname solicitado. Los tres son visibles en la salida de openssl s_client de arriba, antes de abrir un solo log de aplicación.

Si tú mismo terminas TLS, mete ese comando -checkend hoy en una tarea cron o en una alerta de monitorización. El certificado que tumba un sitio es el que nadie recordaba que existía.

Artículos relacionados