Corrigindo "certificate verify failed": Certificados CA e Verificação SSL
O que você vai aprender
- A causa real por trás de "certificate verify failed" e "unable to get local issuer certificate"
- Como fazer a triagem de qual dos três grupos está com problema usando
openssl s_client - Como corrigir CA bundles, cadeias incompletas e desvio de relógio da forma correta
Triagem rápida
A causa é um dos três grupos:
- Lado do cliente: CA bundle está desatualizado ou ausente ->
update-ca-certificates - Lado do servidor: certificado intermediário não enviado (cadeia incompleta) -> servir a fullchain
- Ambiente: relógio do sistema está errado, então as datas de validade falham -> sincronizar com
timedatectl
O ponto de partida é o Verify return code do openssl s_client.
Premissas
- SO: Ubuntu / família Debian (traduza os caminhos para família RHEL; coberto abaixo)
- Um cliente que verifica TLS (curl / wget / Python / git) está lancando o erro
O que significa "certificate verify failed"?
Conclusão: O cliente não conseguiu encadear o certificado do servidor até uma CA raiz confiável. O certificado raramente é falso; geralmente o material de verificação está incompleto.
Em um handshake TLS, o cliente encadeia o certificado do servidor até uma CA raiz para verificá-lo. Se essa cadeia não se conecta, ou as datas de validade ou o hostname não correspondem, a verificação falha.
A mensagem difere por ferramenta, mas a falha subjacente é a mesma.
curl: (60) SSL certificate problem: unable to get local issuer certificate
ssl.SSLCertVerificationError: [SSL: CERTIFICATE_VERIFY_FAILED] certificate verify failed: unable to get local issuer certificate (_ssl.c:1006)
unable to get local issuer certificate significa que o certificado do emissor não pode ser encontrado. Isso aponta fortemente para uma cadeia incompleta ou um CA bundle ausente.
Como faço a triagem da causa?
Conclusão: Use
openssl s_clientpara ver a cadeia real e oVerify return code. O número mapeia unicamente para qual grupo está quebrado.
Antes do navegador ou curl, observe o handshake TLS bruto.
openssl s_client -connect example.com:443 -servername example.com
-servername define o SNI (necessário em configurações de virtual host). Verifique o Verify return code no final.
| Código de retorno | Significado | Causa provável |
|---|---|---|
0 (ok) |
Verificação passou | CA bundle específico do cliente (veja abaixo) |
20 (unable to get local issuer certificate) |
Emissor não encontrado | CA bundle ausente |
21 (unable to verify the first certificate) |
Cadeia não conecta | Servidor omite o certificado intermediário |
10 (certificate has expired) |
Expirado | Expiração real ou desvio de relógio |
9 (certificate is not yet valid) |
Ainda não válido | Quase sempre desvio de relógio |
19 (self signed certificate in certificate chain) |
Autoassinado | CA interna / proxy |
Se openssl s_client retorna 0 (ok) mas apenas curl ou Python falha, o repositório CA do SO está correto e um bundle específico do cliente (como o certifi do Python, abaixo) é o culpado. Este é o ponto de ramificação chave.
Você também pode extrair os detalhes do certificado folha:
openssl s_client -connect example.com:443 -servername example.com -showcerts </dev/null 2>/dev/null \ | openssl x509 -noout -dates -subject -issuer
notBefore=Apr 1 00:00:00 2026 GMT notAfter=Jun 30 23:59:59 2026 GMT subject=CN=example.com issuer=CN=Example Intermediate CA
O repositório CA está desatualizado ou ausente -- e agora?
Conclusão: Atualize o CA bundle do cliente. Na família Debian, instale
ca-certificatese executeupdate-ca-certificates.
Se você vê Verify return code: 20 e o certificado do servidor é de uma CA pública real, seu repositório de confiança local está desatualizado.
sudo apt update sudo apt install --reinstall ca-certificates sudo update-ca-certificates
Updating certificates in /etc/ssl/certs... 3 added, 0 removed; done.
Para confiar em uma raiz personalizada (como uma CA interna), coloque o arquivo PEM (extensão .crt) no diretório correto e regenere.
sudo cp company-root-ca.crt /usr/local/share/ca-certificates/ sudo update-ca-certificates
Arquivos em /usr/local/share/ca-certificates/ devem usar a extensão .crt e formato PEM. Extensão .pem ou formato DER são ignorados. Converta DER com openssl x509 -inform der -in ca.der -out ca.crt.
Família RHEL / CentOS / Fedora
O diretório e o comando diferem.
sudo cp company-root-ca.crt /etc/pki/ca-trust/source/anchors/ sudo update-ca-trust
A cadeia do servidor está incompleta -- como corrigir?
Conclusão:
Verify return code: 21é uma misconfiguracao do servidor. A correção adequada é servir a fullchain, incluindo o certificado intermediário -- não contornar no cliente.
Se a Certificate chain na saída do s_client lista apenas o certificado do servidor (sem CA intermediária), o servidor não está enviando o certificado intermediário. Muitos navegadores fazem cache ou buscam o intermediário, e por isso "funciona no navegador mas falha no curl."
A correção adequada é servir a fullchain no servidor.
- Nginx: aponte
ssl_certificatepara umfullchain.pem(certificado do servidor + intermediário concatenados) - Apache: aponte
SSLCertificateFilepara a fullchain (ou useSSLCertificateChainFilepara o intermediário) - Let's Encrypt (certbot): use
fullchain.pem, nãocert.pem
Verifique a partir de um verificador externo (como SSL Labs) ou openssl s_client de outro host -- não do navegador.
Mesmo quando você não pode corrigir o servidor, não desabilite a verificação (-k / verify=False) e deixe assim. Você perde a capacidade de detectar um man-in-the-middle. Use apenas para triagem pontual.
O relógio do sistema está errado -- isso pode ser a causa?
Conclusão: Se você recebe
certificate has expired/not yet validmas o certificado é realmente válido, suspeite do relógio do sistema. Verifique o estado de sincronização comtimedatectl.
A janela de validade de um certificado (notBefore / notAfter) é verificada contra o relógio do sistema. Em containers ou VMs recem-retomadas com relógio muito desviado, um certificado válido pode ser lido como expired ou not yet valid.
timedatectl
Local time: Fri 2026-06-05 12:00:00 UTC
Universal time: Fri 2026-06-05 12:00:00 UTC
System clock synchronized: yes
NTP service: active
Se você vê System clock synchronized: no ou uma data obviamente errada, corrija o NTP.
sudo timedatectl set-ntp true
Para o fluxo completo de sincronização de relógio, veja Corrigindo desvio de horário do servidor.
Certificados autoassinados ou expirados
Conclusão: Para certificados autoassinados (código de retorno 19/18), adicione essa CA ao repositório de confiança explicitamente. Limite o bypass de verificação a triagem pontual.
Ambientes de desenvolvimento e proxies corporativos usam certificados autoassinados. A abordagem adequada é adicionar esse certificado (ou sua CA emissora) ao repositório de confiança usando os passos de update-ca-certificates acima.
Apenas quando você precisa pular a verificação temporariamente, e entende o raio de impacto:
# Apenas triagem pontual. Nunca torne permanente. curl -v https://internal.example.com # veja a causa primeiro curl -k https://internal.example.com # pular verificacao (arriscado)
-k (--insecure) e verify=False manteem a criptografia mas param de verificar a identidade do par. Impedem espionagem mas não impersonacao (MITM). Nunca use para tráfego de produção ou ao enviar credenciais.
Correções específicas por ferramenta (curl / Python / git)
Conclusão: Quando o CA do SO está correto mas apenas Python falha, o bundle separado do certifi é a causa. Cada ferramenta lê um repositório CA diferente.
curl / wget
Estes leem o repositório CA do SO. Para apontar para um CA específico temporariamente:
curl --cacert /path/to/ca.pem https://example.com wget --ca-certificate=/path/to/ca.pem https://example.com
Python (requests / urllib)
requests lê seu repositório certifi embutido, não o repositório do SO. Este é o caso clássico em que atualizar o CA do SO não ajuda.
# Mostrar qual CA bundle o requests esta usando python3 -c "import certifi; print(certifi.where())"
Para forçar um CA específico, defina variáveis de ambiente:
export REQUESTS_CA_BUNDLE=/etc/ssl/certs/ca-certificates.crt export SSL_CERT_FILE=/etc/ssl/certs/ca-certificates.crt
SSL_CERT_FILE é lido pelo módulo ssl padrão do Python (OpenSSL); REQUESTS_CA_BUNDLE é específico do requests. Apontar ambos para o bundle do SO faz o Python se comportar como o CA do sistema.
git
# Definir o CA para um unico repositorio git config http.sslCAInfo /path/to/ca.pem # Ou via variavel de ambiente export GIT_SSL_CAINFO=/path/to/ca.pem
Evite git config --global http.sslVerify false -- isso desabilita a verificação completamente.
O que não fazer
Conclusão: Desabilitar permanentemente a verificação, confiar em massa em CAs desconhecidas, ou fixar o relógio manualmente são correções "faz funcionar" que se tornam incidentes depois.
Evite estes
- Codificar
curl -k/verify=False/sslVerify falseem arquivos de configuração ou scripts - Adicionar uma CA não confiável ao repositório de confiança sem saber a causa
- Fixar um relógio desviado com
date -se parar o NTP (recorrência e inconsistência de logs) - Contornar um bug de cadeia do servidor em cada cliente (a correção pertence a um servidor)