Corrigindo "certificate verify failed": Certificados CA e Verificacao SSL
O que voce vai aprender
- A causa real por tras de "certificate verify failed" e "unable to get local issuer certificate"
- Como fazer a triagem de qual dos tres grupos esta com problema usando
openssl s_client - Como corrigir CA bundles, cadeias incompletas e desvio de relogio da forma correta
Triagem rapida
A causa e um dos tres grupos:
- Lado do cliente: CA bundle esta desatualizado ou ausente ->
update-ca-certificates - Lado do servidor: certificado intermediario nao enviado (cadeia incompleta) -> servir a fullchain
- Ambiente: relogio do sistema esta errado, entao as datas de validade falham -> sincronizar com
timedatectl
O ponto de partida e o Verify return code do openssl s_client.
Premissas
- SO: Ubuntu / familia Debian (traduza os caminhos para familia RHEL; coberto abaixo)
- Um cliente que verifica TLS (curl / wget / Python / git) esta lancando o erro
O que significa "certificate verify failed"?
Conclusao: O cliente nao conseguiu encadear o certificado do servidor ate uma CA raiz confiavel. O certificado raramente e falso; geralmente o material de verificacao esta incompleto.
Em um handshake TLS, o cliente encadeia o certificado do servidor ate uma CA raiz para verifica-lo. Se essa cadeia nao se conecta, ou as datas de validade ou o hostname nao correspondem, a verificacao falha.
A mensagem difere por ferramenta, mas a falha subjacente e 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 nao pode ser encontrado. Isso aponta fortemente para uma cadeia incompleta ou um CA bundle ausente.
Como faco a triagem da causa?
Conclusao: Use
openssl s_clientpara ver a cadeia real e oVerify return code. O numero mapeia unicamente para qual grupo esta 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 (necessario em configuracoes de virtual host). Verifique o Verify return code no final.
| Codigo de retorno | Significado | Causa provavel |
|---|---|---|
0 (ok) |
Verificacao passou | CA bundle especifico do cliente (veja abaixo) |
20 (unable to get local issuer certificate) |
Emissor nao encontrado | CA bundle ausente |
21 (unable to verify the first certificate) |
Cadeia nao conecta | Servidor omite o certificado intermediario |
10 (certificate has expired) |
Expirado | Expiracao real ou desvio de relogio |
9 (certificate is not yet valid) |
Ainda nao valido | Quase sempre desvio de relogio |
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 repositorio CA do SO esta correto e um bundle especifico do cliente (como o certifi do Python, abaixo) e o culpado. Este e o ponto de ramificacao chave.
Voce tambem 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 repositorio CA esta desatualizado ou ausente -- e agora?
Conclusao: Atualize o CA bundle do cliente. Na familia Debian, instale
ca-certificatese executeupdate-ca-certificates.
Se voce ve Verify return code: 20 e o certificado do servidor e de uma CA publica real, seu repositorio de confianca local esta 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 (extensao .crt) no diretorio 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 extensao .crt e formato PEM. Extensao .pem ou formato DER sao ignorados. Converta DER com openssl x509 -inform der -in ca.der -out ca.crt.
Familia RHEL / CentOS / Fedora
O diretorio e o comando diferem.
sudo cp company-root-ca.crt /etc/pki/ca-trust/source/anchors/ sudo update-ca-trust
A cadeia do servidor esta incompleta -- como corrigir?
Conclusao:
Verify return code: 21e uma misconfiguracao do servidor. A correcao adequada e servir a fullchain, incluindo o certificado intermediario -- nao contornar no cliente.
Se a Certificate chain na saida do s_client lista apenas o certificado do servidor (sem CA intermediaria), o servidor nao esta enviando o certificado intermediario. Muitos navegadores fazem cache ou buscam o intermediario, e por isso "funciona no navegador mas falha no curl."
A correcao adequada e servir a fullchain no servidor.
- Nginx: aponte
ssl_certificatepara umfullchain.pem(certificado do servidor + intermediario concatenados) - Apache: aponte
SSLCertificateFilepara a fullchain (ou useSSLCertificateChainFilepara o intermediario) - Let's Encrypt (certbot): use
fullchain.pem, naocert.pem
Verifique a partir de um verificador externo (como SSL Labs) ou openssl s_client de outro host -- nao do navegador.
Mesmo quando voce nao pode corrigir o servidor, nao desabilite a verificacao (-k / verify=False) e deixe assim. Voce perde a capacidade de detectar um man-in-the-middle. Use apenas para triagem pontual.
O relogio do sistema esta errado -- isso pode ser a causa?
Conclusao: Se voce recebe
certificate has expired/not yet validmas o certificado e realmente valido, suspeite do relogio do sistema. Verifique o estado de sincronizacao comtimedatectl.
A janela de validade de um certificado (notBefore / notAfter) e verificada contra o relogio do sistema. Em containers ou VMs recem-retomadas com relogio muito desviado, um certificado valido 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 voce ve System clock synchronized: no ou uma data obviamente errada, corrija o NTP.
sudo timedatectl set-ntp true
Para o fluxo completo de sincronizacao de relogio, veja Corrigindo desvio de horario do servidor.
Certificados autoassinados ou expirados
Conclusao: Para certificados autoassinados (codigo de retorno 19/18), adicione essa CA ao repositorio de confianca explicitamente. Limite o bypass de verificacao a triagem pontual.
Ambientes de desenvolvimento e proxies corporativos usam certificados autoassinados. A abordagem adequada e adicionar esse certificado (ou sua CA emissora) ao repositorio de confianca usando os passos de update-ca-certificates acima.
Apenas quando voce precisa pular a verificacao 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 nao impersonacao (MITM). Nunca use para trafego de producao ou ao enviar credenciais.
Correcoes especificas por ferramenta (curl / Python / git)
Conclusao: Quando o CA do SO esta correto mas apenas Python falha, o bundle separado do certifi e a causa. Cada ferramenta le um repositorio CA diferente.
curl / wget
Estes leem o repositorio CA do SO. Para apontar para um CA especifico temporariamente:
curl --cacert /path/to/ca.pem https://example.com wget --ca-certificate=/path/to/ca.pem https://example.com
Python (requests / urllib)
requests le seu repositorio certifi embutido, nao o repositorio do SO. Este e o caso classico em que atualizar o CA do SO nao ajuda.
# Mostrar qual CA bundle o requests esta usando python3 -c "import certifi; print(certifi.where())"
Para forcar um CA especifico, defina variaveis 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 e lido pelo modulo ssl padrao do Python (OpenSSL); REQUESTS_CA_BUNDLE e especifico 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 verificacao completamente.
O que nao fazer
Conclusao: Desabilitar permanentemente a verificacao, confiar em massa em CAs desconhecidas, ou fixar o relogio manualmente sao correcoes "faz funcionar" que se tornam incidentes depois.
Evite estes
- Codificar
curl -k/verify=False/sslVerify falseem arquivos de configuracao ou scripts - Adicionar uma CA nao confiavel ao repositorio de confianca sem saber a causa
- Fixar um relogio desviado com
date -se parar o NTP (recorrencia e inconsistencia de logs) - Contornar um bug de cadeia do servidor em cada cliente (a correcao pertence a um servidor)