DevGuideTK
← Trilha Backend

TLS e certificados

Como duas máquinas que nunca se viram combinam uma chave secreta e como o certificado prova a identidade do servidor.

Os comandos e resultados desta página foram executados com o OpenSSL 3.5, numa autoridade certificadora de teste com raiz e intermediária, servida pelo openssl s_server e acessada pelo curl.

Inspecionar o certificado de um servidor

O openssl s_client mostra o que o servidor envia de verdade:

# A cadeia enviada e o resultado da verificação
openssl s_client -connect api.loja.com.br:443 -servername api.loja.com.br -showcerts </dev/null

# Só as datas, o assunto, o emissor e os nomes do certificado final
openssl s_client -connect api.loja.com.br:443 -servername api.loja.com.br </dev/null 2>/dev/null \
  | openssl x509 -noout -subject -issuer -dates -ext subjectAltName

O que procurar na saída:

ItemOndeO que conferir
A cadeiaCertificate chain, com uma linha por certificadoPelo menos o certificado do servidor e o intermediário
A verificaçãoVerify return code0 (ok); qualquer outro código diz o motivo
Os nomesX509v3 Subject Alternative NameO nome acessado precisa estar na lista
A validadenotAfterQuantos dias faltam
A versãoProtocolTLS 1.2 ou 1.3

A opção -servername é importante: um mesmo endereço IP costuma servir vários domínios, e o servidor escolhe o certificado pelo nome enviado na negociação (o SNI). Sem ela, o servidor pode responder com o certificado errado.

Confiar numa autoridade privada, sem desligar a verificação

Serviços internos costumam usar certificados de uma autoridade privada, que não está entre as raízes do sistema. No nosso teste, a chamada com a cadeia completa e o nome certo falhou com unable to get local issuer certificate até o cliente receber a raiz da autoridade de teste.

A correção é acrescentar a raiz da autoridade privada à lista de confiança do cliente, mantendo toda a verificação ligada:

import fs from "node:fs";
import https from "node:https";
import tls from "node:tls";

// A raiz da autoridade interna, além das raízes padrão
const agente = new https.Agent({
  ca: [...tls.rootCertificates, fs.readFileSync("raiz-interna.crt", "utf8")],
  minVersion: "TLSv1.2",
});

https.get("https://estoque.interno:8443/saude", { agent: agente }, (resposta) => {
  console.log(resposta.statusCode);
  resposta.resume();
});

// Alternativa sem mudar o código: NODE_EXTRA_CA_CERTS=raiz-interna.crt node app.js

O que muda de uma linguagem para outra

Raízes padrãoComo acrescentar uma raizVariável de ambiente
NodeEmbutidas no Node, não as do sistemaOpção ca no agenteNODE_EXTRA_CA_CERTS
PythonAs do sistema (ssl); o requests usa as do pacote certifiload_verify_locations ou verify=SSL_CERT_FILE, REQUESTS_CA_BUNDLE
GoAs do sistemaRootCAs com o conjunto do sistema mais a raizSSL_CERT_FILE no Linux
JavaO repositório cacerts da JVMUm KeyStore próprio no SSLContext, ou importar no cacerts com keytool-Djavax.net.ssl.trustStore
C#As do sistemaCertificateChainPolicy com CustomRootTrustNenhuma padrão

Dois detalhes desta tabela causam muitos chamados:

  • Node e Python com requests não usam as raízes do sistema por padrão. Uma raiz corporativa instalada no sistema operacional funciona no curl e falha no Node ou no requests. Versões recentes do Node oferecem a opção --use-system-ca, sugerida na própria mensagem de erro.
  • O Python 3.13 é mais exigente com os certificados das autoridades. Autoridades internas antigas, criadas sem as extensões basicConstraints e keyUsage, passam no curl e falham no Python. A correção é reemitir a autoridade com as extensões, e não desligar a verificação estrita.
  • No Java e no C#, os exemplos confiam só na raiz interna, o que é o mais seguro para um cliente que fala apenas com serviços internos. Para falar também com serviços públicos, o cliente precisa das raízes padrão junto.

