Hola Habr, me llamo Ilya y trabajo en el equipo de plataforma de la empresa Exness. Estamos desarrollando e implementando componentes de infraestructura básicos que utilizan nuestros equipos de desarrollo de productos.
En este artículo, me gustaría compartir la experiencia de implementar la tecnología de SNI cifrado (ESNI) en la infraestructura de sitios web públicos.

El uso de esta tecnología permitirá aumentar el nivel de seguridad al trabajar con un sitio web público y cumplir con los estándares internos de seguridad adoptados en la Compañía.
Primero que nada, quiero señalar que la tecnología no está estandarizada y aún se encuentra en borrador, sin embargo, CloudFlare y Mozilla ya la admiten (en ). Esto nos motivó a realizar tal experimento.
Un poco de teoría
ESNI es una extensión del protocolo TLS 1.3 que permite cifrar el SNI en el mensaje 'Client Hello' del apretón de manos TLS. Así es como se ve el Client Hello con soporte para ESNI (en lugar del SNI habitual, vemos ESNI):

Para utilizar ESNI, se requieren tres componentes:
- DNS;
- Soporte por parte del cliente;
- Soporte por parte del servidor.
DNS
Es necesario añadir dos registros DNS - Acomo TXT (el registro TXT contiene la clave pública con la que el cliente puede cifrar el SNI) - véase más abajo. Además, debe haber soporte para DoH (DNS sobre HTTPS), ya que los clientes disponibles (véase más abajo) no activan el soporte para ESNI sin DoH. Esto tiene sentido, dado que ESNI implica cifrar el nombre del recurso al que estamos accediendo, es decir, es inútil consultar a DNS por UDP. Además, el uso de ayuda a protegerse contra ataques de 'cache poisoning' en este escenario.
Actualmente hay disponibles , entre ellos:
CloudFlare (Check My Browser → Encrypted SNI → Learn More), que sus servidores ya soportan ESNI, es decir, para los servidores de CloudFlare en DNS tenemos al menos dos registros - A y TXT. En el ejemplo a continuación, solicitamos Google DNS (sobre HTTPS):
A registro:
curl 'https://dns.google.com/resolve?name=www.cloudflare.com&type=A'
-s -H 'accept: application/dns+json'
{
"Status": 0,
"TC": false,
"RD": true,
"RA": true,
"AD": true,
"CD": false,
"Question": [
{
"name": "www.cloudflare.com.",
"type": 1
}
],
"Answer": [
{
"name": "www.cloudflare.com.",
"type": 1,
"TTL": 257,
"data": "104.17.210.9"
},
{
"name": "www.cloudflare.com.",
"type": 1,
"TTL": 257,
"data": "104.17.209.9"
}
]
}
TXT registro, la consulta se forma según el patrón _esni.FQDN:
curl 'https://dns.google.com/resolve?name=_esni.www.cloudflare.com&type=TXT'
-s -H 'accept: application/dns+json'
{
"Status": 0,
"TC": false,
"RD": true,
"RA": true,
"AD": true,
"CD": false,
"Question": [
{
"name": "_esni.www.cloudflare.com.",
"type": 16
}
],
"Answer": [
{
"name": "_esni.www.cloudflare.com.",
"type": 16,
"TTL": 1799,
"data": ""/wEUgUKlACQAHQAg9SiAYQ9aUseUZr47HYHvF5jkt3aZ5802eAMJPhRz1QgAAhMBAQQAAAAAXtUmAAAAAABe3Q8AAAA=""
}
],
"Comment": "Respuesta de 2400:cb00:2049:1::a29f:209."
}Así que, desde el punto de vista de DNS, debemos utilizar DoH (preferiblemente con DNSSEC) y agregar dos registros.
Soporte del cliente
Si hablamos de navegadores, en este momento . se presenta una guía sobre cómo activar el soporte de ESNI y DoH en Firefox. Después de que el navegador esté configurado, deberíamos ver algo como esto:

