O que o TLS realmente protege em uma conexão HTTPS

Carregue uma página por http:// puro em uma rede que você não controla, um aeroporto ou o roteador de uma cafeteria, e tudo o que você envia viaja como texto legível. O caminho da URL, os cookies, os campos do formulário, a senha em um POST de login: qualquer pessoa nessa rede com um sniffer de pacotes lê tudo isso ao passar. TLS, Transport Layer Security, é o protocolo que impede isso. Ele fica entre o TCP e o protocolo de aplicação, então quando o HTTP começa a falar, a conexão já está criptografada e vinculada a uma identidade de servidor verificada. HTTPS é o HTTP rodando sobre essa conexão em vez de uma em texto plano.

O TLS reúne três garantias, e elas só valem juntas. Criptografia significa que um observador na rede vê apenas texto cifrado. Integridade significa que um bit alterado em trânsito é detectado e a conexão cai em vez de entregar dados adulterados. Autenticação significa que o cliente consegue confirmar que chegou a example.com, e não a quem respondeu primeiro naquele endereço IP. Perca uma das três e as outras duas deixam de valer muita coisa: um canal criptografado até um impostor não é um canal seguro.

Como o handshake TLS estabelece um canal criptografado

Antes de qualquer requisição HTTP sair, cliente e servidor executam um handshake que combina uma chave secreta compartilhada sem nunca colocar essa chave na rede. O TLS 1.3, a versão atual e a que você deveria estar usando, faz isso em uma única viagem de ida e volta, onde o TLS 1.2 precisava de duas.

1
2
3
4
5
6
7
8
9
Client                                           Server
  |--- ClientHello + key_share ------------------->|
  |    (versões suportadas, cipher suites,         |
  |     um palpite do grupo de troca de chaves)     |
  |<-- ServerHello + key_share --------------------|
  |<-- EncryptedExtensions, Certificate,           |
  |    CertificateVerify, Finished ----------------|
  |--- Finished ---------------------------------->|
  |======== dados de aplicação, criptografados ====|

O cliente envia um ClientHello: as versões de TLS e as cipher suites que suporta, um nonce aleatório, e um palpite sobre qual grupo de troca de chaves o servidor vai escolher, enviado como key_share na mesma mensagem. Esse palpite é o atalho do TLS 1.3. O servidor responde com seu próprio key_share no ServerHello, e os dois lados executam uma troca Diffie-Hellman sobre esses shares para derivar de forma independente a mesma chave de sessão simétrica. A chave em si nunca atravessa a rede.

A partir desse ponto, tudo o mais já vai criptografado sob essa chave de sessão: o certificado do servidor, a prova de que ele possui a chave privada correspondente, as mensagens Finished que fazem o checksum de todo o handshake. Uma viagem de ida e volta, e a conexão está de pé.

O TLS 1.2 precisava de uma segunda viagem porque a troca de chaves e a negociação de cifra rodavam como etapas separadas em vez de se sobreporem. Essa viagem extra é latência pura. Em um link com 150ms de RTT, isso soma 150ms a cada nova conexão HTTPS, e é por isso que o TLS 1.3 apareceu nos números de tempo de carregamento de página, não só na postura de segurança.

TLS 1.2TLS 1.3
Viagens de ida e volta até o primeiro byte criptografado21
Troca de chavesNegociada depois da cipher suiteChutada e enviada com o ClientHello
Renegociação no meio da sessãoPermitidaRemovida
Troca de chaves RSA estáticaPermitidaRemovida (forward secrecy obrigatório)
Cifras fracas (RC4, CBC legado)PermitidasRemovidas

O que um certificado prova e como funciona a cadeia de confiança

A troca de chaves cuida da criptografia. Provar que o servidor é realmente example.com é trabalho do certificado. Um certificado vincula uma chave pública a um nome de domínio e carrega a assinatura de uma Certificate Authority (CA), uma organização cuja própria chave pública já vem nos trust stores do seu sistema operacional e navegador.

Na prática, a cadeia tem três níveis. Um certificado root CA, autoassinado e pré-instalado em todo lugar, assina um certificado intermediate CA. O intermediário assina o certificado folha, o do seu domínio. Servidores quase nunca apresentam um certificado assinado diretamente por uma root: eles servem a folha mais o intermediário, e o cliente reconstrói a cadeia até uma root em que já confia.

