Reanudación de sesiones TLS y HTTP/3 en el perímetro
La reanudación de sesiones permite a los visitantes recurrentes omitir la negociación TLS completa. Por qué las claves de tickets deben conservarse tras los reinicios, cómo las rotamos de forma segura y cómo HTTP/3 reutiliza el mismo flujo de procesamiento de solicitudes en el perímetro.
Para una CDN, la parte costosa a menudo no es transferir bytes, sino establecer conexiones. Una negociación TLS completa requiere criptografía asimétrica y viajes de ida y vuelta adicionales por la red; para los visitantes lejanos, esas decenas o cientos de milisegundos se reflejan directamente en el tiempo hasta el primer byte. Este artículo aborda dos trabajos relacionados con la negociación en nuestra implementación perimetral en Rust: la reanudación de sesiones TLS y HTTP/3.
Qué es la reanudación de sesiones
Cuando un navegador se conecta por primera vez a un sitio, completa una negociación y acuerda las claves de sesión. Si necesita otra conexión poco después (una nueva pestaña, una conexión renovada o un cambio entre redes móviles), no hay motivo para repetir el costoso intercambio de claves. TLS dispone de mecanismos de reanudación precisamente para esto:
- TLS 1.2 tiene dos: los identificadores de sesión (el servidor mantiene el estado de la sesión en memoria) y los tickets de sesión (el servidor cifra el estado de la sesión y lo entrega al cliente, que lo devuelve la próxima vez).
- TLS 1.3 unifica estos mecanismos en la reanudación mediante PSK (clave precompartida): después de la negociación, el servidor envía un
NewSessionTickety el cliente lo presenta en su siguiente conexión.
En ambos casos, los tickets se cifran con una clave de tickets del lado del servidor. Mientras el servidor pueda descifrar el ticket que devuelve un cliente, se omite la negociación completa. Para los visitantes, esto significa menor latencia; para la infraestructura perimetral, menos CPU dedicada a la criptografía.
Un problema fácil de pasar por alto: cada reinicio invalidaba todos los tickets
En una versión inicial de la implementación perimetral en Rust observamos algo extraño: la reanudación funcionaba con mucha menos frecuencia de lo esperado y su tasa de éxito caía después de cada despliegue. La causa era sencilla, pero fácil de pasar por alto: nunca habíamos configurado una clave de tickets en la capa TLS.
Sin una configuración explícita, BoringSSL y OpenSSL generan una clave de tickets aleatoria al iniciar el proceso. Eso funciona para un único proceso de larga duración, pero los procesos perimetrales se reinician con los despliegues y las actualizaciones. Cada reinicio asignaba una clave nueva al proceso nuevo, todos los tickets de los clientes quedaban inutilizados de inmediato y los clientes volvían a realizar negociaciones completas. Ya existía una implementación de un anillo de claves en el código, pero nunca se había conectado al componente que acepta conexiones TLS.
La solución: persistir, rotar y limitar
Ahora tratamos las claves de tickets como un estado que requiere una gestión cuidadosa, en lugar de un subproducto desechable del proceso:
- Persistir en disco. Al iniciar, el servicio perimetral carga el anillo de claves desde el directorio de certificados del nodo; si el archivo no existe, genera claves nuevas con un CSPRNG. Las escrituras se hacen en un archivo temporal y luego se realiza un cambio de nombre atómico. El archivo se crea con permisos
0600desde el principio, por lo que no hay un intervalo en el que pueda leerse sin autorización. - Conectar una función de retorno real. Añadimos una función de retorno para las claves de tickets a los enlaces TLS que incluimos en nuestro código (
SSL_CTX_set_tlsext_ticket_key_cbde BoringSSL). Los tickets nuevos se cifran con la clave actual; al descifrarlos, la clave se busca en el anillo por el nombre de clave incluido en el ticket. - Rotar según un calendario. De forma predeterminada, las claves rotan cada 6 horas. El anillo conserva la clave actual y algunas anteriores, que solo se usan para descifrar, nunca para emitir tickets. Los tickets recién emitidos siguen funcionando, mientras que la exposición de cada clave queda limitada en el tiempo, lo que reduce el impacto sobre el secreto hacia adelante si una clave se filtra.
- Seguir atendiendo ante fallos. Si no se puede leer el archivo de claves, el servicio perimetral genera claves temporales y sigue atendiendo solicitudes; los tickets de esa ejecución simplemente no se conservarán tras el siguiente reinicio. La reanudación es una optimización y nunca debe ser un motivo para dejar de prestar servicio.
Por separado, la caché de sesiones del servidor (utilizada para los identificadores de sesión TLS 1.2) es una LRU acotada con un límite predeterminado de 20,480 entradas. Cuando se llena, se eliminan las sesiones más antiguas, por lo que el uso de memoria no puede crecer sin límite.
Vale la pena explicar cómo lo verificamos. En las pruebas, simulamos un reinicio creando un SSL_CTX completamente nuevo a partir del mismo archivo de claves; luego nos conectamos con un ticket emitido por el contexto anterior y comprobamos session_reused == true. En un nodo real, capturamos un ticket, reiniciamos el servicio perimetral, volvemos a conectarnos con el ticket anterior y confirmamos que la sesión se reanuda. Un detalle que puede causar confusión: envíe una solicitud real y espere un momento. En TLS 1.3, el NewSessionTicket llega después de la negociación; si se desconecta demasiado pronto, nunca recibirá un ticket y cada conexión parecerá nueva.
Una nota sobre el alcance: actualmente, las claves de tickets son propias de cada nodo. Si la siguiente conexión de un visitante llega a otro nodo, se realiza una negociación completa. Es una decisión deliberada; distribuir las claves entre nodos ampliaría su exposición.
Otra corrección durante el proceso: reinicios ordenados
Durante la investigación también descubrimos que los reinicios del servicio perimetral no eran ordenados. El período predeterminado de apagado ordenado de Pingora tras SIGTERM es largo, y el tiempo de espera de detención de systemd era mucho menor, por lo que cada reinicio terminaba con un SIGKILL. Redujimos el período de gracia y el tiempo de espera de apagado a unos segundos, y ahora los reinicios se completan de forma ordenada en segundos. Junto con las claves de tickets persistentes, esto hace que los despliegues sean casi imperceptibles para los visitantes: las conexiones en curso terminan sin interrupciones y los tickets de los visitantes recurrentes siguen siendo válidos.
HTTP/3: el mismo flujo de procesamiento, otro transporte
HTTP/3 funciona sobre QUIC, que a su vez funciona sobre UDP e integra TLS 1.3. Sus ventajas se aprecian sobre todo en redes poco fiables: evita el bloqueo de cabecera de línea a nivel de TCP, reduce los viajes de ida y vuelta necesarios para establecer una conexión y permite migrar las conexiones cuando cambia la red.
El soporte de HTTP/3 del propio Pingora aún no se ha incorporado al proyecto original. No queríamos que la hoja de ruta de nuestro plano de datos dependiera del ciclo de lanzamientos de ese proyecto, así que implementamos la terminación de QUIC y HTTP/3 sobre tokio-quiche de Cloudflare, que se basa en quiche.
La decisión clave de diseño: HTTP/3 se encarga solo del transporte; toda la lógica de solicitudes reutiliza el flujo de procesamiento existente. En concreto:
- tokio-quiche termina QUIC y HTTP/3 en UDP 443 y selecciona un certificado mediante SNI durante la negociación. Los certificados proceden exactamente del mismo almacén que los de TCP 443; no hay un segundo sistema de certificados.
- Las solicitudes HTTP/3 decodificadas se transfieren a Pingora mediante una conexión local de bucle de retorno dentro del mismo proceso, por lo que se reutilizan el enrutamiento, la caché, el WAF, las reglas y las consultas al origen. No hubo que reescribir ningún módulo de funciones para HTTP/3.
- Las solicitudes de bucle de retorno incluyen un secreto aleatorio que solo existe en la memoria del proceso y solo se consideran fiables cuando realmente proceden del bucle de retorno. Así se restablecen la IP y el protocolo reales del cliente. Los encabezados con los mismos nombres que llegan desde el exterior siempre se eliminan.
- H1/H2 y H3 comparten una única instancia de BoringSSL en un mismo proceso, lo que también nos permite calcular las huellas de los clientes durante la negociación QUIC.
No anuncie lo que no puede servir: condiciones para Alt-Svc
Los navegadores detectan que un sitio admite HTTP/3 a partir del encabezado de respuesta Alt-Svc: h3=":443"; ma=86400. Si un servidor envía ese encabezado sin escuchar en UDP, los navegadores desperdician un intento antes de recurrir a HTTP/2. Por eso, el anuncio Alt-Svc está condicionado tanto a nivel de nodo como de dominio: solo se envía cuando el nodo tiene un servicio QUIC escuchando y el dominio tiene HTTP/3 habilitado. También eliminamos cualquier encabezado Alt-Svc que devuelva el origen, para que su propio anuncio no confunda a los clientes.
Actualmente no habilitamos 0-RTT. Ahorra otro viaje de ida y vuelta, pero los datos anticipados pueden reproducirse, y habilitarlo de forma segura exige decidir qué solicitudes son idempotentes. Lo reconsideraremos cuando tengamos suficiente certeza al respecto.
Resumen
Ni la reanudación de sesiones ni HTTP/3 son funciones que baste con activar. La reanudación depende de gestionar el ciclo de vida de las claves de tickets; HTTP/3 necesita una pila de transporte independiente sin dividir la lógica de solicitudes. Nuestro enfoque: tratar las claves como un estado que se conserva y rota, tratar HTTP/3 como una capa de entrada dedicada al transporte e implementar cada función una sola vez.
Cree en Yeap Cloud
Planes gratuitos de DNS y CDN para cada cuenta nueva.
Seguir leyendo
Guía práctica de purga de caché: de las claves a la purga por variantes
¿Purgó la caché pero sigue obteniendo aciertos? Casi siempre la respuesta está en la clave de caché. Cómo Yeap CDN construye las claves, cómo funcionan los modos de cadena de consulta y qué elimina una purga por URL con y sin cadena de consulta.
La nueva infraestructura perimetral de Yeap CDN: por qué elegimos Rust
Reemplazamos nuestra infraestructura perimetral de CDN por una versión propia desarrollada en Rust sobre Pingora. Esto supone para su sitio conexiones más rápidas, configuración que se aplica en segundos y mayor aislamiento y seguridad.
Cómo elige nuestro DNS los servidores de nombres de su zona
Cada zona alojada en Yeap DNS recibe un par de servidores de nombres. Ese par debe ser estable, estar distribuido y poder responder realmente. Cómo funciona nuestra asignación basada en hash y por qué «el estado es intención; el latido, un hecho».