para verificar en el navegador.
Por supuesto, para soportar ESNI se debe utilizar TLS 1.3, ya que ESNI es una extensión de TLS 1.3.
Para fines de prueba del backend con soporte de ESNI, implementamos un cliente en go, pero eso será más adelante.
Soporte del servidor
En este momento, ESNI no es soportado por servidores web como nginx/apache, etc., porque funcionan con TLS a través de OpenSSL/BoringSSL, en los que ESNI no es soportado oficialmente.
Por lo tanto, decidimos crear nuestro propio componente front-end (proxy inverso ESNI) que soporte la terminación de TLS 1.3 con ESNI y el proxy de tráfico HTTP(S) a upstream que no soporte ESNI. Esto permite aplicar la tecnología en una infraestructura ya existente, sin cambiar los componentes principales, es decir, utilizar los servidores web actuales que no soportan ESNI.
Para mayor claridad, proporcionamos un esquema:

Cabe mencionar que el proxy fue diseñado con la posibilidad de terminar la conexión TLS sin ESNI, para soportar clientes sin ESNI. Además, el protocolo de comunicación con upstream puede ser tanto HTTP como HTTPS con una versión de TLS inferior a 1.3 (si upstream no soporta 1.3). Este esquema ofrece la máxima flexibilidad.
La implementación del soporte de ESNI en go la tomamos de . Cabe destacar que la implementación en sí es bastante no trivial, ya que implica cambios en la biblioteca estándar crypto/tls y por lo tanto requiere "parcheo" GOROOT antes de la compilación.
Para generar las claves ESNI utilizamos (también una creación de CloudFlare). Estas claves se utilizan para la encriptación/desencriptación de SNI.
Probamos la compilación utilizando go 1.13 en Linux (Debian, Alpine) y MacOS.
Unas palabras sobre características operativas
El proxy inverso ESNI proporciona métricas en formato Prometheus, como rps, latencia hacia el upstream y códigos de respuesta, así como los handshakes TLS fallidos/exitosos y la duración del handshake TLS. A primera vista, esto pareció suficiente para evaluar cómo el proxy maneja el tráfico.
Además, realizamos pruebas de carga antes de usarlo. Los resultados son los siguientes:
wrk -t50 -c1000 -d360s 'https://esni-rev-proxy.npw:443' --timeout 15s
Ejecutando prueba de 6 minutos en https://esni-rev-proxy.npw:443
50 hilos y 1000 conexiones
Estadísticas por hilo Prom. Desv. Máx. +/− Desv.
Latencia 1.77s 1.21s 7.20s 65.43%
Req/Sec 13.78 8.84 140.00 83.70%
206357 solicitudes en 6.00m, 6.08GB leídos
Solicitudes/seg: 573.07
Transferencia/seg: 17.28MB Las pruebas de carga que realizamos fueron puramente cualitativas, para comparar el esquema utilizando el proxy inverso ESNI y sin él. Inyectamos tráfico localmente para eliminar ‘interferencias’ en los componentes intermedios.
Entonces, con el soporte de ESNI y el proxy hacia el upstream con HTTP, obtuvimos alrededor de ~ 550 rps desde una instancia, mientras que el consumo promedio de CPU/RAM del proxy inverso ESNI fue:
- 80% Uso de CPU (4 vCPU, 4 GB RAM en hosts, Linux)
- 130 MB Mem RSS

Para comparación, el RPS para el mismo upstream nginx sin terminación TLS (protocolo HTTP) es ~ 1100:
wrk -t50 -c1000 -d360s 'http://lb.npw:80' --timeout 15s
Ejecutando prueba de 6 minutos en http://lb.npw:80
50 hilos y 1000 conexiones
Estadísticas por hilo Prom. Desv. Máx. +/− Desv.
Latencia 1.11s 2.30s 15.00s 90.94%
Req/Sec 23.25 13.55 282.00 79.25%
393093 solicitudes en 6.00m, 11.35GB leídos
Errores de socket: conectar 0, leer 0, escribir 0, tiempo de espera 9555
Respuestas no 2xx o 3xx: 8111
Solicitudes/seg: 1091.62
Transferencia/seg: 32.27MB La presencia de tiempos de espera indica que hay falta de recursos (utilizamos hosts de 4 vCPU y 4 GB RAM, Linux), y de hecho, el RPS potencial es mayor (obtuvimos cifras de hasta 2700 RPS en recursos más potentes).
En conclusión, debo mencionar que la tecnología ESNI parece bastante prometedora. Aún hay muchas preguntas abiertas, como el almacenamiento de la clave pública ESNI en DNS y la rotación de claves ESNI; estos temas se están discutiendo activamente, y la última versión del borrador (en el momento de la escritura) de ESNI ya Arqueólogos de la era digital .
Fuente: habr.com
