Errores de certificado SSL: cómo solucionarlos

El cliente no pudo confiar en el certificado del servidor: venció, no cubre el dominio, tiene la cadena incompleta o viene de un emisor desconocido. Ejecuta openssl s_client para ver cuál es.

Actualizado el 5 min de lectura

Objetivos de aprendizaje

Después de leer este artículo podrás:

  • Identificar la causa por el mensaje de error del navegador, curl, Node.js o Python
  • Diagnosticar certificado vencido, dominio incorrecto y cadena incompleta con openssl
  • Corregir el problema sin desactivar la verificación del certificado
En esta página

Abres tu sitio y el navegador muestra una pantalla roja o el aviso "No seguro". O tu bot, tu API o un curl falla con certificate has expired. Todos estos casos tienen la misma raíz: al establecer la conexión TLS, el cliente recibió un certificado y no pudo confiar en él.

La buena noticia es que las causas son pocas y openssl encuentra la correcta en un minuto. Esta guía va del mensaje de error a la corrección sin pedirte que apagues la verificación. Si quieres entender qué hace el certificado, mira qué es SSL/TLS.

Mensaje de errornavegador, curl o Node.jsrevisaFechanotAftercorrigerenueva yrecargaDominiosubjectAltNameemite paracada nombreCadenaintermediossirve elfullchain.pem
Del mensaje de error a la causa: revisa la fecha, el nombre y la cadena, y cada causa tiene su propia corrección.

¿Cómo leer el mensaje de error?#

El mensaje ya señala la causa. Chrome y los navegadores basados en él muestran un código NET::ERR_...:

text
NET::ERR_CERT_DATE_INVALID          # venció, o el reloj del dispositivo está mal
NET::ERR_CERT_COMMON_NAME_INVALID   # el certificado no cubre este dominio
NET::ERR_CERT_AUTHORITY_INVALID     # emisor desconocido, autofirmado o cadena incompleta
ERR_SSL_VERSION_OR_CIPHER_MISMATCH  # handshake: sin versión de TLS o cifrado en común
ERR_SSL_PROTOCOL_ERROR              # handshake: el servidor no habló TLS correctamente

Fuera del navegador, los nombres vienen de OpenSSL. En Node.js, el error trae err.code:

text
CERT_HAS_EXPIRED                    # venció
CERT_NOT_YET_VALID                  # la fecha de inicio está en el futuro
ERR_TLS_CERT_ALTNAME_INVALID        # el nombre del dominio no coincide
UNABLE_TO_VERIFY_LEAF_SIGNATURE     # cadena incompleta
DEPTH_ZERO_SELF_SIGNED_CERT         # autofirmado
SELF_SIGNED_CERT_IN_CHAIN           # autofirmado dentro de la cadena

curl responde con curl: (60) SSL certificate problem: unable to get local issuer certificate, y Python con ssl.SSLCertVerificationError: [SSL: CERTIFICATE_VERIFY_FAILED]. Estos dos son genéricos: dicen que la verificación falló, y la causa está en el texto que sigue o en el diagnóstico de abajo.

¿Cómo diagnosticar con openssl?#

Un comando muestra fechas, emisor y nombres cubiertos:

bash
echo | openssl s_client -connect ejemplo.com:443 -servername ejemplo.com 2>/dev/null \
  | openssl x509 -noout -dates -issuer -subject -ext subjectAltName

Lee la salida así:

  • notAfter en el pasado: el certificado venció.
  • subjectAltName sin tu dominio: el certificado es de otro nombre.
  • issuer igual a subject: autofirmado.

Para ver la cadena y el veredicto de la verificación, ejecuta s_client sin filtros y busca Verify return code. El valor 0 (ok) indica éxito. Cualquier otro trae el motivo, como 20 (unable to get local issuer certificate).

El argumento -servername importa: muchos servidores alojan varios dominios en la misma IP y eligen el certificado por el nombre enviado en la extensión SNI. Sin él, puedes recibir el certificado equivocado y buscar el problema en otro lado.

¿Por qué venció el certificado?#

Es la causa número uno. Los certificados de Let's Encrypt tienen una validez corta, y la renovación automática falla en silencio cuando el timer se desactiva, el puerto 80 pasa a usarlo otro servicio o la credencial de la API de DNS expira. Let's Encrypt viene acortando ese plazo por etapas, así que la renovación manual deja de ser viable.

Corrección:

  1. Ejecuta la renovación y lee el error: sudo certbot renew --dry-run.
  2. Corrige la causa (puerto 80 abierto, DNS apuntando al servidor correcto, credencial renovada).
  3. Recarga el servidor web, porque muchos solo leen el certificado al iniciar: sudo systemctl reload nginx.
  4. Confirma con el comando openssl de arriba.

El proceso se explica en cómo funciona Let's Encrypt. Antes de culpar al servidor, revisa la fecha de tu computadora o celular: un reloj incorrecto produce ERR_CERT_DATE_INVALID incluso con un certificado válido.

¿Qué hacer cuando el dominio no coincide?#