Em containers, a forma mais simples costuma ser acrescentar a raiz interna ao repositório do sistema na imagem (update-ca-certificates em Debian e Alpine), e configurar as variáveis de ambiente para os runtimes que não usam o sistema.

Montar uma autoridade de teste

Para testar TLS localmente com uma cadeia realista, sem desligar a verificação:

# Raiz, marcada como autoridade e com o uso da chave declarado
openssl req -x509 -newkey ec -pkeyopt ec_paramgen_curve:P-256 -nodes \
  -keyout raiz.key -out raiz.crt -days 3650 -subj "/CN=Raiz de Teste" \
  -addext "basicConstraints=critical,CA:TRUE" \
  -addext "keyUsage=critical,keyCertSign,cRLSign"

# Intermediária, assinada pela raiz
openssl req -newkey ec -pkeyopt ec_paramgen_curve:P-256 -nodes \
  -keyout inter.key -out inter.csr -subj "/CN=Intermediaria de Teste"
printf "basicConstraints=critical,CA:TRUE,pathlen:0\nkeyUsage=critical,keyCertSign,cRLSign\n" > inter.ext
openssl x509 -req -in inter.csr -CA raiz.crt -CAkey raiz.key -CAcreateserial \
  -out inter.crt -days 1825 -extfile inter.ext

# Certificado do servidor, assinado pela intermediária
openssl req -newkey ec -pkeyopt ec_paramgen_curve:P-256 -nodes \
  -keyout servidor.key -out servidor.csr -subj "/CN=api.loja.test"
printf "subjectAltName=DNS:api.loja.test\nextendedKeyUsage=serverAuth\n" > servidor.ext
openssl x509 -req -in servidor.csr -CA inter.crt -CAkey inter.key -CAcreateserial \
  -out servidor.crt -days 90 -extfile servidor.ext

# Servir e testar
openssl s_server -accept 8443 -cert servidor.crt -key servidor.key -cert_chain inter.crt -www &
curl --cacert raiz.crt --resolve api.loja.test:8443:127.0.0.1 https://api.loja.test:8443/

Quatro detalhes que fizeram diferença no nosso teste:

  • A raiz precisa declarar o uso da chave. Na primeira versão do teste, a raiz foi criada sem a extensão keyUsage. O curl, o Node e o Go aceitaram; o Python 3.13 recusou com CA cert does not include key usage extension, porque a partir dessa versão o ssl.create_default_context() liga a verificação estrita de certificados. Com as duas opções -addext acima, os três clientes aceitaram.
  • O nome vai no subjectAltName, e não só no CN. Os clientes atuais conferem o nome apenas nos nomes alternativos.
  • O servidor precisa enviar o intermediário. Com um arquivo que junta servidor e intermediário, o s_server mandou só o primeiro certificado, e a verificação falhou. Com -cert_chain, passou. Servidores como Nginx e Caddy aceitam o arquivo com a cadeia inteira (fullchain), na ordem servidor e depois intermediário.
  • --resolve faz o curl acessar api.loja.test no endereço local sem mexer no arquivo de hosts, mantendo o nome correto para a verificação.

Para testar o comportamento com um certificado vencido, o OpenSSL 3.4 ou mais novo aceita datas explícitas: -not_before 20240101000000Z -not_after 20240401000000Z. Para o dia a dia, ferramentas como mkcert criam uma autoridade local e certificados de desenvolvimento com um comando.

Monitorar o vencimento

Um teste simples, que pode rodar num agendador ou na integração contínua:

# Sai com erro se o certificado vencer nos próximos 14 dias (1.209.600 segundos)
openssl s_client -connect api.loja.com.br:443 -servername api.loja.com.br </dev/null 2>/dev/null \
  | openssl x509 -noout -checkend 1209600