Esquecer de servir o intermediário produz uma falha que parece assombração. Navegadores com uma cópia em cache daquele intermediário conectam sem problema, então o site funciona no seu notebook; todo mundo mais recebe um erro de confiança. É uma das configurações incorretas de TLS mais comuns em produção, e nunca se reproduz na máquina de onde você testou.

Um certificado também carrega uma janela de validade (notBefore / notAfter) e os hostnames que cobre, listados na extensão Subject Alternative Name (SAN). SAN é o campo que os clientes realmente leem hoje; o antigo campo Common Name é vestigial. Um certificado para example.com não cobre api.example.com a menos que esse nome também esteja na lista SAN, ou o certificado seja um wildcard para *.example.com.

Como inspecionar um certificado com openssl s_client

Você não precisa de um navegador para ver nada disso. O openssl s_client abre uma conexão TLS crua e despeja o que foi negociado:

1
openssl s_client -connect example.com:443 -servername example.com </dev/null

-servername define o SNI (Server Name Indication), o hostname enviado sem criptografia no ClientHello para que um servidor que hospeda vários domínios em um único IP saiba qual certificado apresentar. Omita e um servidor multi-tenant não tem como escolher.

Para pegar só a data de expiração em vez do dump completo do handshake, encadeie com openssl x509:

1
2
echo | openssl s_client -connect example.com:443 -servername example.com 2>/dev/null \
  | openssl x509 -noout -dates -subject -issuer

Isso imprime notBefore, notAfter, o subject (o domínio para o qual o certificado foi emitido) e o issuer (a CA que o assinou). Para um script de monitoramento que só precisa de um pass/fail, -checkend recebe uma janela em segundos e define o exit code:

1
2
3
4
openssl s_client -connect example.com:443 -servername example.com 2>/dev/null \
  | openssl x509 -noout -checkend 2592000 \
  && echo "OK: válido por pelo menos mais 30 dias" \
  || echo "AVISO: expira em menos de 30 dias"

curl -v é mais direto para uma checagem pontual: ele já imprime a versão de TLS negociada e a cadeia de certificados no meio do rastro detalhado do handshake:

1
curl -v https://example.com 2>&1 | grep -E "SSL connection|subject:|expire date"

Como conseguir HTTPS no localhost com mkcert

Um certificado autoassinado cru, que você mesmo gera e assina em vez de obter de uma CA, dá criptografia mas não autenticação. Ninguém garante por ele, então todo navegador mostra um aviso. Tudo bem para um teste rápido com openssl. Irritante para desenvolvimento local, onde você quer que https://myapp.local carregue sem precisar clicar para passar pelo aviso.

O mkcert resolve isso gerando uma CA local, instalando-a nos trust stores do sistema operacional e do navegador, e emitindo certificados assinados por ela:

1
2
mkcert -install                          # gera e confia em uma CA local
mkcert localhost 127.0.0.1 myapp.local   # emite um certificado para esses nomes

Você obtém um par certificado/chave .pem no diretório atual, em que todo navegador da máquina confia porque agora confia na CA instalada pelo mkcert. Aponte a configuração TLS do seu servidor de desenvolvimento para esses dois arquivos e o aviso desaparece.

Mantenha o rootCA-key.pem fora do controle de versão. Quem tiver a chave privada da CA local do mkcert pode gerar um certificado para qualquer domínio em que sua máquina vai confiar, a mesma classe de exposição descrita em Variáveis de Ambiente e Secrets no Docker para secrets em variáveis de ambiente: uma chave privada que vaza é uma credencial que vazou.

Como conseguir um certificado de produção com Let’s Encrypt e certbot

Em produção você precisa de um certificado assinado por uma CA em que o navegador de cada visitante já confia, e o Let’s Encrypt é a opção gratuita e automatizada que a maioria dos sites usa. O certbot fala o protocolo ACME com os servidores do Let’s Encrypt e, com um plugin de servidor web, configura o Nginx ou o Apache para você:

1
sudo certbot --nginx -d example.com -d www.example.com

Isso prova que você controla o domínio (servindo um token que o certbot coloca, ou via um registro DNS, dependendo do tipo de desafio), obtém o certificado e grava as diretivas ssl_certificate e ssl_certificate_key na sua configuração do Nginx.

