// Engineering Log

HTTP, HTTPS y TLS: Parte 10 — Depuración de TLS: curl, openssl s_client y análisis de errores de certificados

Publicado el 16.10.2026

// Ruta rapida

Este articulo pertenece al tema Seguridad y proteccion.

La mayoría de los problemas con HTTPS se reducen a varias causas: cadena de certificados incompleta, certificado caducado, nombre incorrecto en el certificado, incompatibilidad de versiones o cifrados, intervención de equipos intermedios. El navegador normalmente muestra un genérico «Conexión no segura», y la causa exacta la proporcionan dos herramientas: curl y openssl s_client. En esta parte — cómo usarlas y cómo leer su salida.

Qué muestra curl -v

bash
curl -sv -o /dev/null https://example.ru/ 2>&1 | grep '^\*'
* Host example.ru:443 was resolved.
*   Trying 192.0.2.10:443...
* Connected to example.ru (192.0.2.10) port 443
* ALPN: curl offers h2,http/1.1
* TLSv1.3 (OUT), TLS handshake, Client hello (1):
* TLSv1.3 (IN), TLS handshake, Server hello (2):
* TLSv1.3 (IN), TLS handshake, Certificate (11):
* SSL connection using TLSv1.3 / TLS_AES_256_GCM_SHA384 / X25519 / RSASSA-PSS
* ALPN: server accepted h2
* Server certificate:
*  subject: CN=example.ru
*  start date: Sep  1 00:00:00 2026 GMT
*  expire date: Nov 30 23:59:59 2026 GMT
*  subjectAltName: host "example.ru" matched cert's "example.ru"
*  issuer: C=US; O=Let's Encrypt; CN=R11
*  SSL certificate verify ok.

De la salida se ve: a qué IP se conectó, qué versión de TLS y qué cifrado se negociaron, qué protocolo se seleccionó vía ALPN, a quién y por quién fue emitido el certificado, hasta qué fecha es válido y si el nombre coincide. El formato de la salida varía un poco según la biblioteca TLS con la que se compiló curl (OpenSSL, LibreSSL, GnuTLS, Schannel en Windows): curl --version la muestra en la primera línea.

Opciones de curl para TLS

bash
curl --tlsv1.2 --tls-max 1.2 https://example.ru/   # solo TLS 1.2
curl --tlsv1.3 https://example.ru/                  # no inferior a TLS 1.3
curl --ciphers ECDHE-RSA-AES128-GCM-SHA256 --tls-max 1.2 https://example.ru/   # cifrado específico TLS 1.2
curl --cacert internal-ca.crt https://intranet.example.ru/   # confiar en tu CA
curl --cert client.crt --key client.key https://admin.example.ru/   # certificado de cliente (mTLS)
curl -k https://example.ru/                         # no verificar el certificado — solo para diagnóstico

La opción --tlsv1.2 significa “no inferior a 1.2”, y no “exactamente 1.2”. Para comprobar que el servidor soporta exactamente 1.2, hace falta también --tls-max 1.2.

-k (--insecure) es útil para separar el problema del certificado del resto: si con -k todo funciona, el problema está en el certificado. No se debe dejar -k en scripts.

openssl s_client

s_client — cliente TLS de OpenSSL que realiza el handshake y muestra todo lo que envía el servidor.

bash
openssl s_client -connect example.ru:443 -servername example.ru </dev/null
  • -servername establece el SNI. En OpenSSL 1.1.1 y posteriores el SNI se toma de -connect automáticamente si allí hay un nombre y no una IP. Si te conectas por IP, especifica -servername explícitamente, de lo contrario obtendrás el certificado por defecto;
  • </dev/null cierra la entrada para que el comando termine inmediatamente después del handshake.

Lo principal en la salida:

Certificate chain
 0 s:CN=example.ru
   i:C=US, O=Let's Encrypt, CN=R11
 1 s:C=US, O=Let's Encrypt, CN=R11
   i:C=US, O=Internet Security Research Group, CN=ISRG Root X1
---
New, TLSv1.3, Cipher is TLS_AES_256_GCM_SHA384
...
Verify return code: 0 (ok)
  • Certificate chain — lo que envió el servidor: s — a quién fue emitido, i — por quién. En cada siguiente s debe coincidir con el i del anterior. Si en la cadena solo hay la línea 0, el servidor no está entregando el certificado intermedio;
  • Verify return code: 0 (ok) — la cadena se verificó. Cualquier otro valor indica un error con explicación.