Ferramentas de monitoramento, como Prometheus com o blackbox_exporter, expõem a data de vencimento de cada certificado como métrica, para alertar sem scripts.

Lista de revisão

Clientes

  • A verificação de certificado está ligada em todos os clientes, inclusive em homologação?
  • Autoridades privadas foram acrescentadas à lista de confiança, em vez de a verificação ser desligada?
  • A conexão com o banco usa verify-full ou equivalente quando atravessa redes não controladas?
  • A versão mínima é TLS 1.2?

Servidores

  • O servidor envia a cadeia completa, com o intermediário?
  • Os nomes acessados estão no subjectAltName?
  • A configuração segue um perfil pronto, com TLS 1.2 e 1.3?
  • Há redirecionamento de HTTP para HTTPS e o cabeçalho HSTS?

Operação

  • Os certificados são renovados automaticamente?
  • Há alerta antes do vencimento, independente da renovação automática?
  • As chaves privadas ficam fora do repositório e das imagens?

Prompts para aprofundar

Ao abrir no ChatGPT ou no Claude, o prompt é enviado ao serviço escolhido pela URL e também fica copiado na sua área de transferência.

  • Montar uma autoridade de teste e quebrar a verificação de propósito

    Quero montar uma autoridade certificadora de teste e reproduzir os erros de certificado mais comuns.
    
    Vamos em etapas, com comandos do OpenSSL num container:
    1. Criar uma raiz, uma intermediária e um certificado de servidor com o nome nos nomes alternativos.
    2. Servir com openssl s_server e acessar com curl, confiando só na raiz de teste.
    3. Remover o intermediário do que o servidor envia e ver o erro.
    4. Servir um certificado de outro nome e ver o erro.
    5. Emitir um certificado vencido e ver o erro.
    6. Servir um certificado autoassinado e ver o erro.
    7. Medir o tempo da negociação com TLS 1.2 e TLS 1.3, com latência simulada pelo tc netem.
    
    Em cada etapa, peça que eu preveja a mensagem de erro antes de rodar.
  • Confiar numa autoridade interna sem desligar a verificação

    Meus serviços chamam APIs internas com certificados de uma autoridade privada, e as chamadas falham com erro de certificado. Quero corrigir do jeito certo.
    
    Pergunte qual linguagem, bibliotecas de cliente e forma de implantação eu uso (containers, máquinas virtuais).
    
    Explique, com código e configuração para o meu caso:
    1. Por que a chamada falha e por que desligar a verificação não é uma correção.
    2. Quais raízes o meu runtime usa por padrão.
    3. Como acrescentar a raiz interna mantendo as raízes públicas, no código e por variáveis de ambiente.
    4. Como acrescentar a raiz à imagem do container.
    5. Como conferir que a verificação de nome e validade continua ligada.
    
    Termine com um teste automatizado que falha se alguém desligar a verificação.
  • Revisar configurações de TLS

    Vou colar abaixo código e configurações que envolvem TLS: clientes HTTP, conexões com banco e filas, configuração de servidor ou proxy. Revise com foco em segurança.
    
    Verifique, e para cada item diga se está correto, incorreto ou ausente, citando o trecho:
    1. Alguma verificação de certificado está desligada (verify=False, rejectUnauthorized: false, InsecureSkipVerify, aceitar todos os certificados)?
    2. A conexão com o banco usa verificação completa do certificado e do nome?
    3. A versão mínima é TLS 1.2?
    4. O servidor envia a cadeia completa?
    5. Há redirecionamento para HTTPS e HSTS?
    6. Há renovação automática e alerta de vencimento?
    7. Alguma chave privada está no repositório ou na imagem?
    
    Para cada problema, descreva o ataque ou a falha possível e mostre a correção. Não reescreva o que já está correto.
    
    Código e configurações:
    [cole aqui]