O que o force_ssl realmente faz
Esta aplicação já roda em HTTPS, e três linhas do config/environments/production.rb são a razão:
# Assume all access to the app is happening through a SSL-terminating reverse proxy.
config.assume_ssl = true
# Force all access to the app over SSL, use Strict-Transport-Security, and use secure cookies.
config.force_ssl = true
# Skip http-to-https redirect for the default health check endpoint.
config.ssl_options = { redirect: { exclude: ->(request) { request.path == "/up" } } }
A maioria das pessoas lê isso, concorda e segue em frente. Elas merecem uma leitura mais atenta, porque entre as três elas decidem se as suas pessoas usuárias estão protegidas e se os seus deploys funcionam.
O modelo de terminação
O processo Rails quase nunca fala TLS. Algo na frente dele, como o kamal-proxy, um ingress controller ou um balanceador de nuvem, aceita a conexão criptografada, decifra e encaminha HTTP puro por uma rede privada.
flowchart LR
B["Navegador"] -->|"HTTPS :443"| P["kamal-proxy / ingress / balanceador"]
P -->|"HTTP :3000, rede privada"| R["Rails / Puma"]
P -.->|"X-Forwarded-Proto: https"| R
C[("certificado + chave privada")] --- P
Isso se chama terminação TLS, e explica a primeira linha de configuração. Do ponto de vista do Puma, toda requisição chegou em HTTP puro. O Rails concluiria que a conexão é insegura, se recusaria a marcar cookies como seguros e, com force_ssl ligado, redirecionaria para HTTPS para sempre.
assume_ssl = true diz ao Rails para tratar toda requisição como segura. Como alternativa, o proxy define X-Forwarded-Proto: https e o Rails confia nisso. Qualquer um funciona. Não ter nenhum dos dois é o que falha, e o sintoma é um laço infinito de redirecionamento que só acontece em produção.
O assume_ssl vem com uma ressalva: é uma suposição incondicional. Se uma requisição consegue chegar ao Puma sem passar pelo proxy, por um health check ao IP do pod ou uma porta de container exposta sem querer, o Rails vai tratar aquela requisição em texto puro como segura. A mitigação é entediante e eficaz: a porta do app nunca é alcançável de fora da rede privada.
O que o force_ssl liga
force_ssl = true são três comportamentos, não um:
Primeiro, o redirecionamento. Requisições HTTP recebem um 301 para a URL https://. Isso protege quem digitou o domínio sem esquema, mas só depois de uma requisição desprotegida já ter acontecido.
Segundo, o Strict-Transport-Security. Um cabeçalho de resposta manda o navegador usar HTTPS para este host, e os subdomínios dele, por um período, sem perguntar. Isso fecha a brecha acima: depois da primeira visita, o navegador já sobe para HTTPS antes de mandar qualquer coisa.
Strict-Transport-Security: max-age=63072000; includeSubDomains
Terceiro, cookies seguros. Cookies de sessão e outros são marcados como secure, então o navegador não os envia em HTTP puro. Sem isso, uma única requisição rebaixada vaza o cookie de sessão, e o redirecionamento não ajuda, porque o cookie sai junto com a requisição que é redirecionada.
Dos três, o marcador de cookie é o que mais importa e o menos discutido.
A exclusão do health check
config.ssl_options = { redirect: { exclude: ->(request) { request.path == "/up" } } }
Um balanceador ou uma probe do Kubernetes consulta /up em HTTP puro na rede interna. Se essa requisição for redirecionada, a probe recebe um 301, e uma probe que espera 200 lê um 301 como falha. A plataforma conclui que o pod não está saudável e reinicia uma aplicação funcional em laço. O gerador do Rails 8 traz essa linha, comentada, por esse motivo.
Nesta aplicação a linha é uma rede de segurança, e não o que mantém o /up vivo. O Rails coloca o ActionDispatch::AssumeSSL na pilha de middlewares antes do ActionDispatch::SSL, então, com assume_ssl ligado, toda requisição já conta como HTTPS e nada é redirecionado. A exclusão passa a importar no dia em que o assume_ssl for desligado e o proxy consultar em HTTP puro.
A exclusão cobre mais que o redirecionamento. Uma requisição excluída que conta como HTTPS continua recebendo o cabeçalho HSTS, mas os cookies dela não são marcados como secure.
O que este curso cobre
As três linhas acima pressupõem que existe um certificado válido. Este curso é sobre de onde ele vem e como ele continua válido sem ninguém lembrar de renovar.
- O que o
force_sslliga (esta aula). - Certificados e a cadeia de confiança.
- ACME e Let's Encrypt.
- Automatizando a renovação.
- Terminando TLS, e o que o Rails enxerga.
- Wildcards, múltiplos domínios e os erros de sempre.
- Juntando tudo: HTTPS para esta aplicação.
Pontos-chave
- O TLS é terminado por um proxy; o Rails recebe HTTP puro por uma rede privada.
- O
assume_sslimpede o Rails de concluir que a conexão é insegura. Sem ele ou umX-Forwarded-Protoconfiável, você tem um laço de redirecionamento. - O
force_sslfaz três coisas: redirecionar, HSTS e cookies seguros. O marcador de cookie é o que evita o vazamento de sessão. - Excluir
/updo redirecionamento evita que um health check em HTTP leia um 301 como falha; comassume_sslligado, nada é redirecionado, para começar.
Continue com a aula 2
Certificados e a cadeia de confiança
O que um certificado contém, por que o servidor envia o intermediário e como diagnosticar os erros comuns.
Com uma conta grátis você também tem os quizzes, seu progresso e suas anotações, e a discussão.