Introdução

Quando eu falo de certificados SSL/TLS em times de backend, quase sempre encontro o mesmo atalho perigoso: “não conecta no ambiente de teste, então coloca verify=False no Python ou callback => true no .NET e segue”. Esse atalho parece inofensivo porque resolve o erro de conexão na hora, mas ele remove justamente o mecanismo que impede ataques de man-in-the-middle (MITM). Em outras palavras: a aplicação continua “funcionando”, mas deixa de saber se está falando com o servidor legítimo.

Neste artigo, eu vou explicar como certificados SSL/TLS funcionam de forma prática e, principalmente, como eu valido certificado corretamente em aplicações .NET e Python em cenários reais de produção. Vou cobrir handshake TLS 1.3, cadeia de confiança X.509, trust store, mTLS, containers e o cenário que mais gera dúvida em times de backend: uma API chamando outra API por HTTPS com Load Balancer, NGINX, HAProxy, F5, Azure Application Gateway, AWS ALB ou Kubernetes Ingress no meio. A ideia é sair da teoria abstrata e ir para código e arquitetura que você pode colocar em produção sem vergonha de auditoria.

Quando uma API chama https://api2.interno.empresa.com, ela valida o certificado apresentado pelo endpoint HTTPS que recebeu a conexão. Em muitas arquiteturas, esse endpoint não é o container da API 2; é um Load Balancer ou Proxy Reverso que faz TLS Termination. Depois disso, o tráfego pode seguir para o backend por HTTP interno, HTTPS interno com nova validação, ou TLS passthrough até o próprio container. Entender onde o TLS termina evita uma confusão clássica: achar que a API cliente sempre valida o certificado do container.

Este é o quarto artigo da série Segurança para Devs. Nos artigos anteriores eu tratei de autenticação, BFF e API Gateway. Agora eu foco na camada de transporte, porque sem um TLS validado corretamente todo o resto fica frágil: JWT forte não salva um canal comprometido, OAuth2 correto não protege contra interceptação de tráfego e mTLS mal configurado pode virar teatro de segurança.

ℹ️ Informação: TLS 1.3 (RFC 8446) reduziu o handshake para 1-RTT no caso comum e removeu suites e algoritmos antigos inseguros. Isso melhora segurança e latência ao mesmo tempo.

Se você quer um resumo da tese logo no início: validar certificado não é opcional. É controle de integridade do canal. Quando eu desativo verificação para “fazer funcionar”, eu troco um erro explícito por um risco silencioso. O objetivo daqui é te mostrar como manter conexão segura sem cair nesses atalhos.

Pré-requisitos

Para acompanhar os exemplos sem fricção, eu recomendo este ambiente:

  • .NET 10 SDK instalado para compilar os samples C#.
  • Python 3.12+ com requests disponível.
  • Conhecimento básico de HTTP/HTTPS, status codes e cabeçalhos.
  • Familiaridade mínima com Docker e imagens Linux (Debian/Ubuntu) para a parte de container.

Também ajuda ter acesso a um endpoint HTTPS de teste com certificado próprio (CA interna) e, se quiser reproduzir mTLS, um certificado de cliente (.pfx no .NET ou par .crt/.key no Python).

O que é TLS/SSL e por que “SSL” é termo legado

No uso diário, eu ainda ouço “certificado SSL” o tempo todo, inclusive em documentação de produtos. Tecnicamente, porém, SSL (Secure Sockets Layer) é o nome histórico. SSL 2.0 e SSL 3.0 estão obsoletos há anos por problemas graves de segurança. O protocolo atual é TLS (Transport Layer Security), com foco moderno em TLS 1.2 e TLS 1.3.

Por que essa distinção importa? Porque linguagem molda decisão técnica. Quando eu trato TLS como “SSL”, eu costumo herdar mentalidade antiga: compatibilidade acima de segurança, cifra fraca para atender cliente legado, tolerância a certificados mal emitidos. Quando eu trato como TLS moderno, eu priorizo suites seguras, validação estrita e observabilidade de expiração/revogação.

