Unable to Get Local Issuer Certificate: cómo solucionarlo

Publicado:

16 min de lectura

Acar Diveroli
Autor: Acar Diveroli
Pila de cubos: un cubo hoja flota sobre un hueco punteado y un cubo raíz de vidrio azul; una regla marca LEAF, MISSING, ROOT.

Ejecutas npm install en un portátil del trabajo y se detiene con npm error code UNABLE_TO_GET_ISSUER_CERT_LOCALLY. El registro se abre sin problemas en Chrome en el mismo equipo, y un compañero que trabaja desde casa instala los mismos paquetes sin ningún fallo. Un script de Python falla con CERTIFICATE_VERIFY_FAILED y git clone informa de un problema con el certificado SSL. Al registro no le pasa nada: tus herramientas y tu navegador confían en listas distintas de autoridades de certificación.

Esta guía explica qué comprueba el cliente, separa las tres causas con errores que reprodujimos en servidores de prueba locales y da la solución para Python, npm, Git y curl, además de la corrección en el lado del servidor.

¿Qué significa «unable to get local issuer certificate»?

Un certificado TLS nombra a su titular (el subject) y a la autoridad de certificación (CA) que lo firmó (el issuer, o emisor). Un servidor envía su propio certificado, llamado certificado hoja (leaf), junto con uno o varios certificados intermedios que lo enlazan con un certificado raíz. Las raíces no se envían durante la conexión: cada cliente guarda su propio conjunto de raíces de confianza, el almacén de confianza, y ese conjunto es el «local» del mensaje.

El texto es el error de verificación 20 de OpenSSL, X509_V_ERR_UNABLE_TO_GET_ISSUER_CERT_LOCALLY. El cliente llegó a un certificado cuyo emisor no encontró ni entre los certificados que envió el servidor ni en su propio almacén. La negociación TLS se detiene antes de que se envíe tu petición.

¿Cómo funciona la verificación de la cadena de certificados?

  1. El servidor envía su cadena. Primero va el certificado hoja, cada certificado siguiente firma al anterior y la raíz puede omitirse porque los clientes ya la tienen (RFC 8446, sección 4.4.2).
  2. El cliente comprueba el certificado hoja: el nombre debe coincidir con el host de la URL y las fechas deben ser válidas.
  3. El cliente busca cada emisor entre los certificados enviados, comprueba la firma y repite un nivel más arriba.
  4. La cima debe terminar en el almacén de confianza, firmada por una raíz en la que el cliente ya confía.
  5. Un eslabón que falta detiene la negociación, y el texto depende de dónde se cortó la cadena.

El almacén de confianza depende de la herramienta, no del equipo. Requests usa certifi, un paquete de Python con la lista de raíces de Mozilla; Node.js compila en cada versión una copia del almacén de Mozilla; Git for Windows lee su propio ca-bundle.crt o el almacén de Windows. Por eso un programa puede fallar donde otro funciona en el mismo portátil.

Tres mensajes de error, una misma familia

Creamos con OpenSSL una raíz, un intermedio y un certificado hoja, levantamos tres servidores HTTPS locales que envían partes distintas de la cadena y los llamamos en Windows 11 desde Python 3.13.9 (Requests 2.34.2), Node.js 24.11.1 (npm 11.6.2), curl 8.21.0 y Git 2.55 con OpenSSL. Ninguna herramienta conocía nuestra raíz.

Lo que envió el servidorPython (ssl, Requests)Node.js y npmcurl y Git (OpenSSL)
Hoja + intermediounable to get local issuer certificateUNABLE_TO_GET_ISSUER_CERT_LOCALLYunable to get local issuer certificate (20)
Solo la hojaunable to get local issuer certificateUNABLE_TO_VERIFY_LEAF_SIGNATUREunable to get local issuer certificate (20)
Hoja + intermedio + raízself-signed certificate in certificate chainSELF_SIGNED_CERT_IN_CHAINself-signed certificate in certificate chain (19)

Python y curl usan las mismas palabras para un intermedio que falta y para una raíz desconocida, así que solo la cadena te dice qué lado hay que corregir. La tercera fila es el mismo problema con la raíz incluida: el servidor, o un proxy por el camino, envió una raíz en la que tu herramienta no confía.

Las tres causas del error

1. El servidor no envía su certificado intermedio

