Mensajes típicos
curl: (60) SSL certificate problem: unable to get local issuer certificate git: SSL certificate problem: unable to get local issuer certificate Python: [SSL: CERTIFICATE_VERIFY_FAILED] certificate verify failed: unable to get local issuer certificate Node: Error: unable to get local issuer certificate (UNABLE_TO_GET_ISSUER_CERT_LOCALLY) Java: PKIX path building failed: unable to find valid certification path to requested target
Todos significan lo mismo: partiendo del certificado del servidor, el cliente no consigue formar una cadena hasta una CA raíz de confianza.
Por qué en el navegador funciona igualmente
Los navegadores son indulgentes. Si falta un intermedio, lo descargan mediante la dirección AIA del certificado o lo toman de su caché porque lo vieron en otra web. curl, Python, Java, Git y la mayoría de las apps no lo hacen. Que una web funcione en el navegador no demuestra, por tanto, que la cadena esté completa.
Tres causas posibles
1. El servidor no envía la cadena completa (la causa más habitual)
Solo se configuró el certificado del servidor, sin el intermedio. Compruébalo con el comprobador SSL o en la línea de comandos:
openssl s_client -connect www.ejemplo.es:443 -servername www.ejemplo.es -showcerts </dev/null
Si la salida muestra un solo certificado (0 s:CN=www.ejemplo.es) junto con verify error:num=20:unable to get local issuer certificate, falta el intermedio.
2. Al cliente le falta la CA raíz
Imágenes Docker antiguas sin el paquete ca-certificates, una instalación de Java desactualizada o un paquete certifi viejo en Python no conocen las CA raíz más recientes.
3. Un cortafuegos intercepta TLS o se trata de una CA interna
Muchos proxies y cortafuegos de empresa sustituyen los certificados por los suyos, emitidos por una CA interna. Los navegadores de los equipos de la empresa conocen esa CA por directiva de grupo, pero los contenedores Docker, Python y Java no. Lo mismo vale para servidores con certificados de tu propia CA corporativa.
Solución en el servidor: enviar la cadena completa
# nginx: certificado + intermedio en un archivo ssl_certificate /etc/ssl/ejemplo/fullchain.pem; ssl_certificate_key /etc/ssl/ejemplo/server.key; # Apache 2.4.8+: la cadena va en SSLCertificateFile SSLCertificateFile /etc/ssl/ejemplo/fullchain.pem SSLCertificateKeyFile /etc/ssl/ejemplo/server.key # Let's Encrypt: usar fullchain.pem, no cert.pem
En Windows/IIS, el intermedio tiene que estar en el almacén «Entidades de certificación intermedias» del equipo. Lo más fácil es importar un PFX que contenga la cadena. Si no tienes el intermedio, el conversor de certificados completa la cadena mediante AIA, y el comprobador SSL se la pasa directamente con «Completar la cadena en el conversor».
Solución en el cliente
# Debian/Ubuntu: actualizar CA raíz o añadir tu propia CA apt install ca-certificates cp raiz-empresa.crt /usr/local/share/ca-certificates/ && update-ca-certificates # Python (requests) pip install --upgrade certifi export REQUESTS_CA_BUNDLE=/etc/ssl/certs/ca-certificates.crt # Java 9+ keytool -importcert -cacerts -alias raiz-empresa -file raiz-empresa.crt
No uses curl -k, verify=False ni NODE_TLS_REJECT_UNAUTHORIZED=0 como solución permanente. La conexión sigue cifrada, pero ya no está protegida frente a atacantes.
FAQ
¿Tengo que enviar la CA raíz?
No. El servidor envía el certificado del servidor y los intermedios. La CA raíz ya tiene que estar en el cliente; una raíz enviada se ignora.
¿Cómo sé si el problema está en el servidor o en el cliente?
Comprueba el servidor con el comprobador SSL. Si indica una cadena completa, el problema está en el cliente (raíz que falta o proxy corporativo). Si avisa de un intermedio que falta, hay que corregir el servidor.
¿Por qué el error solo aparece en Docker?
Las imágenes ligeras como alpine o debian-slim a menudo no incluyen CA raíz. Instala el paquete ca-certificates en el Dockerfile o copia tu CA corporativa y ejecuta update-ca-certificates.