Na prática, o certificado digital é um documento X.509 que vincula uma identidade (domínio, organização, serviço) a uma chave pública. O TLS usa esse certificado para estabelecer um canal criptografado e autenticado entre cliente e servidor. O cliente valida se o certificado é confiável, se o nome do host bate com SAN/CN e se a cadeia até uma CA raiz confiável fecha corretamente.

Eu gosto de resumir assim:

  • Criptografia protege confidencialidade do tráfego.
  • Integridade garante que o pacote não foi alterado.
  • Autenticidade confirma com quem eu estou falando.

Sem validação de certificado, eu mantenho criptografia, mas perco autenticidade. É exatamente esse o ponto que um atacante MITM explora.

⚠️ Atenção: Se o cliente aceita qualquer certificado, o TLS vira túnel criptografado para o atacante certo. Criptografado, porém comprometido.

Como funciona o handshake TLS 1.3

O handshake TLS 1.3 é o processo de negociação entre cliente e servidor antes dos dados de aplicação trafegarem. Eu acho útil enxergar esse fluxo em cinco blocos:

  1. ClientHello: o cliente envia versões de TLS suportadas, suites criptográficas, extensões (como SNI e ALPN) e chave efêmera para ECDHE.
  2. ServerHello: o servidor escolhe versão/suite, responde com sua chave efêmera e inicia parâmetros da sessão.
  3. Certificate + CertificateVerify: o servidor envia cadeia de certificados e prova posse da chave privada.
  4. Finished: ambos confirmam que chegaram às mesmas chaves de sessão.
  5. Application Data: tráfego HTTP passa a ser protegido pelo canal estabelecido.

Diagrama do handshake TLS 1.3 entre cliente e servidor com troca de ClientHello, ServerHello, Certificate e Finished

A mudança mais importante em relação a versões antigas é a redução de complexidade. TLS 1.3 removeu renegociação insegura, removeu RSA key exchange e tornou ECDHE o padrão para perfect forward secrecy (PFS). Em termos práticos, mesmo que a chave privada do servidor vaze no futuro, sessões antigas não são descriptografadas retroativamente.

Outro ponto que eu observo em produção: erro de handshake costuma apontar mais para configuração de certificado/trust store do que para bug de código. Por isso, log detalhado de falha TLS é essencial. Quando eu capturo hostname, issuer, thumbprint e status da cadeia, eu resolvo incidente muito mais rápido.

ℹ️ Informação: TLS 1.3 usa apenas suites AEAD modernas (como AES-GCM e ChaCha20-Poly1305), reduzindo superfície de ataque e simplificando hardening operacional.

Cadeia de confiança: CA raiz, intermediária e certificado de entidade final

Certificado não é uma entidade isolada. Ele participa de uma cadeia de confiança. Em geral, eu tenho:

  • Certificado de entidade final: pertence ao servidor (por exemplo api.minhaempresa.com).
  • CA intermediária: emite certificados de servidor e reduz exposição da raiz.
  • CA raiz: âncora de confiança presente no trust store do sistema cliente.

Quando o cliente recebe o certificado do servidor, ele tenta montar essa cadeia até uma raiz confiável localmente. Se a raiz não estiver no trust store, a validação falha com erro de untrusted root. Isso é comum em ambientes corporativos com PKI interna, laboratórios e ambientes air-gapped.

Em certificados públicos de internet, a cadeia normalmente termina em uma CA raiz já presente no trust store do sistema ou navegador. É o caso de certificados emitidos por ACs públicas (autoridades certificadoras) como Certisign, DigiCert, GlobalSign e Let’s Encrypt. Nesses cenários, eu não preciso distribuir CA interna para clientes externos; o foco passa a ser enviar a cadeia intermediária correta no servidor e manter renovação em dia.

No dia a dia, os problemas mais frequentes que eu vejo são:

  • Servidor sem enviar intermediária correta.
  • Certificado expirado ou fora da janela de validade.
  • SAN sem hostname esperado.
  • Cadeia confiável no host, mas não no container.

Esse último é traiçoeiro: no meu notebook funciona, no pod não. O motivo normalmente é simples: a imagem não recebeu a CA interna no trust store do sistema.

Quando eu trabalho com CA privada, minha abordagem é dupla:

  1. Instalo a CA no trust store do sistema no ambiente controlado.
  2. Quando necessário, uso trust customizado na aplicação para cenários isolados.

