Problema

Varias aplicaciones iOS que consumen una API HTTPS alojada en AWS presentan fallos esporádicos al iniciar sesión como invitado. Los dispositivos afectados registran mensajes como “An SSL error has occurred” o “A TLS error caused the secure connection to fail”. La mayoría de los usuarios pueden conectarse sin problemas; el error aparece solo en una fracción de los clientes y de forma intermitente. La arquitectura típica incluye:

  • Dominio con registro A apuntando a una dirección IPv4 pública de un ELB o de una instancia EC2.
  • Certificado TLS válido (por ejemplo, ACM o Let’s Encrypt) sin registro AAAA.
  • Cliente iOS que usa URLSession para realizar la petición.

El desafío es determinar si el fallo ocurre antes de que el tráfico alcance AWS (por ejemplo, en la ruta del ISP, resolución DNS o traducción NAT64) o si la conexión llega a AWS y se rompe durante el handshake o en la capa de aplicación (Nginx, WAF, etc.).

Causa

Los errores intermitentes de SSL/TLS en este contexto suelen originarse en uno de los siguientes grupos:

  1. Ruta IPv6/NAT64 inesperada

    • Dispositivos iOS con conectividad IPv6‑only (por ejemplo, redes móviles que usan NAT64/DNS64) intentan resolver el dominio a una dirección IPv6 sintética. Al no existir registro AAAA, el resolutor devuelve un prefijo NAT64 y el cliente intenta conectar a una dirección IPv6 que el balanceador de carga no soporta, provocando un handshake fallido.
  2. Problemas de DNS y caché

    • Propagación incompleta de cambios en el registro A o TTL demasiado bajo pueden generar respuestas inconsistentes entre resolutores. Algunas resoluciones pueden devolver una IP antigua que ya no tiene el certificado correcto o que está fuera de servicio.
  3. Restricciones de ISP o carrier

    • Algunos proveedores interceptan tráfico TLS para inspección o aplican listas de bloqueo basadas en reputación de IP. Si la IP del ELB está en una lista negra temporal, solo ciertos usuarios verán el error.
  4. Configuración de la cadena de certificados

    • Un certificado intermedio faltante o una cadena incompleta funciona con algunos clientes (que usan caché de certificados) pero falla con otros que requieren la cadena completa. La intermitencia aparece cuando la caché se expira.
  5. Timeouts y SNI incompatibles

    • URLSession envía SNI automáticamente; sin embargo, si el cliente usa una versión antigua de iOS o una configuración de red que modifica el MTU, el paquete SYN puede fragmentarse y el servidor no responder al handshake.
  6. Políticas de seguridad en el ELB/Nginx

    • Reglas de seguridad que limitan rangos de IP o que aplican tls_version restrictivo pueden rechazar conexiones que provienen de ciertas regiones o que usan ciphers menos comunes.

Solución

Abordar el problema paso a paso permite aislar la capa responsable y aplicar la corrección adecuada.

1. Verificar la exposición IPv6

  1. Consulta DNS con dig

    dig +short A ejemplo.com
    dig +short AAAA ejemplo.com
    

    Si solo hay registro A, confirma que no hay IPv6 directa.

  2. Simular NAT64
    Usa un resolutor DNS64 (por ejemplo, dns64.cloudflare-dns.com) y verifica la dirección IPv6 generada:

    dig @1.1.1.1 AAAA ejemplo.com +dnssec +short
    

    Si se devuelve una dirección IPv6 bajo el prefijo 64:ff9b::/96, los dispositivos IPv6‑only intentarán esa ruta.

  3. Añadir registro AAAA o desactivar NAT64

    • Opción A: Publicar un registro AAAA que apunte a un ALB habilitado para IPv6.
    • Opción B: En la configuración del ELB, habilitar IPv6 y asociar el mismo certificado.
    • Opción C: Si no es posible, forzar a los clientes a usar IPv4 mediante URLSessionConfiguration:
    let config = URLSessionConfiguration.default
    config.connectionProxyDictionary = ["HTTPEnable": 0, "HTTPSEnable": 0]
    config.preferredInterface = .ipv4
    