Salida breve y casos particulares:

bash
openssl s_client -connect example.ru:443 -brief </dev/null          # solo versión, cifrado, verificación
openssl s_client -connect example.ru:443 -showcerts </dev/null      # todos los certificados de la cadena en PEM
openssl s_client -connect example.ru:443 -tls1_2 </dev/null         # solo TLS 1.2
openssl s_client -connect example.ru:443 -alpn h2 </dev/null        # comprobar ALPN
openssl s_client -connect example.ru:443 -status </dev/null         # respuesta OCSP, si el servidor la adjunta
openssl s_client -connect mail.example.ru:587 -starttls smtp </dev/null   # TLS en protocolos de correo

-starttls funciona para smtp, imap, pop3, ftp, xmpp, postgres, mysql y otros protocolos donde TLS se activa con un comando dentro de la conexión abierta.

Información sobre el certificado

Vigencia, nombres y emisor del certificado en el servidor:

bash
openssl s_client -connect example.ru:443 -servername example.ru </dev/null 2>/dev/null \
  | openssl x509 -noout -subject -issuer -dates -ext subjectAltName
subject=CN=example.ru
issuer=C=US, O=Let's Encrypt, CN=R11
notBefore=Sep  1 00:00:00 2026 GMT
notAfter=Nov 30 23:59:59 2026 GMT
X509v3 Subject Alternative Name:
    DNS:example.ru, DNS:www.example.ru

La opción -ext está en OpenSSL 1.1.1 y posteriores. Para un archivo de certificado en disco — openssl x509 -in cert.pem -noout -text.

Comprobar si el certificado expirará en los próximos 14 días (código de retorno 1 — expirará):

bash
openssl s_client -connect example.ru:443 -servername example.ru </dev/null 2>/dev/null \
  | openssl x509 -noout -checkend $((14*24*3600)) && echo ok || echo 'истекает'

Para control permanente es mejor el monitoreo: Prometheus blackbox_exporter expone la métrica probe_ssl_earliest_cert_expiry — la fecha de expiración del certificado más próximo en la cadena, y con ella se configura la alerta.

Verificar que la clave corresponde al certificado (los hashes deben coincidir):

bash
openssl x509 -in cert.pem -noout -pubkey | openssl sha256
openssl pkey -in privkey.pem -pubout | openssl sha256

Verificar la cadena desde archivos:

bash
openssl verify -untrusted chain.pem cert.pem

Qué versiones y cifrados soporta el servidor

Manualmente — probando con -tls1, -tls1_1, -tls1_2, -tls1_3 en s_client. Más cómodo con herramientas listas:

bash
nmap --script ssl-enum-ciphers -p 443 example.ru
testssl.sh example.ru

testssl.sh comprueba versiones, cifrados, cadena, HSTS, vulnerabilidades conocidas y funciona sin instalación, en un único script. Para sitios públicos lo mismo ofrece SSL Labs.

Descifrar el tráfico en Wireshark

Cómo capturar tráfico con tcpdump y trabajar con Wireshark está descrito en el artículo «Solución de problemas de red: tcpdump, sngrep y Wireshark».

A veces hace falta ver el propio HTTP dentro de TLS: qué envía exactamente la aplicación, qué cabeceras van. Para eso el cliente escribe las claves de sesión en un archivo, y Wireshark las usa para descifrar el tráfico.

bash
export SSLKEYLOGFILE=$HOME/tls-keys.log
curl https://example.ru/            # curl compilado con OpenSSL, BoringSSL o GnuTLS
# o inicia Chrome/Firefox desde esta misma shell

En Wireshark: Settings → Protocols → TLS → (Pre)-Master-Secret log filename — especifica el archivo. El tráfico grabado mientras las claves están en ese archivo aparecerá descifrado, incluyendo HTTP/2.

El archivo de claves permite descifrar toda la conversación registrada — trátalo como una contraseña y elimínalo después de depurar. El curl del sistema en macOS está compilado con LibreSSL y puede no soportar SSLKEYLOGFILE; usa el curl de Homebrew.

Errores frecuentes y qué significan