Un certificado vale para una lista de nombres. ejemplo.com no cubre www.ejemplo.com, a menos que ambos estén en la lista, y un certificado *.ejemplo.com cubre api.ejemplo.com, pero no ejemplo.com ni a.b.ejemplo.com. Causas frecuentes:

  • El dominio apunta a un servidor que tiene el certificado de otro sitio.
  • El DNS todavía apunta a la IP anterior (mira propagación de DNS).
  • Emitiste el certificado solo para el dominio raíz y olvidaste el www.

Corrígelo emitiendo el certificado con todos los nombres que usas y comprobando adónde resuelve el dominio (dig +short ejemplo.com). El paso a paso está en cómo apuntar un dominio a tu aplicación.

¿Y si la cadena está incompleta?#

El servidor debe enviar el certificado del sitio más los intermedios. Los navegadores a veces completan la cadena por su cuenta, pero curl, Node.js y Python no, y por eso el sitio abre en Chrome mientras tu API falla con UNABLE_TO_VERIFY_LEAF_SIGNATURE o unable to get local issuer certificate.

La corrección es servir el archivo con la cadena completa. Con Certbot, es fullchain.pem, no cert.pem:

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

Si el error aparece solo en tu máquina, con un certificado de una CA privada (red corporativa, proxy de inspección, entorno interno), confía en esa CA en vez de apagar la verificación:

bash
curl --cacert ./mi-ca.pem https://interno.ejemplo.com
NODE_EXTRA_CA_CERTS=./mi-ca.pem node app.js

En Node.js, rejectUnauthorized es true por defecto, y false elimina toda la protección contra interceptación. Para una CA propia, pásala en la opción ca.

¿Por qué aparece "No seguro" con HTTPS activo?#

Si el certificado está bien y el candado sigue sin aparecer, sospecha de contenido mixto: la página llegó por HTTPS, pero carga algo por http://. Según MDN, los navegadores actualizan imágenes, audio y video a HTTPS por su cuenta, pero bloquean scripts, CSS, iframes y llamadas fetch. El resultado es una página rota o con aviso.

Abre la consola del navegador (F12): cada recurso afectado aparece con su URL. Cambia http:// por https:// o por rutas relativas. Como atajo, el encabezado Content-Security-Policy: upgrade-insecure-requests pide al navegador que actualice todas las solicitudes, pero solo funciona si el otro lado también atiende por HTTPS.

¿Y los errores de handshake?#

Si el error es ERR_SSL_VERSION_OR_CIPHER_MISMATCH o ERR_SSL_PROTOCOL_ERROR, la negociación falló antes de verificar el certificado. Revisa:

  • Puerto o protocolo incorrecto: hablas HTTPS con un puerto que atiende HTTP plano (o al revés).
  • Versión de TLS antigua: los clientes actuales rechazan servidores limitados a TLS 1.0 o 1.1, ya obsoletos. Usa TLS 1.2 o 1.3.
  • Servidor sin certificado configurado para ese nombre o puerto.

Prueba con openssl s_client -connect ejemplo.com:443 -tls1_2 y -tls1_3 para ver qué versión acepta el servidor.

Cómo hacerlo en Square Cloud#

En Square Cloud, varias de estas causas desaparecen porque el TLS es automático: un certificado DV se emite y se renueva solo, y el HTTP se redirige a HTTPS. Toda aplicación web recibe un subdominio *.squareweb.app con HTTPS, en todos los planes.

Para un dominio propio, desde el plan Standard, creas en tu registrador los registros DNS que muestra el panel y sigues ahí el estado de la validación en tiempo real. Si la validación no se completa, revisa que los registros que creaste sean exactamente los que muestra el panel. El TLS del dominio también es automático, con soporte para dominios con acentos (Punycode) y purga de caché. El dominio sigue registrado a tu nombre en tu registrador.

Consulta la página de dominios para el paso a paso.

Red

Dominio propio, TLS automático y CDN global para tu app

Más información

Preguntas frecuentes

¿Por qué el navegador dice 'No seguro' si mi sitio tiene HTTPS?

Dos causas comunes: el certificado tiene un problema (vencido, dominio incorrecto, emisor desconocido) o la página HTTPS carga algún recurso por HTTP, lo que se llama contenido mixto. La consola del navegador muestra cuál de los dos es.

¿Puedo desactivar la verificación del certificado para que desaparezca el error?

Solo en una prueba local desechable. Con rejectUnauthorized: false en Node.js o curl -k, cualquiera en el camino puede hacerse pasar por el servidor. En producción, corrige el certificado o instala la CA correcta.

¿Qué significa NET::ERR_CERT_DATE_INVALID?

El certificado venció o todavía no es válido. Si el certificado del servidor está dentro de su vigencia, revisa el reloj del dispositivo: una fecha incorrecta provoca el mismo error.

¿Cuál es la diferencia entre un error de certificado y uno de handshake?

En el error de certificado, el handshake llegó hasta la verificación y el certificado fue rechazado. En el de handshake, la negociación falla antes: no hay versión de TLS o cifrado en común, o el servidor no habla TLS en ese puerto.

Siguiente artículoQué es un CDN y cómo funcionaUn CDN es una red de servidores repartidos por el mundo que guarda copias de tu contenido y responde cerca del visitante. Cómo funciona, qué cachear y sus límites.

Más en Redes y dominios

Ver los 27 artículos