2. Auditar la cadena de certificados

  1. Descargar la cadena completa

    openssl s_client -connect ejemplo.com:443 -servername ejemplo.com -showcerts </dev/null
    

    Verifica que el certificado intermedio aparezca después del certificado del servidor.

  2. Instalar la cadena completa en el ELB/Nginx

    • En ACM, importa el certificado con su cadena intermedia.
    • En Nginx, usa ssl_certificate con el archivo que incluya fullchain.pem.
  3. Probar con clientes diferentes
    Ejecuta la misma petición desde macOS, Linux y Android para confirmar que la cadena es aceptada universalmente.

3. Detectar bloqueos de ISP

  1. Traceroute a la IP del ELB

    traceroute -n <IP_ELB>
    

    Busca saltos que terminen en “* * *” o que indiquen routers de ISP conocidos.

  2. Consultar listas de reputación
    Usa herramientas como spamhaus.org o talosintelligence.com para verificar si la IP está listada.

  3. Implementar fallback DNS
    Configura un registro CNAME que apunte a un CDN (por ejemplo, CloudFront) que tenga múltiples rangos de IP. Esto reduce la exposición a una única IP.

4. Ajustar políticas TLS en el balanceador

  1. Permitir versiones TLS 1.2 y 1.3
    En el ELB, selecciona un security policy que incluya ambos. Evita políticas que excluyan ciphers antiguos si esperas dispositivos iOS < 12.

  2. Desactivar SSL renegotiation si está habilitado en Nginx; algunos clientes iOS la rechazan.

5. Instrumentar logs y métricas

  1. Activar CloudWatch Access Logs en el ELB para registrar cada intento de conexión, incluyendo el código de error TLS (ELB-5xx, ELB-4xx).
  2. Habilitar Nginx error log con nivel info para capturar SSL: handshake failed.
  3. Añadir client_hello logging con ssl_handshake_timeout para ver si el cliente nunca envía SNI.

6. Pruebas de campo

  1. Distribuir una versión de la app con URLSessionConfiguration forzada a IPv4 a un grupo de usuarios que reportan el error.
  2. Recopilar métricas de éxito mediante un endpoint de telemetría. Si la tasa de éxito sube, la causa estaba en la ruta IPv6/NAT64.

Cuándo aplicar esta solución

  • Síntomas: errores “SSL error” o “TLS handshake failed” que aparecen solo en algunos usuarios, sin patrón de versión de iOS o modelo de dispositivo.
  • Indicadores de red: presencia de NAT64 en la red del cliente (Wi‑Fi corporativo, red móvil con IPv6‑only).
  • No aplicar: si todos los usuarios fallan consistentemente o si el certificado está revocado; en esos casos el problema es a nivel de infraestructura del servidor.

Código

# Verificar cadena de certificados
openssl s_client -connect ejemplo.com:443 -servername ejemplo.com -showcerts </dev/null | \
awk '/BEGIN CERTIFICATE/,/END CERTIFICATE/ {print}' > chain.pem

# Probar handshake desde un contenedor Docker con IPv6 habilitado
docker run --rm -it --network=host alpine sh -c "\
apk add --no-cache openssl && \
openssl s_client -connect ejemplo.com:443 -servername ejemplo.com -tls1_2"

Verificación

  1. Repetir la petición desde un dispositivo iOS después de aplicar los cambios.
  2. Revisar CloudWatch: la métrica ELB 5XX debe disminuir a cero para el dominio.
  3. Confirmar en los logs de Nginx que ya no aparecen líneas SSL: error:1408A0C1:SSL routines:ssl3_get_client_hello:no shared cipher.
  4. Ejecutar pruebas de velocidad con curl -v https://ejemplo.com/api/guest-login desde diferentes redes (Wi‑Fi, 4G, 5G). La salida debe mostrar SSL connection using TLSv1.3 sin errores.

Notas adicionales

  • Mantener el TTL de los registros DNS en al menos 300 s durante cambios de infraestructura evita respuestas inconsistentes.
  • Si decides publicar un registro AAAA, verifica que el certificado incluya Subject Alternative Name con el mismo dominio; de lo contrario, los clientes iOS rechazarán la conexión.
  • En entornos con alta variabilidad de ISP, considera usar un CDN frente al ELB para distribuir la carga y mitigar bloqueos de IP.
  • Los dispositivos iOS con versiones < 11 pueden requerir ciphers RSA‑SHA1; evalúa si es necesario mantener compatibilidad o forzar una actualización de la app.