unable to get local issuer certificate (curl, código 60; en s_client — verify error:num=20). El cliente no puede construir la cadena hasta una raíz de confianza. Causas:

  • el servidor no entrega el certificado intermedio — corrígelo en el servidor: usa fullchain.pem en lugar de cert.pem;
  • el certificado fue emitido por una CA interna que no está en el cliente — usa --cacert o instala la raíz en el sistema;
  • en el sistema el almacén de certificados raíz está obsoleto — actualiza el paquete ca-certificates.

certificate has expired (código 10). Comprueba las fechas. Si el certificado en el servidor es reciente pero el cliente ve uno viejo — el servidor web no recargó el certificado después de renovarlo (nginx -s reload) o la respuesta viene de otro nodo del balanceador.

no alternative certificate subject name matches target host name (curl, código 60). El nombre no está en el SAN. Causas comunes: acceder con www. cuando el certificado es solo para el dominio base; conexión sin SNI o por IP; certificado por defecto de otro sitio en ese servidor.

self-signed certificate / self-signed certificate in certificate chain (códigos 18 y 19). Certificado autofirmado, o la cadena termina en una raíz en la que el cliente no confía. Si en el emisor aparece el nombre de un antivirus o de un gateway corporativo, el tráfico se está descifrando en el trayecto.

tlsv1 alert protocol version, unsupported protocol (curl, código 35). No hay versión TLS común: cliente antiguo (solo TLS 1.0) y servidor moderno, o al revés. Prueba con --tlsv1.2 --tls-max 1.2 y con -tls1_2 en s_client.

sslv3 alert handshake failure (código 35). No hay cifrado o grupo común, o el servidor requiere certificado de cliente. En la salida de s_client con -state se ve en qué paso falló el handshake.

Connection reset by peer justo después del ClientHello. El servidor o un equipo intermedio cerró la conexión al ver el ClientHello. Si por IP (sin SNI) la conexión pasa, y por nombre no, es un signo de filtrado por SNI en el camino. Ocurre también en firewalls corporativos que no aceptan un ClientHello grande con la agrupación postcuántica X25519MLKEM768: prueba con -groups x25519 en s_client o --curves x25519 en curl.

SSL routines::wrong version number en s_client. Te conectaste por TLS a un puerto donde no hay TLS: por ejemplo al puerto 80, o al 587 donde hace falta -starttls smtp, o el servidor en 443 responde HTTP normal.

El navegador abre, pero curl o la aplicación no. Casi siempre es cadena incompleta: los navegadores reconstruyen certificados intermedios por su cuenta, las bibliotecas no. Comprobación: openssl s_client -showcerts y la línea Certificate chain.

Orden de diagnóstico

  1. curl -sv -o /dev/null https://nombre/ — ¿se llega al TLS, en qué paso está el error, código de error de curl?
  2. openssl s_client -connect nombre:443 -servername nombre — cadena y Verify return code.
  3. openssl x509 -noout -dates -ext subjectAltName — vigencias y nombres.
  4. curl --resolve nombre:443:IP — ¿se comportan igual todos los servidores detrás del balanceador?
  5. testssl.sh nombre — versiones, cifrados y comprobación completa de la configuración.
  6. Si nada ayuda — captura de tráfico y Wireshark con SSLKEYLOGFILE.

// Tarea parecida

Si estas resolviendo algo parecido

Este articulo pertenece a uno de los temas principales de trabajo. Puedes seguir leyendo sobre el tema, ir a la pagina principal para entender a que me dedico o abrir directamente los servicios.

Tema del articulo

Seguridad y proteccion

SSL, hardening, accesos, proteccion de servicios y configuraciones seguras.

Tareas frecuentes de esta tema

  • Configurar SSL, certificados y conexiones seguras
  • Restringir accesos y cerrar puntos de entrada innecesarios
  • Reforzar la configuracion del servidor y los servicios

// Siguiente paso

Si necesitas ayuda con este tema y no solo otro articulo, es mejor ir directo a la pagina del servicio. La pagina principal y la seleccion de materiales quedan como rutas secundarias.

Abrir servicios

// Contact

¿Necesitas ayuda?

Escríbeme y te ayudaré a resolver el problema

Respondo en un día laborable (03:00-13:00 GMT)

Или оставьте заявку здесь:

Confirme que no es un bot.

Escribir y recibir una respuesta rápida