El administrador instaló solo el certificado hoja, así que falla cualquier cliente que no tenga el intermedio, en cualquier red. Los navegadores suelen ocultarlo: Mozilla explica que Firefox incluye de antemano los intermedios conocidos, mientras que otros navegadores descargan en segundo plano el que falta. Python, Node.js, curl y Git no hacen ninguna de las dos cosas, así que «en Chrome funciona» no demuestra nada.

2. Un proxy o un antivirus con inspección TLS vuelve a firmar el tráfico

Los gateways web de las empresas, los antivirus con análisis HTTPS y herramientas de depuración como mitmproxy, Charles y Fiddler abren la conexión cifrada y presentan un certificado nuevo para el sitio, firmado por su propia raíz. El equipo de TI instala esa raíz en el almacén del sistema de los portátiles gestionados, así que los navegadores la aceptan; las herramientas con su propia lista, no. Pistas: el error aparece en la red de la oficina o en la VPN, no en casa, y afecta a casi todos los sitios. Si tú mismo usas una de estas herramientas, el paso del certificado raíz se explica en ¿Qué es un proxy MITM? Guía de Charles, Fiddler y mitmproxy.

3. Tu herramienta confía en una lista distinta de la de tu sistema

Los bundles de Requests, Node.js y Git nunca ven una raíz que tu empresa instaló en Windows o macOS, ni una CA privada que firma servicios internos. El Python del instalador de python.org para macOS necesita su propio paso de certificados por el mismo motivo, y a los sistemas y bundles desactualizados pueden faltarles raíces públicas más recientes.

¿Cómo sabes cuál es tu causa?

Mira la cadena que el servidor envía de verdad. Git for Windows incluye openssl, así que esto también funciona en Git Bash:

bash
openssl s_client -connect localhost:47444 -servername localhost -showcerts </dev/null

Nuestro servidor del puerto 47444 envía solo su certificado hoja. Las líneas relevantes:

text
Certificate chain
 0 s:CN=localhost
   i:O=Example Corp, CN=Example Corp Issuing CA
Verify return code: 21 (unable to verify the first certificate)

s: es el titular (subject) e i: es el emisor (issuer); una cadena completa añade una entrada 1 s:. Para el caso de la inspección, ejecutamos un proxy local que vuelve a firmar el tráfico como un gateway de empresa, con una CA llamada Example Corp, y pedimos example.com a través de él (-proxy 127.0.0.1:47461):

text
Certificate chain
 0 s:CN=example.com
   i:O=Example Corp, CN=Example Corp Issuing CA
 1 s:O=Example Corp, CN=Example Corp Issuing CA
   i:O=Example Corp, CN=Example Corp Root CA
Verify return code: 20 (unable to get local issuer certificate)

A través de un túnel normal, el mismo comando mostró la cadena real del sitio, emitida por Cloudflare TLS Issuing ECC CA 3, y Verify return code: 0 (ok). Lee tu propia salida así:

  • Solo la entrada 0, emisor público: el servidor no envía su intermedio (causa 1).
  • El emisor es tu empresa, un producto de seguridad o una herramienta de depuración: algo vuelve a firmar el tráfico (causa 2).
  • Cadena pública completa y 0 (ok), pero tu herramienta falla: la herramienta lee otro almacén de confianza (causa 3).

¿Cómo se soluciona en Python?

Requests envuelve el error en una línea más larga; Max Retries Exceeded With URL: qué es y cómo solucionarlo explica cómo leer su parte Caused by. Nuestra prueba produjo:

text
requests.exceptions.SSLError: HTTPSConnectionPool(host='localhost', port=47443): Max retries exceeded with url: / (Caused by SSLError(SSLCertVerificationError(1, '[SSL: CERTIFICATE_VERIFY_FAILED] certificate verify failed: unable to get local issuer certificate (_ssl.c:1032)')))

Si el equipo de TI instaló la raíz de la empresa en el sistema operativo, deja que Python use ese almacén. El paquete truststore (Python 3.10 o posterior) hace que Requests y el módulo ssl verifiquen a través del almacén del sistema; su documentación limita inject_into_ssl() a aplicaciones y scripts, no a bibliotecas:

python
import truststore
truststore.inject_into_ssl()  # llámalo una vez, al principio de tu script

import requests

print(requests.get("https://example.com/", timeout=10).status_code)

