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 subjectAltNameO que procurar na saída:
| Item | Onde | O que conferir |
|---|---|---|
| A cadeia | Certificate chain, com uma linha por certificado | Pelo menos o certificado do servidor e o intermediário |
| A verificação | Verify return code | 0 (ok); qualquer outro código diz o motivo |
| Os nomes | X509v3 Subject Alternative Name | O nome acessado precisa estar na lista |
| A validade | notAfter | Quantos dias faltam |
| A versão | Protocol | TLS 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.jsimport ssl
import urllib.request
# Contexto com as raízes padrão do sistema e a raiz da autoridade interna
contexto = ssl.create_default_context()
contexto.load_verify_locations(cafile="raiz-interna.crt")
contexto.minimum_version = ssl.TLSVersion.TLSv1_2
with urllib.request.urlopen("https://estoque.interno:8443/saude", context=contexto) as resposta:
print(resposta.status)
# Com a biblioteca requests: requests.get(url, verify="raiz-interna.crt")package main
import (
"crypto/tls"
"crypto/x509"
"fmt"
"log"
"net/http"
"os"
)
func main() {
raizes, err := x509.SystemCertPool()
if err != nil {
raizes = x509.NewCertPool()
}
pem, err := os.ReadFile("raiz-interna.crt")
if err != nil {
log.Fatal(err)
}
if !raizes.AppendCertsFromPEM(pem) {
log.Fatal("raiz interna inválida")
}
cliente := &http.Client{
Transport: &http.Transport{
TLSClientConfig: &tls.Config{
RootCAs: raizes,
MinVersion: tls.VersionTLS12,
},
},
}
resposta, err := cliente.Get("https://estoque.interno:8443/saude")
if err != nil {
log.Fatal(err)
}
defer resposta.Body.Close()
fmt.Println(resposta.StatusCode)
}import java.io.FileInputStream;
import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.security.KeyStore;
import java.security.cert.CertificateFactory;
import javax.net.ssl.SSLContext;
import javax.net.ssl.TrustManagerFactory;
public class ClienteInterno {
public static void main(String[] args) throws Exception {
// Um repositório de confiança só com a raiz interna
var confianca = KeyStore.getInstance(KeyStore.getDefaultType());
confianca.load(null, null);
try (var arquivo = new FileInputStream("raiz-interna.crt")) {
var raiz = CertificateFactory.getInstance("X.509").generateCertificate(arquivo);
confianca.setCertificateEntry("raiz-interna", raiz);
}
var fabrica = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm());
fabrica.init(confianca);
var contexto = SSLContext.getInstance("TLS");
contexto.init(null, fabrica.getTrustManagers(), null);
var cliente = HttpClient.newBuilder().sslContext(contexto).build();
var resposta = cliente.send(
HttpRequest.newBuilder(URI.create("https://estoque.interno:8443/saude")).build(),
HttpResponse.BodyHandlers.discarding());
System.out.println(resposta.statusCode());
}
}using System.Net.Security;
using System.Security.Cryptography.X509Certificates;
var raiz = X509Certificate2.CreateFromPem(File.ReadAllText("raiz-interna.crt"));
var manipulador = new SocketsHttpHandler
{
SslOptions = new SslClientAuthenticationOptions
{
// Confia apenas na raiz interna, com a verificação completa ligada
CertificateChainPolicy = new X509ChainPolicy
{
TrustMode = X509ChainTrustMode.CustomRootTrust,
CustomTrustStore = { raiz },
RevocationMode = X509RevocationMode.NoCheck,
},
},
};
using var cliente = new HttpClient(manipulador);
var resposta = await cliente.GetAsync("https://estoque.interno:8443/saude");
Console.WriteLine((int)resposta.StatusCode);O que muda de uma linguagem para outra
| Raízes padrão | Como acrescentar uma raiz | Variável de ambiente | |
|---|---|---|---|
| Node | Embutidas no Node, não as do sistema | Opção ca no agente | NODE_EXTRA_CA_CERTS |
| Python | As do sistema (ssl); o requests usa as do pacote certifi | load_verify_locations ou verify= | SSL_CERT_FILE, REQUESTS_CA_BUNDLE |
| Go | As do sistema | RootCAs com o conjunto do sistema mais a raiz | SSL_CERT_FILE no Linux |
| Java | O repositório cacerts da JVM | Um KeyStore próprio no SSLContext, ou importar no cacerts com keytool | -Djavax.net.ssl.trustStore |
| C# | As do sistema | CertificateChainPolicy com CustomRootTrust | Nenhuma padrão |
Dois detalhes desta tabela causam muitos chamados:
- Node e Python com
requestsnão usam as raízes do sistema por padrão. Uma raiz corporativa instalada no sistema operacional funciona nocurle falha no Node ou norequests. 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
basicConstraintsekeyUsage, passam nocurle 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. Ocurl, o Node e o Go aceitaram; o Python 3.13 recusou comCA cert does not include key usage extension, porque a partir dessa versão ossl.create_default_context()liga a verificação estrita de certificados. Com as duas opções-addextacima, os três clientes aceitaram. - O nome vai no
subjectAltName, e não só noCN. 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_servermandou 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. --resolvefaz ocurlacessarapi.loja.testno 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 1209600Ferramentas 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-fullou 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]