Essa segunda opção precisa de disciplina. Trust customizado é ferramenta boa quando bem delimitada, mas perigosa se virar regra geral sem governança.

Validando certificados corretamente em .NET

No .NET, minha referência para cliente HTTP moderno é HttpClient com SocketsHttpHandler. Ele permite configurar TLS sem truques inseguros. O ponto chave é: eu nunca retorno true de forma incondicional no callback.

Quando o endpoint usa certificado público emitido por AC pública confiável, o caminho padrão do .NET já resolve a validação usando o trust store do sistema. Eu só adiciono trust customizado quando tenho CA privada ou requisito de isolamento específico.

Trecho de sample com trust customizado e validação real da cadeia:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
public static SocketsHttpHandler CriarHandlerComCaPrivada(string caminhoCaRaiz)
{
    var caRaiz = X509CertificateLoader.LoadCertificateFromFile(caminhoCaRaiz);

    return new SocketsHttpHandler
    {
        SslOptions = new SslClientAuthenticationOptions
        {
            EnabledSslProtocols = SslProtocols.Tls12 | SslProtocols.Tls13,
            CertificateRevocationCheckMode = X509RevocationMode.Online,
            RemoteCertificateValidationCallback = (_, certificado, cadeia, erros) =>
                CertificadoTlsValidator.ValidarServidor(certificado, cadeia, erros, caRaiz)
        }
    };
}

📂 Código Fonte: O exemplo completo está disponível no repositório de exemplos do blog: BlogSamples/Security/Certificates/

No validator, eu recuso imediatamente RemoteCertificateNotAvailable e RemoteCertificateNameMismatch, depois reconstruo a cadeia com política estrita (NoFlag) e revogação online. Se houver CA personalizada, aplico TrustMode = CustomRootTrust e adiciono a raiz no CustomTrustStore.

Exemplo de criação de cliente mTLS:

1
2
3
4
5
6
7
var cliente = HttpsClientesSeguros.CriarClienteComMtls(
    caminhoCaRaiz: "/etc/ssl/custom/minha-ca.pem",
    caminhoPfxCliente: "/run/secrets/cliente.pfx",
    senhaPfxCliente: senha);

var response = await cliente.GetAsync("https://api.interna.local/health");
response.EnsureSuccessStatusCode();

📂 Código Fonte: O exemplo completo está disponível no repositório de exemplos do blog: BlogSamples/Security/Certificates/

Dois detalhes operacionais fazem diferença em produção:

  • Eu prefiro X509CertificateLoader em vez de construtores legados para evitar warnings e garantir compatibilidade atual.
  • Eu monitoro erros de cadeia e expiração em logs estruturados para acionar alerta antes da queda.

Validando certificados corretamente em Python

No Python, eu vejo o mesmo anti-pattern do .NET: verify=False para “destravar” integração. Em produção, isso não pode existir. O caminho correto com requests é informar o bundle de CA confiável em verify, ou manter o trust store padrão quando usa CA pública.

Exemplo seguro com requests:

1
2
3
4
def requisicao_https_com_ca_privada(url: str, ca_bundle: Path) -> requests.Response:
    response = requests.get(url, verify=str(ca_bundle), timeout=10)
    response.raise_for_status()
    return response

Se eu preciso de controle mais fino, crio SSLContext explícito:

1
2
3
4
5
6
7
def criar_contexto_ssl(ca_bundle: Path) -> ssl.SSLContext:
    context = ssl.create_default_context(purpose=ssl.Purpose.SERVER_AUTH)
    context.verify_mode = ssl.CERT_REQUIRED
    context.check_hostname = True
    context.minimum_version = ssl.TLSVersion.TLSv1_2
    context.load_verify_locations(cafile=str(ca_bundle))
    return context

📂 Código Fonte: O exemplo completo está disponível no repositório de exemplos do blog: BlogSamples/python/

Para mTLS no requests, eu passo par certificado/chave do cliente no parâmetro cert:

1
2
3
4
5
6
response = requests.get(
    url,
    verify=str(ca_bundle),
    cert=(str(client_cert), str(client_key)),
    timeout=10,
)