Si no, crea un bundle con las raíces de certifi más la raíz de la empresa y apunta REQUESTS_CA_BUNDLE a él:

bash
cat "$(python -m certifi)" corp-root.pem > ca-bundle-plus-corp.pem
export REQUESTS_CA_BUNDLE="$PWD/ca-bundle-plus-corp.pem"

Después, tanto example.com como nuestro servidor de prueba devolvieron 200. No uses nunca la raíz de la empresa sola: la variable reemplaza a certifi, y en nuestra prueba example.com falló entonces con el mismo error. Vuelve a crear el archivo cada vez que actualices certifi.

A diferencia de Requests, pip también usa los certificados del sistema desde la versión 24.2 (documentación de pip), así que pip install puede funcionar donde Requests falla. Para un bundle concreto, pasa --cert o define PIP_CERT. En macOS con el instalador de python.org, ejecuta una vez Install Certificates.command.

Si un sitio del que recopilas datos no envía su intermedio, descárgalo de la CA emisora, añádelo al archivo combinado (así pasó nuestro servidor que solo enviaba la hoja) y avisa al propietario.

¿Cómo se soluciona en Node.js y npm?

La salida de npm lleva el código de Node.js:

text
npm error code UNABLE_TO_GET_ISSUER_CERT_LOCALLY
npm error errno UNABLE_TO_GET_ISSUER_CERT_LOCALLY
npm error request to https://localhost:47443/demo-pkg failed, reason: unable to get local issuer certificate

Node.js añade a su lista integrada las raíces del archivo indicado en NODE_EXTRA_CA_CERTS (documentación de Node.js). npm se ejecuta sobre Node.js, así que una sola variable arregla los dos:

bash
export NODE_EXTRA_CA_CERTS="$PWD/corp-root.pem"
npm view demo-pkg version --registry https://localhost:47443/

El segundo comando mostró 1.0.0 (en PowerShell: $env:NODE_EXTRA_CA_CERTS = "C:\certs\corp-root.pem"). Node.js lee la variable solo al arrancar, no desde dentro de un script en ejecución.

El ajuste cafile propio de npm, en cambio, reemplaza la lista de confianza. Con solo la raíz de la empresa dentro, nuestra petición al registro público falló:

text
npm error request to https://registry.npmjs.org/left-pad failed, reason: unable to get local issuer certificate

Si la raíz ya está en el almacén del sistema, Node.js puede leer ese almacén: --use-system-ca llegó en Node.js 23.8.0 y NODE_USE_SYSTEM_CA=1 en 24.6.0 y 22.19.0, y los dos funcionaron con npm en nuestra prueba (NODE_OPTIONS=--use-system-ca). Los asistentes de programación con IA basados en Node.js leen las mismas variables; la documentación de Claude Code indica NODE_EXTRA_CA_CERTS para las CA de empresa.

¿Cómo se soluciona en Git?

Con el backend de OpenSSL, Git transmite el texto de curl:

text
fatal: unable to access 'https://localhost:47443/repo.git/': SSL certificate OpenSSL verify result: unable to get local issuer certificate (20)

En Windows, deja que Git use el almacén de Windows. El instalador de Git for Windows ofrece «Use the OpenSSL library» (usar la biblioteca OpenSSL) y «Use the native Windows Secure Channel library» (usar la biblioteca nativa Secure Channel de Windows); puedes cambiarlo más tarde:

bash
git config --global http.sslBackend schannel

Si la raíz tampoco está ahí, Schannel informa de SEC_E_UNTRUSTED_ROOT (0x80090325) con un mensaje en el idioma del sistema; en inglés es «The certificate chain was issued by an authority that is not trusted» (la cadena de certificados la emitió una entidad que no es de confianza). Schannel ignora http.sslCAInfo salvo que esté definido http.schannelUseSSLCAInfo.

En macOS, Linux o con el backend de OpenSSL, indica a Git un archivo para un único servidor:

bash
git config --global http.https://git.example.com/.sslCAInfo /path/to/corp-root.pem

En nuestra prueba, el host configurado superó TLS, otro host siguió fallando y github.com siguió funcionando. Si el gateway inspecciona todos los sitios, usa Schannel o un archivo combinado (el bundle de Git más la raíz de la empresa).

¿Cómo se soluciona en curl?

