Unable to Get Local Issuer Certificate: como resolver

Publicado:

16 min de leitura

Acar Diveroli
Autor: Acar Diveroli
Pilha de cubos: cubo folha flutua sobre um vão tracejado e um cubo raiz de vidro azul; uma régua marca LEAF, MISSING, ROOT.

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?

  1. 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).
  2. O cliente verifica o certificado folha: o nome precisa corresponder ao host da URL, e as datas precisam ser válidas.
  3. O cliente encontra cada emissor entre os certificados enviados, confere a assinatura e repete um nível acima.
  4. O topo precisa terminar no repositório de confiança, assinado por uma raiz em que o cliente já confia.
  5. 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 enviouPython (ssl, Requests)Node.js e npmcurl e Git (OpenSSL)
Folha + intermediáriounable to get local issuer certificateUNABLE_TO_GET_ISSUER_CERT_LOCALLYunable to get local issuer certificate (20)
Só a folhaunable to get local issuer certificateUNABLE_TO_VERIFY_LEAF_SIGNATUREunable to get local issuer certificate (20)
Folha + intermediário + raizself-signed certificate in certificate chainSELF_SIGNED_CERT_IN_CHAINself-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:

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

Nosso servidor na porta 47444 envia só o certificado folha. As linhas 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: é 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):

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)

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:

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)')))

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:

python
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:

bash
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:

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

O 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:

bash
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:

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

Se 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:

text
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:

bash
git config --global http.sslBackend schannel

Se 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:

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

No 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:

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 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.exe do 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 em schannel: the revocation status is unknown; --ssl-revoke-best-effort aceita isso e continua verificando a cadeia.
  • --proxy-cacert verifica um proxy HTTPS, ou seja, um proxy acessado por uma URL de proxy https://. Um proxy http:// 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, o cafile do npm ou --cacert só 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_CERTS dentro do script. O Node.js só lê a variável na inicialização.
  • Publicar cert.pem em vez de fullchain.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úblicoO 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çãoPeça essa raiz à TI e adicione-a em cada ferramenta
Cadeia pública e 0 (ok), uma ferramenta falhaFaça a ferramenta ler o repositório do sistema: truststore, --use-system-ca, Schannel
npm falhaNODE_EXTRA_CA_CERTS; cafile só com um bundle completo
Git falha em um servidor internohttp.<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.