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?
- 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).
- El cliente comprueba el certificado hoja: el nombre debe coincidir con el host de la URL y las fechas deben ser válidas.
- El cliente busca cada emisor entre los certificados enviados, comprueba la firma y repite un nivel más arriba.
- La cima debe terminar en el almacén de confianza, firmada por una raíz en la que el cliente ya confía.
- 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 servidor | Python (ssl, Requests) | Node.js y npm | curl y Git (OpenSSL) |
|---|---|---|---|
| Hoja + intermedio | unable to get local issuer certificate | UNABLE_TO_GET_ISSUER_CERT_LOCALLY | unable to get local issuer certificate (20) |
| Solo la hoja | unable to get local issuer certificate | UNABLE_TO_VERIFY_LEAF_SIGNATURE | unable to get local issuer certificate (20) |
| Hoja + intermedio + raíz | self-signed certificate in certificate chain | SELF_SIGNED_CERT_IN_CHAIN | self-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:
openssl s_client -connect localhost:47444 -servername localhost -showcerts </dev/nullNuestro servidor del puerto 47444 envía solo su certificado hoja. Las líneas relevantes:
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):
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:
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:
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:
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:
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 certificateNode.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:
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ó:
npm error request to https://registry.npmjs.org/left-pad failed, reason: unable to get local issuer certificateSi 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:
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:
git config --global http.sslBackend schannelSi 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:
git config --global http.https://git.example.com/.sslCAInfo /path/to/corp-root.pemEn 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:
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.pemverifica 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.exepropio de Windows usa Schannel y el almacén de Windows. Con una CA privada que no publica datos de revocación, se detiene enschannel: the revocation status is unknown;--ssl-revoke-best-effortlo acepta y sigue comprobando la cadena. --proxy-cacertverifica un proxy HTTPS, es decir, uno al que llegas con una URL de proxyhttps://. Un proxyhttp://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, elcafilede npm o--cacertsolo 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_CERTSdentro del script. Node.js solo la lee al arrancar. - Desplegar
cert.pemen lugar defullchain.pem. Puede que los navegadores sigan funcionando, así que nadie se da cuenta.
Guía de decisión
| Lo que ves | Qué hacer |
|---|---|
Solo la entrada 0, emisor público | El 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ón | Pide esa raíz a TI y añádela en cada herramienta |
Cadena pública y 0 (ok), falla una herramienta | Haz que la herramienta lea el almacén del sistema: truststore, --use-system-ca, Schannel |
| Falla npm | NODE_EXTRA_CA_CERTS; cafile solo con un bundle completo |
| Git falla con un servidor interno | http.<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.