Essa abordagem me atende bem para automações, workers e integrações backend-to-backend. Em APIs sensíveis, eu combino mTLS com token de aplicação para defesa em camadas.

Para certificado público de servidor (por exemplo, domínio público com emissão por Certisign ou outra AC pública), o padrão costuma ser manter verify=True e usar o trust store nativo, sem CA bundle customizado. Eu só uso verify apontando arquivo quando preciso confiar em CA privada ou cadeia fora do padrão do sistema.

mTLS: quando o cliente também apresenta certificado

No TLS tradicional, só o servidor apresenta certificado. No mTLS (mutual TLS), cliente e servidor apresentam certificado. Eu uso isso quando quero identidade forte de serviço para serviço sem depender apenas de segredo estático.

Cenários comuns onde mTLS vale o custo:

  • Comunicação interna entre API Gateway e serviços críticos.
  • Integração B2B com parceiros fixos e requisitos de compliance.
  • Ambientes financeiros e industriais onde identidade criptográfica é obrigatória.

mTLS não substitui autorização de negócio. Ele prova identidade técnica do cliente, mas ainda preciso decidir se aquele cliente pode executar aquela ação. Na prática, eu trato mTLS como controle de borda e mantenho autorização por escopo/claims na aplicação.

Se você quiser contexto arquitetural da série, este artigo conversa diretamente com API Gateway: A Peça que Falta na Segurança da Sua SPA, onde eu mostro como gateway aplica políticas em escala.

Certificados em containers

Container muda bastante o jogo porque o trust store do host não é herdado automaticamente da forma que muita gente imagina. Cada imagem tem seu próprio conjunto de CAs. Quando eu recebo erro de TLS só no pod, quase sempre é CA ausente no filesystem da imagem.

No Debian/Ubuntu, o fluxo padrão é:

1
2
3
4
5
6
RUN apt-get update \
    && apt-get install -y --no-install-recommends ca-certificates \
    && rm -rf /var/lib/apt/lists/*

COPY certs/minha-ca.crt /usr/local/share/ca-certificates/minha-ca.crt
RUN update-ca-certificates

Com isso, bibliotecas que usam trust store do sistema passam a confiar na CA interna. Eu evito colocar certificado sensível em variável de ambiente. Para certificado de cliente e chave privada, eu monto secret em tmpfs (como /run/secrets) e limito permissão de leitura ao processo da aplicação.

Também tomo cuidado com rotação:

  • CA e certs com data de vencimento monitorada.
  • Pipeline de imagem preparado para troca sem downtime desnecessário.
  • Health checks que falham cedo quando cadeia não valida.

Em Kubernetes, isso normalmente significa combinar Secret/CSI driver com rollout controlado e observabilidade de handshake.

TLS Termination em Load Balancers e Reverse Proxies

Em arquitetura corporativa, a chamada raramente é tão direta quanto “API 1 abre TLS com o container da API 2”. O desenho mais comum é este:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
API 1
  |
  | HTTPS
  v
Load Balancer / Reverse Proxy
(certificado TLS apresentado aqui)
  |
  | HTTP
  v
Container API 2

Nesse modelo, a API 1 valida o certificado do Load Balancer ou Proxy Reverso, porque é ele que aparece como servidor TLS na conexão. Se o DNS api2.interno.empresa.com aponta para um NGINX, F5, HAProxy, Azure Application Gateway, AWS ALB ou Ingress Controller, é esse componente que apresenta o certificado para o cliente. O container da API 2 pode estar servindo HTTP puro em uma rede privada e, ainda assim, a chamada externa da API 1 continua sendo HTTPS.

O código da API cliente não precisa mudar por causa disso:

1
2
var response = await httpClient.GetAsync("https://api2.interno.empresa.com/health");
response.EnsureSuccessStatusCode();

Do ponto de vista do HttpClient, o fluxo é simples: resolver DNS, conectar no IP retornado, receber o certificado do servidor TLS, validar hostname/cadeia/validade e só então enviar HTTP dentro do túnel criptografado. O que acontece depois do Load Balancer é transparente para a API cliente.

ℹ️ Informação: Se a API cliente chama https://api2.interno.empresa.com, esse nome precisa estar no SAN do certificado apresentado pelo Load Balancer ou Proxy Reverso. O SAN do certificado do container só importa para quem abre TLS diretamente com o container.

A pergunta que costuma aparecer é: “se o certificado está no Load Balancer, por que a API precisa validá-lo?” A resposta é que, para a API cliente, o Load Balancer é o servidor TLS. A validação impede que a API envie dados para um endpoint que apenas parece ser api2.interno.empresa.com, mas não tem um certificado confiável para esse nome. TLS não autentica “o container” por definição; TLS autentica a outra ponta da conexão que foi aberta.

Quando o proxy reverso tem uma CA

A frase “o proxy reverso tem o certificado CA” pode significar três coisas diferentes, e cada uma cobre um cenário distinto:

O que o proxy possuiPara que serveCenário coberto
Certificado de servidor assinado por uma CAApresentar identidade HTTPS aos clientesTLS Termination no Load Balancer/Proxy
Certificado da CA confiávelValidar o certificado de um upstream HTTPSTLS Re-encryption entre proxy e backend
CA de clientes confiáveisValidar certificados apresentados por clientesmTLS na borda do proxy reverso

No primeiro caso, o proxy não “tem a CA” como chave de autoridade; ele tem um certificado de servidor emitido por uma CA pública ou privada. Esse é o caso mais comum de TLS Termination: API 1 valida o certificado do proxy, e o proxy encaminha para a API 2.

No segundo caso, o proxy tem o certificado público da CA no trust store dele para validar o backend. Aqui o proxy atua como cliente TLS quando chama https://api2-backend.interno.local. A API 1 valida o proxy; o proxy valida o backend. São duas sessões TLS independentes.

No terceiro caso, o proxy tem uma CA de clientes confiáveis para exigir mTLS na entrada. A API 1 apresenta um certificado de cliente, o proxy valida esse certificado contra a CA configurada e só então aceita a conexão. Isso autentica a API chamadora na borda, mas não substitui autorização de negócio dentro da aplicação.

⚠️ Atenção: A chave privada de uma CA não deve ficar em Load Balancer, NGINX ou container de aplicação. O proxy precisa do certificado de servidor e, quando necessário, do certificado público da CA para validação. A chave privada da CA pertence ao processo de emissão, não ao runtime da API.

TLS Termination, Re-encryption e Passthrough

Os três modelos abaixo resolvem problemas diferentes. Eu gosto de escolher explicitamente um deles em desenho de arquitetura, porque a frase “usa HTTPS” sozinha não diz onde o TLS começa, termina ou é reaberto.

ModeloFluxoQuem apresenta certificado ao cliente?Backend usa TLS?Quando usar
TLS TerminationCliente -> HTTPS -> LB/proxy -> HTTP -> APILB/proxyNãoRede privada controlada, menor complexidade operacional
TLS Re-encryptionCliente -> HTTPS -> LB/proxy -> HTTPS -> APILB/proxySimAmbientes regulados, rede interna compartilhada ou tráfego sensível
TLS PassthroughCliente -> HTTPS -> LB TCP -> APIAPI/backendSimQuando o LB não deve encerrar TLS nem inspecionar HTTP

Em TLS Termination, o Load Balancer encerra a sessão TLS e passa a enxergar a requisição HTTP. Isso permite roteamento por path, headers, cookies e políticas de WAF, mas exige confiança na rede entre proxy e backend. Em muitos ambientes internos, esse desenho é aceitável quando existe segmentação de rede, firewall, autenticação entre serviços e observabilidade.

Em TLS Re-encryption, o proxy encerra o TLS do cliente e abre uma nova conexão HTTPS para o backend. Esse desenho cria duas proteções independentes: cliente valida proxy, e proxy valida backend. É o modelo que eu prefiro quando o tráfego passa por rede compartilhada, Kubernetes multi-tenant, service mesh ou ambiente com requisito regulatório mais forte.

Em TLS Passthrough, o Load Balancer opera em camada TCP e encaminha a conexão TLS sem abrir o conteúdo HTTP. O certificado apresentado ao cliente é o certificado do backend, não do proxy. O ganho é manter TLS ponta a ponta até a aplicação; a perda é reduzir recursos de roteamento HTTP, inspeção e WAF na borda.

NGINX como ponto de terminação TLS

Quando eu uso NGINX como web server/reverse proxy, o ponto central é sempre o mesmo: o certificado apresentado ao cliente fica no bloco server HTTPS, via ssl_certificate e ssl_certificate_key.

Certificado público apresentado ao cliente

No cenário público (AC pública como Certisign, DigiCert, GlobalSign ou Let’s Encrypt), minha prática é configurar o NGINX com cadeia completa (fullchain) para evitar erro de cadeia incompleta no cliente:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
server {
  listen 443 ssl http2;
  server_name api.minhaempresa.com;

  ssl_certificate     /etc/nginx/tls/fullchain.pem;
  ssl_certificate_key /etc/nginx/tls/privkey.pem;

  ssl_protocols TLSv1.2 TLSv1.3;
  ssl_session_timeout 1d;
  ssl_session_cache shared:SSL:50m;

  location / {
    proxy_pass http://app:8080;
  }
}

Nesse exemplo, o NGINX faz TLS Termination. O cliente valida api.minhaempresa.com, recebe o certificado do NGINX e só depois a requisição segue para http://app:8080 dentro da rede interna.

mTLS entre cliente e NGINX

No cenário interno com CA privada, a configuração do certificado no NGINX é parecida, mas os clientes só vão confiar se a CA interna estiver no trust store deles. Se o endpoint exigir mTLS de cliente, eu habilito verificação explícita:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
server {
  listen 443 ssl http2;
  server_name api.interna.local;

  ssl_certificate     /etc/nginx/tls/api.interna.local.fullchain.pem;
  ssl_certificate_key /etc/nginx/tls/api.interna.local.key;

  ssl_client_certificate /etc/nginx/tls/ca-clientes-interna.crt;
  ssl_verify_client on;
  ssl_verify_depth 2;

  location / {
    proxy_pass http://app:8080;
  }
}

Aqui o NGINX usa ssl_client_certificate como lista de CAs confiáveis para validar certificados apresentados por clientes. Isso cobre o cenário em que a API 1 também precisa provar identidade por certificado antes de acessar a API 2.

NGINX validando certificado do upstream HTTPS

Quando o NGINX também atua como cliente TLS para um upstream HTTPS interno, eu ativo validação de certificado no proxy para não cair no anti-pattern de confiar cegamente:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
location /backend/ {
  proxy_pass https://backend.interno.local;

  proxy_ssl_server_name on;
  proxy_ssl_name backend.interno.local;

  proxy_ssl_trusted_certificate /etc/nginx/tls/ca-upstream-interna.crt;
  proxy_ssl_verify on;
  proxy_ssl_verify_depth 2;
}

Com isso, eu cubro os dois lados da segurança: o certificado que o NGINX apresenta para fora e a validação do certificado quando ele chama serviços HTTPS internos.

Exemplo Prático

Como exemplo prático, eu uso duas implementações de cliente e três topologias de infraestrutura para validar:

  1. .NET em src/BlogSamples/Security/Certificates/ com HttpClient seguro, trust customizado e mTLS.
  2. Python em python/ssl_validation_example.py com requests e SSLContext estrito.
  3. API chamando API por Load Balancer: a API 1 acessa https://api2.interno.empresa.com e valida o certificado apresentado pelo Load Balancer ou Reverse Proxy.
  4. TLS Re-encryption: o proxy encerra HTTPS na borda e abre uma nova conexão HTTPS validada até a API backend.
  5. TLS Passthrough: o proxy não encerra TLS; o certificado visto pelo cliente é o certificado do backend.

Fluxo que eu recomendo para validar ponta a ponta:

  1. Suba um endpoint HTTPS de teste com CA privada.
  2. Aponte um DNS interno para o Load Balancer ou NGINX.
  3. Execute cliente .NET com CriarClienteComCaPrivada chamando o DNS público do serviço, não o nome do container.
  4. Execute script Python passando TLS_CA_BUNDLE, TLS_CLIENT_CERT e TLS_CLIENT_KEY quando houver mTLS.
  5. Verifique logs de cadeia e hostname em ambos.
  6. Force erro (hostname errado ou cert expirado) para confirmar que a aplicação falha de forma segura.

📝 Exemplo: Se o endpoint for https://api.interna.local e o certificado tiver SAN apenas para api.dev.local, a conexão deve falhar por mismatch de nome. Esse é o comportamento correto e esperado.

Com esse fluxo, eu provo duas coisas para o time: que o caminho feliz funciona e que o caminho de falha falha do jeito certo. Segurança madura não é apenas “funciona quando tudo está bonito”; é previsível quando algo sai do esperado.

Dicas e Boas Práticas

  • Nunca desabilite validação (verify=False, CERT_NONE, callback => true). Quando eu removo validação, eu elimino autenticidade do canal e abro caminho para MITM. Em vez disso, eu corrijo trust store, SAN ou cadeia.

  • Prefira pin de CA/intermediária ao invés de pin de certificado de entidade final. Pin no leaf quebra com renovação frequente. Pin em CA bem governada reduz manutenção sem perder controle.

  • Monitore expiração e falhas de handshake como métrica de plataforma. Eu trato erro TLS como sinal operacional de alto valor. Alertar 30 dias antes da expiração evita incidente evitável.

  • Use mTLS em tráfego lateral sensível, especialmente entre gateway e backend. Isso reduz risco de chamada direta indevida a serviços internos. Ainda assim, mantenha autorização por claims/escopos no app.

  • Padronize instalação de CAs em imagens e ambientes locais. Eu documento o passo de trust store por distro e automatizo no Dockerfile. Isso elimina “na minha máquina funciona” de TLS.

  • Em domínio público, prefira AC pública bem suportada e automação de renovação. Isso reduz fricção de distribuição de confiança para clientes externos. Mesmo com AC pública, eu valido cadeia enviada pelo servidor e monitoro expiração para evitar indisponibilidade.

  • Garanta que o SAN corresponda ao DNS usado pelo cliente. Se a API chama api2.interno.empresa.com, esse nome precisa estar no certificado apresentado pelo Load Balancer ou Proxy Reverso. O certificado do backend só entra na validação quando existe TLS Re-encryption ou Passthrough.

  • Escolha explicitamente entre Termination, Re-encryption e Passthrough. TLS Termination simplifica operação, Re-encryption protege também o trecho interno, e Passthrough mantém o certificado no backend. Não trate todos como “HTTPS” genérico, porque cada modelo muda quem valida quem.

  • Não coloque chave privada de CA no proxy reverso. O proxy precisa da chave privada do certificado de servidor para apresentar sua identidade e dos certificados públicos das CAs que usa para validar clientes ou upstreams. A chave privada da CA deve ficar protegida no processo de emissão, fora do runtime.

  • Teste cenários negativos de forma intencional. Inclua casos com hostname inválido, cert expirado e cadeia incompleta no pipeline de qualidade. Se o cliente aceitar esses cenários, algo está errado.

Resumo Objetivo

  • TLS 1.3 — é o protocolo moderno de transporte seguro; SSL é legado obsoleto e não deve ser alvo de configuração nova.
  • Validação de certificado — precisa confirmar cadeia de confiança, hostname (SAN/CN), validade temporal e, quando aplicável, revogação.
  • TLS Termination — ocorre quando Load Balancer ou Reverse Proxy encerra HTTPS, apresenta o certificado ao cliente e encaminha a requisição ao backend por HTTP ou HTTPS.
  • TLS Re-encryption — cria duas sessões TLS independentes: cliente-proxy e proxy-backend; o cliente valida o proxy, e o proxy valida a API backend.
  • TLS Passthrough — mantém a sessão TLS até o backend; o certificado visto pelo cliente é o da aplicação, mas o proxy perde inspeção HTTP detalhada.
  • Clientes .NET e Python — devem validar certificados com SocketsHttpHandler, CustomRootTrust, verify, CERT_REQUIRED e check_hostname=True, sem bypass inseguro.
  • mTLS — adiciona autenticação do cliente por certificado e é especialmente útil em comunicação service-to-service crítica.
  • NGINX com HTTPS — usa ssl_certificate e ssl_certificate_key para apresentar identidade; ssl_client_certificate valida clientes mTLS e proxy_ssl_trusted_certificate valida upstreams.

Leia Também

Referências