Corrigindo "certificate verify failed": Certificados CA e Verificação SSL

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:

  1. Lado do cliente: CA bundle está desatualizado ou ausente -> update-ca-certificates
  2. Lado do servidor: certificado intermediário não enviado (cadeia incompleta) -> servir a fullchain
  3. 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_client para ver a cadeia real e o Verify 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-certificates e execute update-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_certificate para um fullchain.pem (certificado do servidor + intermediário concatenados)
  • Apache: aponte SSLCertificateFile para a fullchain (ou use SSLCertificateChainFile para o intermediário)
  • Let's Encrypt (certbot): use fullchain.pem, não cert.pem

Verifique a partir de um verificador externo (como SSL Labs) ou openssl s_client de outro host -- não do navegador.

O relógio do sistema está errado -- isso pode ser a causa?

Conclusão: Se você recebe certificate has expired / not yet valid mas o certificado é realmente válido, suspeite do relógio do sistema. Verifique o estado de sincronização com timedatectl.

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.

Resumo e próximas leituras

  • A causa é um dos três grupos -- CA do cliente / cadeia do servidor / relógio. O Verify return code do openssl s_client mapeia para cada um unicamente

  • Corrija o cliente com update-ca-certificates, o servidor servindo a fullchain, e o relógio com timedatectl

  • Lembre-se que ferramentas leem repositórios diferentes: Python usa certifi, git usa http.sslCAInfo

  • Bypass de verificação (-k, etc.) é apenas para triagem pontual -- nunca permanente

  • Diagnosticando "Connection refused"

  • Troubleshooting de resolução DNS

  • Corrigindo desvio de horário do servidor