Certificados do Let’s Encrypt duram 90 dias. A janela curta é proposital: torna a renovação automatizada o único caminho viável. O certbot instala um timer do systemd, ou um cron job em sistemas mais antigos, que roda duas vezes por dia e renova tudo que estiver a menos de 30 dias de expirar:

1
sudo certbot renew --deploy-hook "systemctl reload nginx"

--deploy-hook só dispara em uma renovação de fato, não em toda execução do timer, então o Nginx não recarrega duas vezes por dia à toa. Teste o pipeline antes de confiar nele sem supervisão:

1
sudo certbot renew --dry-run

Como funciona a terminação TLS em um reverse proxy ou CDN

A maioria dos serviços nunca termina o TLS dentro do processo da aplicação. Um reverse proxy na frente da sua aplicação é o lugar usual para isso: Nginx ou Caddy guarda o certificado, descriptografa a conexão HTTPS que chega e encaminha a requisição para o backend em HTTP puro na rede interna. O código da sua aplicação nunca vê um certificado.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
server {
    listen 443 ssl;
    server_name example.com;

    ssl_certificate     /etc/letsencrypt/live/example.com/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/example.com/privkey.pem;

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

fullchain.pem é o certificado folha com o intermediário já anexado, o arquivo com a cadeia de confiança pronta para servir do jeito que está. Use esse em vez de cert.pem, que é só a folha sozinha e reintroduz a falha de intermediário ausente vista antes.

Uma CDN faz o mesmo trabalho um salto mais longe. Ele termina o TLS no PoP de borda mais próximo do visitante, e então abre uma segunda conexão TLS de volta para a sua origem. Duas conexões, dois handshakes, e o certificado que o navegador do visitante valida é o certificado de borda da CDN. Seu certificado de origem só precisa satisfazer a CDN.

Essa divisão decide onde debugar. Se um curl direto na origem mostra um certificado válido mas o domínio público não, o problema está na camada do proxy ou da CDN, e nenhuma quantidade de leitura de logs da aplicação vai encontrar isso.

O que o HSTS adiciona por cima do TLS

O TLS protege uma conexão a partir do momento em que ela é HTTPS. Ele não faz nada pela primeira requisição, que ainda pode sair por http:// puro quando um usuário digita um domínio sem protocolo ou segue um link antigo, e é exatamente nessa primeira requisição que um atacante no caminho consegue interceptar e forçar o downgrade. O Strict-Transport-Security fecha essa brecha: depois de uma primeira visita HTTPS bem-sucedida, o navegador reescreve toda requisição futura para esse domínio para HTTPS antes de sair da máquina.

1
Strict-Transport-Security: max-age=63072000; includeSubDomains; preload

max-age é em segundos, e 63072000 são dois anos, confortavelmente acima do piso de um ano exigido para envio à preload list. includeSubDomains estende a política a todo subdomínio. preload marca o domínio como elegível para a lista embutida no Chrome, Firefox e Safari, cujas entradas recebem a reescrita só-HTTPS já na primeira requisição, sem precisar de uma visita anterior.

Adicione preload só quando todo subdomínio realmente servir HTTPS. Sair da preload list leva meses para se propagar de volta pelas versões dos navegadores, e até que isso aconteça, qualquer subdomínio que não conseguir fazer HTTPS fica inacessível.

Quando o TLS não é suficiente, e o que checar primeiro

O TLS verifica o servidor perante o cliente. Ele não diz nada sobre o que esse servidor faz com a sua requisição depois, e não impede que um usuário vítima de phishing digite uma senha em um domínio falso convincente que tem um certificado perfeitamente válido. O Let’s Encrypt emite para examp1e.com com a mesma facilidade que para example.com. Um cadeado significa que a conexão é privada, não que o outro lado é honesto.

Três falhas explicam a maioria dos relatos de “o site está fora do ar” que acabam se revelando uma configuração incorreta de TLS: um certificado expirado, um intermediário ausente, e uma lista SAN que não cobre o hostname solicitado. As três aparecem na saída do openssl s_client acima, antes mesmo de você abrir um único log de aplicação.

Se você mesmo termina o TLS, coloque esse comando -checkend em um cron job ou em um alerta de monitoramento ainda hoje. O certificado que derruba um site é sempre aquele que ninguém lembrava que existia.

Artigos relacionados