El número de error de curl es el 60. Nuestro curl 8.21.0 lo redacta como aparece abajo; otras compilaciones muestran SSL certificate problem: unable to get local issuer certificate:

text
curl: (60) SSL certificate OpenSSL verify result: unable to get local issuer certificate (20)
More details here: https://curl.se/docs/sslcerts.html
  • --cacert corp-root.pem verifica contra ese archivo y reemplaza el bundle predeterminado, así que usa un archivo combinado si también llamas a sitios públicos.
  • --ca-native (curl 8.2.0+) añade el almacén del sistema a las compilaciones con OpenSSL en Windows, y en macOS cuando curl está compilado con SecTrust de Apple.
  • El curl.exe propio de Windows usa Schannel y el almacén de Windows. Con una CA privada que no publica datos de revocación, se detiene en schannel: the revocation status is unknown; --ssl-revoke-best-effort lo acepta y sigue comprobando la cadena.
  • --proxy-cacert verifica un proxy HTTPS, es decir, uno al que llegas con una URL de proxy https://. Un proxy http:// normal no tiene certificado propio; cURL con proxy: cómo configurar proxies HTTP y SOCKS5 cubre ambos casos.

La página del proyecto curl sobre verificación de certificados SSL recomienda encarecidamente no saltarse nunca la verificación en producción.

¿Por qué verify=False, -k y NODE_TLS_REJECT_UNAUTHORIZED=0 no son soluciones?

Dejan de comprobar la cadena en lugar de repararla, así que el cliente acepta cualquier certificado para cualquier nombre, incluido el de alguien en la misma red que quiera leer tu tráfico; la documentación de Node.js califica NODE_TLS_REJECT_UNAUTHORIZED=0 de inseguro con todas las letras. En nuestra prueba con Git, http.sslVerify=false llegó al servidor sin ningún aviso, así que la configuración rota simplemente se queda como está. Además, estos interruptores se propagan: una variable en el perfil de la shell llega a todos los programas de Node.js, y el verify=False de una prueba acaba en producción.

¿Cómo envías una cadena completa desde tu propio servidor?

Configura el certificado hoja seguido de todos los intermedios y deja fuera la raíz. En nginx, el archivo de ssl_certificate contiene primero el certificado del servidor y después los intermedios (documentación de nginx); en el orden incorrecto, nginx se niega a arrancar con key values mismatch. Certbot escribe fullchain.pem para esto, mientras que cert.pem contiene solo la hoja; Apache 2.4.8 y posteriores aceptan fullchain.pem en SSLCertificateFile. Compruébalo desde fuera con openssl s_client -connect yourdomain:443 -servername yourdomain -showcerts: deberías ver las entradas 0 y 1 y Verify return code: 0 (ok).

Dos cambios de 2026 hacen que merezca la pena automatizar esta comprobación. Desde el 15 de marzo de 2026, los Baseline Requirements del CA/Browser Forum limitan los certificados públicos a 200 días (100 desde marzo de 2027, 47 desde marzo de 2029), así que las renovaciones llegan más a menudo, y cada una es una ocasión de desplegar el archivo equivocado. El 27 de mayo de 2026, Let's Encrypt pasó su perfil predeterminado a los intermedios Generation Y, que llegan a las conocidas raíces ISRG mediante firmas cruzadas; Certify The Web, un cliente de certificados para Windows, documenta servidores Windows que envían la cadena más corta hacia las raíces nuevas, que los clientes antiguos no pueden verificar.

¿Un proxy causa este error?

Solo un proxy que descifra el tráfico. En una petición https:// a través de un proxy HTTP, el cliente envía CONNECT host:443 y el proxy retransmite bytes cifrados, así que verificas el certificado del propio sitio, como mostró nuestro túnel normal. Los gateways de Proxynet también son túneles normales: una conexión con los Proxies HTTPS no necesita ningún certificado raíz en tu equipo.

Cuando haces scraping con los Proxies residenciales, la dirección de salida cambia, pero el certificado del sitio no, así que un error de cadena en un objetivo te sigue a todas las salidas; rotar las IP no lo soluciona.

