Você roda npm install em um notebook do trabalho e o comando para com npm error code UNABLE_TO_GET_ISSUER_CERT_LOCALLY. O registry abre normalmente no Chrome na mesma máquina, e um colega que trabalha de casa instala os mesmos pacotes sem problema. Um script Python falha com CERTIFICATE_VERIFY_FAILED, e o git clone informa um problema de certificado SSL. Não há nada de errado com o registry: suas ferramentas e seu navegador confiam em listas diferentes de autoridades certificadoras.
Este guia explica o que o cliente verifica, separa as três causas com erros que reproduzimos em servidores de teste locais e mostra a correção para Python, npm, Git e curl, além da correção no lado do servidor.
O que significa "unable to get local issuer certificate"?
Um certificado TLS informa o titular (o subject) e a autoridade certificadora (CA) que o assinou (o issuer, ou emissor). Um servidor envia o próprio certificado, chamado de certificado folha (leaf), mais um ou mais certificados intermediários que o ligam a um certificado raiz. As raízes não são enviadas durante a conexão: cada cliente mantém o próprio conjunto de raízes confiáveis, o repositório de confiança, e esse conjunto é o "local" da mensagem.
O texto é o erro de verificação 20 do OpenSSL, X509_V_ERR_UNABLE_TO_GET_ISSUER_CERT_LOCALLY. O cliente chegou a um certificado cujo emissor não encontrou nem entre os certificados que o servidor enviou nem no próprio repositório. O handshake TLS para antes de a sua requisição ser enviada.
Como funciona a verificação da cadeia de certificados?
- O servidor envia a cadeia. O certificado folha vem primeiro, cada certificado seguinte assina o anterior, e a raiz pode ficar de fora porque os clientes já a têm (RFC 8446, seção 4.4.2).
- O cliente verifica o certificado folha: o nome precisa corresponder ao host da URL, e as datas precisam ser válidas.
- O cliente encontra cada emissor entre os certificados enviados, confere a assinatura e repete um nível acima.
- O topo precisa terminar no repositório de confiança, assinado por uma raiz em que o cliente já confia.
- Um elo faltando interrompe o handshake, e o texto depende de onde a cadeia parou.
O repositório de confiança depende da ferramenta, não da máquina. O Requests usa o certifi, um pacote Python com a lista de raízes da Mozilla; o Node.js embute em cada versão uma cópia do repositório da Mozilla; o Git for Windows lê o próprio ca-bundle.crt ou o repositório do Windows. Por isso um programa pode falhar onde outro funciona no mesmo notebook.
Três mensagens de erro, uma mesma família
Criamos com o OpenSSL uma raiz, um intermediário e um certificado folha, rodamos três servidores HTTPS locais que enviam partes diferentes da cadeia e os chamamos no Windows 11 a partir do Python 3.13.9 (Requests 2.34.2), Node.js 24.11.1 (npm 11.6.2), curl 8.21.0 e Git 2.55 com OpenSSL. Nenhuma ferramenta conhecia a nossa raiz.
| O que o servidor enviou | Python (ssl, Requests) | Node.js e npm | curl e Git (OpenSSL) |
|---|---|---|---|
| Folha + intermediário | unable to get local issuer certificate | UNABLE_TO_GET_ISSUER_CERT_LOCALLY | unable to get local issuer certificate (20) |
| Só a folha | unable to get local issuer certificate | UNABLE_TO_VERIFY_LEAF_SIGNATURE | unable to get local issuer certificate (20) |
| Folha + intermediário + raiz | self-signed certificate in certificate chain | SELF_SIGNED_CERT_IN_CHAIN | self-signed certificate in certificate chain (19) |
Python e curl usam as mesmas palavras para um intermediário ausente e para uma raiz desconhecida, então só a cadeia mostra qual lado corrigir. A terceira linha é o mesmo problema com a raiz incluída: o servidor, ou um proxy no meio do caminho, enviou uma raiz em que a sua ferramenta não confia.
As três causas do erro
1. O servidor deixa de enviar o certificado intermediário
O administrador instalou só o certificado folha, então todo cliente sem o intermediário falha, em qualquer rede. Os navegadores costumam esconder isso: a Mozilla explica que o Firefox já traz os intermediários conhecidos, enquanto outros navegadores baixam o que falta em segundo plano. Python, Node.js, curl e Git não fazem nem uma coisa nem outra, então "funciona no Chrome" não prova nada.
2. Um proxy ou antivírus com inspeção TLS assina o tráfego de novo
Gateways web de empresas, antivírus com verificação de HTTPS e ferramentas de depuração como mitmproxy, Charles e Fiddler abrem a conexão criptografada e apresentam um certificado novo para o site, assinado pela própria raiz. A TI instala essa raiz no repositório do sistema dos notebooks gerenciados, então os navegadores a aceitam; as ferramentas com lista própria, não. Pistas: o erro aparece na rede do escritório ou na VPN, não em casa, e atinge quase todos os sites. Se você mesmo usa uma ferramenta dessas, o passo do certificado raiz está em O que é um proxy MITM? Guia de Charles, Fiddler e mitmproxy.
3. Sua ferramenta confia em uma lista diferente da do sistema
Os bundles do Requests, do Node.js e do Git nunca veem uma raiz que a sua empresa instalou no Windows ou no macOS, nem uma CA privada que assina serviços internos. O Python do instalador do python.org para macOS precisa de uma etapa própria de certificados pelo mesmo motivo, e sistemas e bundles desatualizados podem não ter raízes públicas mais novas.
Como descobrir qual é a sua causa?
Veja a cadeia que o servidor realmente envia. O Git for Windows traz o openssl, então isto também roda no Git Bash:
openssl s_client -connect localhost:47444 -servername localhost -showcerts </dev/nullNosso servidor na porta 47444 envia só o certificado folha. As linhas 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: é o titular (subject) e i: é o emissor (issuer); uma cadeia completa acrescenta uma entrada 1 s:. Para o caso da inspeção, rodamos um proxy local que assina o tráfego de novo como um gateway de empresa, com uma CA chamada Example Corp, e pedimos example.com por ele (-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)Por um túnel comum, o mesmo comando mostrou a cadeia real do site, emitida por Cloudflare TLS Issuing ECC CA 3, e Verify return code: 0 (ok). Leia a sua saída assim:
- Só a entrada 0, emissor público: o servidor deixa de enviar o intermediário (causa 1).
- O emissor é a sua empresa, um produto de segurança ou uma ferramenta de depuração: algo assina o tráfego de novo (causa 2).
- Cadeia pública completa e
0 (ok), mas a sua ferramenta falha: a ferramenta lê outro repositório de confiança (causa 3).
Como resolver no Python?
O Requests embrulha o erro em uma linha mais longa; Max Retries Exceeded With URL: o que é e como resolver explica como ler a parte Caused by. Nosso teste gerou:
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)')))Se a TI instalou a raiz da empresa no sistema operacional, deixe o Python usar esse repositório. O pacote truststore (Python 3.10 ou posterior) faz o Requests e o módulo ssl verificarem pelo repositório do sistema; a documentação dele restringe inject_into_ssl() a aplicações e scripts, não a bibliotecas:
import truststore
truststore.inject_into_ssl() # chame uma vez, no início do seu script
import requests
print(requests.get("https://example.com/", timeout=10).status_code)Caso contrário, monte um bundle com as raízes do certifi mais a raiz da empresa e aponte REQUESTS_CA_BUNDLE para ele:
cat "$(python -m certifi)" corp-root.pem > ca-bundle-plus-corp.pem
export REQUESTS_CA_BUNDLE="$PWD/ca-bundle-plus-corp.pem"Depois disso, tanto example.com quanto o nosso servidor de teste retornaram 200. Nunca use a raiz da empresa sozinha: a variável substitui o certifi, e no nosso teste example.com passou a falhar com o mesmo erro. Gere o arquivo de novo depois de cada atualização do certifi.
Diferente do Requests, o pip também usa os certificados do sistema desde a versão 24.2 (documentação do pip), então pip install pode funcionar onde o Requests falha. Para um bundle específico, passe --cert ou defina PIP_CERT. No macOS com o instalador do python.org, rode Install Certificates.command uma vez.
Se um site do qual você coleta dados deixa de enviar o intermediário, baixe-o da CA emissora, adicione-o ao arquivo combinado (foi assim que o nosso servidor que só enviava a folha passou) e avise o dono do site.
Como resolver no Node.js e no npm?
A saída do npm traz o código do 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 certificateO Node.js acrescenta à lista embutida as raízes do arquivo indicado em NODE_EXTRA_CA_CERTS (documentação do Node.js). O npm roda sobre o Node.js, então uma única variável corrige os dois:
export NODE_EXTRA_CA_CERTS="$PWD/corp-root.pem"
npm view demo-pkg version --registry https://localhost:47443/O segundo comando mostrou 1.0.0 (no PowerShell: $env:NODE_EXTRA_CA_CERTS = "C:\certs\corp-root.pem"). O Node.js lê a variável só na inicialização, não de dentro de um script em execução.
Já a configuração cafile do próprio npm substitui a lista confiável. Com só a raiz da empresa dentro, a nossa requisição ao registry público falhou:
npm error request to https://registry.npmjs.org/left-pad failed, reason: unable to get local issuer certificateSe a raiz já está no repositório do sistema, o Node.js pode ler esse repositório: --use-system-ca chegou no Node.js 23.8.0 e NODE_USE_SYSTEM_CA=1 no 24.6.0 e no 22.19.0, e os dois funcionaram com o npm no nosso teste (NODE_OPTIONS=--use-system-ca). Assistentes de programação com IA feitos sobre o Node.js leem as mesmas variáveis; a documentação do Claude Code indica NODE_EXTRA_CA_CERTS para CAs de empresa.
Como resolver no Git?
Com o backend OpenSSL, o Git repassa o texto do curl:
fatal: unable to access 'https://localhost:47443/repo.git/': SSL certificate OpenSSL verify result: unable to get local issuer certificate (20)No Windows, deixe o Git usar o repositório do Windows. O instalador do Git for Windows oferece "Use the OpenSSL library" (usar a biblioteca OpenSSL) e "Use the native Windows Secure Channel library" (usar a biblioteca nativa Secure Channel do Windows); dá para trocar depois:
git config --global http.sslBackend schannelSe a raiz também não estiver lá, o Schannel informa SEC_E_UNTRUSTED_ROOT (0x80090325) com uma mensagem no idioma do sistema; em inglês, ela diz "The certificate chain was issued by an authority that is not trusted" (a cadeia de certificados foi emitida por uma autoridade que não é confiável). O Schannel ignora http.sslCAInfo, a menos que http.schannelUseSSLCAInfo esteja definido.
No macOS, no Linux ou com o backend OpenSSL, aponte o Git para um arquivo só para um servidor:
git config --global http.https://git.example.com/.sslCAInfo /path/to/corp-root.pemNo nosso teste, o host configurado passou pelo TLS, outro host continuou falhando e github.com seguiu funcionando. Se o gateway inspeciona todos os sites, use o Schannel ou um arquivo combinado (o bundle do Git mais a raiz da empresa).
Como resolver no curl?
O número do erro no curl é 60. O nosso curl 8.21.0 escreve a mensagem como abaixo; outros builds mostram 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 com esse arquivo e substitui o bundle padrão, então use um arquivo combinado se também acessar sites públicos.--ca-native(curl 8.2.0+) acrescenta o repositório do sistema aos builds com OpenSSL no Windows, e no macOS quando o curl foi compilado com o SecTrust da Apple.- O
curl.exedo próprio Windows usa o Schannel e o repositório do Windows. Com uma CA privada que não publica dados de revogação, ele para emschannel: the revocation status is unknown;--ssl-revoke-best-effortaceita isso e continua verificando a cadeia. --proxy-cacertverifica um proxy HTTPS, ou seja, um proxy acessado por uma URL de proxyhttps://. Um proxyhttp://comum não tem certificado próprio; cURL com proxy: como configurar proxy HTTP e SOCKS5 cobre os dois casos.
A página do projeto curl sobre verificação de certificados SSL recomenda com ênfase nunca pular a verificação em produção.
Por que verify=False, -k e NODE_TLS_REJECT_UNAUTHORIZED=0 não são soluções?
Essas opções param de verificar a cadeia em vez de consertá-la, então o cliente aceita qualquer certificado para qualquer nome, inclusive o de alguém na mesma rede que queira ler o seu tráfego; a documentação do Node.js chama NODE_TLS_REJECT_UNAUTHORIZED=0 de inseguro com todas as letras. No nosso teste com o Git, http.sslVerify=false chegou ao servidor sem nenhum aviso, então a configuração quebrada simplesmente continua lá. Essas opções também se espalham: uma variável no perfil do shell chega a todo programa Node.js, e o verify=False de um teste acaba em produção.
Como enviar uma cadeia completa do seu próprio servidor?
Configure o certificado folha seguido de todos os intermediários e deixe a raiz de fora. No nginx, o arquivo de ssl_certificate traz primeiro o certificado do servidor e depois os intermediários (documentação do nginx); na ordem errada, o nginx se recusa a iniciar com key values mismatch. O Certbot grava fullchain.pem para isso, enquanto cert.pem contém só a folha; o Apache 2.4.8 e posteriores aceitam fullchain.pem em SSLCertificateFile. Confira de fora com openssl s_client -connect yourdomain:443 -servername yourdomain -showcerts: você deve ver as entradas 0 e 1 e Verify return code: 0 (ok).
Duas mudanças de 2026 fazem valer a pena automatizar essa verificação. Desde 15 de março de 2026, os Baseline Requirements do CA/Browser Forum limitam os certificados públicos a 200 dias (100 a partir de março de 2027, 47 a partir de março de 2029), então as renovações ficam mais frequentes, e cada uma é uma chance de publicar o arquivo errado. Em 27 de maio de 2026, o Let's Encrypt passou o perfil padrão para os intermediários Generation Y, que chegam às conhecidas raízes ISRG por meio de assinaturas cruzadas; a Certify The Web, um cliente de certificados para Windows, documenta servidores Windows que enviam a cadeia mais curta até as novas raízes, que clientes mais antigos não conseguem verificar.
Um proxy causa esse erro?
Só um proxy que descriptografa o tráfego. Em uma requisição https:// por um proxy HTTP, o cliente envia CONNECT host:443 e o proxy repassa bytes criptografados, então você verifica o certificado do próprio site, como o nosso túnel comum mostrou. Os gateways da Proxynet também são túneis comuns: uma conexão pelos Proxies HTTPS não precisa de nenhum certificado raiz no seu computador.
Quando você faz scraping com os Proxies residenciais, o endereço de saída muda, mas o certificado do site não, então um erro de cadeia em um alvo acompanha você em todas as saídas; trocar de IP não resolve.
Onde você encontra esse erro
- Instalação de pacotes em notebook da empresa: npm, pip e yarn falham atrás de um gateway com inspeção TLS enquanto o navegador funciona.
- Runners de CI e builds Docker: um contêiner tem o próprio repositório de confiança, então a raiz da empresa precisa entrar na imagem.
- Assistentes de programação com IA: ferramentas de linha de comando feitas sobre o Node.js chamam as APIs pelo mesmo gateway.
- Clientes de API: no Postman, abra Settings (Configurações), depois Certificates (Certificados), ative CA certificates (certificados de CA) e selecione o arquivo PEM.
- Scrapers: um site sem o intermediário falha em todo script, mas abre no navegador.
- Serviços internos: painéis e servidores Git assinados por uma CA da empresa.
Erros comuns
- Apontar
REQUESTS_CA_BUNDLE, ocafiledo npm ou--cacertsó para a raiz da empresa. Os três substituem a lista padrão. - Adicionar o certificado errado. O arquivo precisa conter a raiz que a TI publica, não o certificado folha de um site.
- Salvar o certificado em DER binário. Essas configurações esperam PEM, o formato de texto que começa com
-----BEGIN CERTIFICATE-----. - Definir
NODE_EXTRA_CA_CERTSdentro do script. O Node.js só lê a variável na inicialização. - Publicar
cert.pemem vez defullchain.pem. Os navegadores podem continuar funcionando, então ninguém percebe.
Guia de decisão
| O que você vê | O que fazer |
|---|---|
Só a entrada 0, emissor público | O dono do site deve servir a cadeia completa; enquanto isso, adicione o intermediário ao seu bundle |
| O emissor é a sua empresa, o antivírus ou uma ferramenta de depuração | Peça essa raiz à TI e adicione-a em cada ferramenta |
Cadeia pública e 0 (ok), uma ferramenta falha | Faça a ferramenta ler o repositório do sistema: truststore, --use-system-ca, Schannel |
| npm falha | NODE_EXTRA_CA_CERTS; cafile só com um bundle completo |
| Git falha em um servidor interno | http.<url>.sslCAInfo para esse host |
| curl falha | --cacert com um arquivo combinado, ou --ca-native |
Perguntas frequentes
Por que o site abre no navegador, mas falha no Python, no curl ou no Node.js?
O navegador e a ferramenta leem repositórios de confiança diferentes, e os navegadores completam sozinhos as cadeias incompletas. Um script lê o próprio bundle e não faz nenhuma das duas coisas; compare a cadeia com openssl s_client para ver qual é o seu caso.
É seguro usar verify=False, -k ou NODE_TLS_REJECT_UNAUTHORIZED=0?
Não como correção. Essas opções desligam toda verificação de certificado, então qualquer um entre você e o servidor pode ler ou alterar o tráfego. Use-as no máximo para um único comando contra o seu próprio servidor de teste.
Qual é a diferença entre "unable to get local issuer certificate" e "self-signed certificate in certificate chain"?
As duas significam que a cadeia termina em uma raiz em que a sua ferramenta não confia. Na primeira, a raiz não foi enviada e não foi encontrada localmente; na segunda, o servidor ou um proxy enviou a própria raiz. A correção é a mesma.
Onde consigo o certificado raiz da minha empresa?
Com a equipe de TI ou de segurança; em um notebook gerenciado, ele já está no repositório do sistema, então truststore, --use-system-ca e Schannel funcionam sem arquivo. Nunca pegue uma raiz de uma fonte não oficial: uma raiz confiável pode assinar para qualquer domínio.
Por que o pip funciona quando o Requests falha na mesma máquina?
Desde a versão 24.2, o pip verifica os certificados do sistema além do certifi, enquanto o Requests lê só o certifi. O pacote truststore dá ao seu script o mesmo comportamento.
Um proxy pode causar "unable to get local issuer certificate"?
Só um que descriptografa o tráfego: um gateway de empresa, um antivírus com verificação de HTTPS ou um proxy de depuração. Um proxy de túnel que usa CONNECT repassa o certificado do próprio site sem alterações.
Em resumo
O erro significa que o seu cliente não conseguiu ligar o certificado do servidor a uma raiz em que confia. O openssl s_client mostra o porquê: uma folha sozinha aponta para o servidor, um emissor da empresa para a inspeção TLS, e uma cadeia pública limpa para o bundle próprio da sua ferramenta. Adicione a raiz certa onde essa ferramenta procura e deixe a verificação ligada. Para proxies que tunelam o tráfego sem tocar nos certificados, veja a nossa página de serviços de proxy.




