Corrigindo "certificate verify failed": Certificados CA e Verificacao SSL

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:

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

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

O relogio do sistema esta errado -- isso pode ser a causa?

Conclusao: Se voce recebe certificate has expired / not yet valid mas o certificado e realmente valido, suspeite do relogio do sistema. Verifique o estado de sincronizacao com timedatectl.

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.

Resumo e proximas leituras

  • A causa e um dos tres grupos -- CA do cliente / cadeia do servidor / relogio. 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 relogio com timedatectl

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

  • Bypass de verificacao (-k, etc.) e apenas para triagem pontual -- nunca permanente

  • Diagnosticando "Connection refused"

  • Troubleshooting de resolucao DNS

  • Corrigindo desvio de horario do servidor