Dónde aparece este error

  • Instalación de paquetes en un portátil de empresa: npm, pip y yarn fallan detrás de un gateway con inspección TLS mientras el navegador funciona.
  • Runners de CI y builds de Docker: un contenedor tiene su propio almacén de confianza, así que la raíz de la empresa tiene que ir dentro de la imagen.
  • Asistentes de programación con IA: las herramientas de línea de comandos basadas en Node.js llaman a sus API a través del mismo gateway.
  • Clientes de API: en Postman, abre Settings (Configuración), después Certificates (Certificados), activa CA certificates (certificados de CA) y selecciona el archivo PEM.
  • Scrapers: un sitio al que le falta el intermedio falla en todos los scripts aunque se abra en el navegador.
  • Servicios internos: paneles y servidores Git firmados por una CA de la empresa.

Errores comunes

  • Apuntar REQUESTS_CA_BUNDLE, el cafile de npm o --cacert solo a la raíz de la empresa. Los tres reemplazan la lista predeterminada.
  • Añadir el certificado equivocado. El archivo debe contener la raíz que publica TI, no el certificado hoja de un sitio.
  • Guardar el certificado en DER binario. Estos ajustes esperan PEM, la forma de texto que empieza por -----BEGIN CERTIFICATE-----.
  • Definir NODE_EXTRA_CA_CERTS dentro del script. Node.js solo la lee al arrancar.
  • Desplegar cert.pem en lugar de fullchain.pem. Puede que los navegadores sigan funcionando, así que nadie se da cuenta.

Guía de decisión

Lo que vesQué hacer
Solo la entrada 0, emisor públicoEl propietario debe servir la cadena completa; mientras tanto, añade el intermedio a tu bundle
El emisor es tu empresa, el antivirus o una herramienta de depuraciónPide esa raíz a TI y añádela en cada herramienta
Cadena pública y 0 (ok), falla una herramientaHaz que la herramienta lea el almacén del sistema: truststore, --use-system-ca, Schannel
Falla npmNODE_EXTRA_CA_CERTS; cafile solo con un bundle completo
Git falla con un servidor internohttp.<url>.sslCAInfo para ese host
Falla curl--cacert con un archivo combinado, o --ca-native

Preguntas frecuentes

¿Por qué el sitio se abre en el navegador pero falla en Python, curl o Node.js?

El navegador y la herramienta leen almacenes de confianza distintos, y los navegadores reparan por sí mismos las cadenas incompletas. Un script lee su propio bundle y no hace ninguna de las dos cosas; compara la cadena con openssl s_client para ver cuál es tu caso.

¿Es seguro usar verify=False, -k o NODE_TLS_REJECT_UNAUTHORIZED=0?

No como solución. Desactivan todas las comprobaciones del certificado, así que cualquiera entre tú y el servidor puede leer o modificar el tráfico. Úsalos como mucho para un solo comando contra tu propio servidor de prueba.

¿Qué diferencia hay entre «unable to get local issuer certificate» y «self-signed certificate in certificate chain»?

Los dos significan que la cadena termina en una raíz en la que tu herramienta no confía. En el primero, la raíz no se envió y no se encontró en local; en el segundo, el servidor o un proxy envió la propia raíz. La solución es la misma.

¿Dónde consigo el certificado raíz de mi empresa?

En tu equipo de TI o de seguridad; en un portátil gestionado ya está en el almacén del sistema, así que truststore, --use-system-ca y Schannel funcionan sin archivo. No tomes nunca una raíz de una fuente no oficial: una raíz de confianza puede firmar para cualquier dominio.

¿Por qué pip funciona cuando Requests falla en el mismo equipo?

Desde la versión 24.2, pip comprueba los certificados del sistema además de certifi, mientras que Requests solo lee certifi. El paquete truststore da a tu script el mismo comportamiento.

¿Puede un proxy causar «unable to get local issuer certificate»?

Solo uno que descifre el tráfico: un gateway de empresa, un antivirus con análisis HTTPS o un proxy de depuración. Un proxy de túnel que usa CONNECT deja pasar sin cambios el certificado del propio sitio.

En resumen

El error significa que tu cliente no pudo enlazar el certificado del servidor con una raíz en la que confía. openssl s_client te dice por qué: una hoja sola apunta al servidor, un emisor de empresa a la inspección TLS y una cadena pública limpia al bundle propio de tu herramienta. Añade la raíz correcta donde busca esa herramienta y deja la verificación activada. Si buscas proxies que tunelizan el tráfico sin tocar los certificados, consulta nuestra página de servicios de proxy.