Me deparei com uma postagem patrocinada do Intercept Brasil pedindo doação para reforçar a segurança do site. De acordo com o próprio veículo, a matéria que envolvia o vazamento de áudios de Flávio Bolsonaro com o banqueiro Daniel Vorcaro saiu do ar logo em seguida de ser publicada, e agora eles estão pedindo dinheiro aos leitores para não passar por isso de novo.

Na hora eu já fiquei curioso com o que poderia ter acontecido. Segurança é um daqueles investimentos que, quando não é feito no começo, é cobrado em dobro quando você menos espera. Segurança não investida cedo tem esse hábito de virar o maior item de orçamento de uma empresa justamente na sua fase mais crítica.

Lendo aquele pedido de doação, não consegui deixar de imaginar que brechas podem estar por trás dele:

  • Será que existia alguma página ou rota mais pesada de processar, sem limite de requisições, esperando qualquer pico de tráfego pra derrubar o sistema inteiro?
  • Será que o IP de origem estava exposto o suficiente para alguém pular a proteção da borda e ir direto na fonte?
  • Será que um sequestro de infraestrutura, parcial ou total, dependendo do nível da vulnerabilidade explorada, teve algum papel nisso?

Não sei, e não tenho como saber de fora. O que eu sei é reconhecer o nível de exposição do veículo: ter que pedir doação em público já reflete a gravidade do problema, algo que provavelmente nunca imaginaram que se tornaria uma vulnerabilidade para atacantes com as piores intenções.

Este post não é uma apuração do caso Intercept: não tenho acesso a nada que confirme o que de fato aconteceu lá dentro. O que tenho são +7 anos trabalhando com tecnologia em mídia e comunicação, tempo suficiente pra notar que empresas desse setor costumam compartilhar estruturas de sistema parecidas, e é daí que vem a bagagem pra especular. Tudo a seguir é hipótese minha, não fato apurado.


Três formas de cair por excesso de tráfego

“Caiu depois de viralizar” não aponta pra uma causa só. Pelo menos três coisas diferentes evidenciam isso, e cada uma pede uma correção diferente:

  • Cache stampede (efeito manada): uma página que não existia uma hora atrás recebe milhões de requisições de repente. Como nada ainda está em cache, cada requisição bate direto no backend: banco de dados, renderização, tudo de uma vez. Ninguém precisa estar atacando nada, um link popular o suficiente produz a mesma carga que um ataque. É o cenário mais inocente dos três, o tipo de coisa que uma empresa preparada nem sente.
  • L7 flood: uma onda de requisições individualmente válidas, navegadores ou bots de verdade, mas mirando de propósito nos endpoints mais caros: busca, comentários, qualquer coisa que dispare uma query pesada em vez de devolver um arquivo estático. Se alguém quisesse mesmo derrubar o site logo depois daquela matéria, é o primeiro ataque que eu imaginaria: barato de montar, difícil de distinguir de curiosidade genuína em escala.
  • DDoS volumétrico clássico: força bruta na camada de rede pra saturar banda ou tabelas de conexão, sem nenhum interesse em qual página está sendo atingida. Só em derrubar tudo.

Se eu tivesse acesso aos logs do Intercept, a forma prática de diferenciar os três seria olhar onde os erros se concentram. 5xx acumulado numa única URL, com um grande número de IPs distintos, parece atenção, orgânica ou direcionada, convergindo pra uma página. Erro espalhado pelo site inteiro, sem URL quente, aponta mais pra um problema de rede ou infraestrutura. Nenhum dos dois prova intenção, mas me diria qual camada investigar primeiro.


A borda não é decoração

Uma coisa que eu suspeito: boa parte da confiança que uma empresa de mídia deposita na própria segurança vem só de estar atrás de uma CDN como a Cloudflare, Azion ou Akamai. É fácil tratar isso como caixinha marcada, “a gente está atrás da CDN, estamos protegidos”. E a borda faz trabalho de verdade, sim: absorve tráfego volumétrico bruto antes que ele chegue aos servidores, aplica firewall de aplicação e detecção de bot, impõe rate limit global em todos os seus pontos de presença, cacheia o que pode.

Mas a borda não conhece a aplicação por trás dela. Ela não sabe que /busca é caro e que /robots.txt é de graça. Esse ponto cego é um clássico candidato a brecha que deveria ser pensado pelos engenheiros do sistema. Sem políticas de seguranças na camada seguinte, o Nginx (ou outro proxy reverso) na frente da aplicação, a borda vira o único ponto de proteção, não o primeiro de dois. No mínimo, essa segunda frente precisa ter:

  • Rate limit por cliente, pra que uma única origem não monopolize um recurso compartilhado mesmo que a borda tenha deixado o tráfego passar.
  • Limite de conexões simultâneas por cliente.
  • Cache próprio, independente da borda, pra que um cache miss lá na frente não vire hit completo e sem cache na aplicação.
  • Uma regra que só aceita tráfego vindo da borda. Talvez a brecha mais provável de todas, e assunto mais a frente.

Custa poucas horas de configuração hoje. Parece completamente dispensável até o dia em que vira a diferença entre um pico de tráfego chato e uma manchete sobre pedido de doação.


Quando o cache mente: poisoning e deception

Se eu fosse apostar em uma brecha explorada pelos atacantes nesse caso, seria a que ocorre com mais frequência e é mais barata de explorar: cache mal configurado. Se o problema não foi só volume, mas algum tipo de sequestro parcial do conteúdo servido, é um dos jeitos mais silenciosos disso acontecer, sem barulho, sem log de erro óbvio, só uma resposta errada sendo entregue pra todo mundo.

Cache poisoning: um cabeçalho influencia a resposta sem entrar na chave de cache. X-Forwarded-Host, Accept-Language, X-Forwarded-Proto podem mudar o que um backend renderiza (um link, um redirect, um pedaço de conteúdo embutido) sem estarem incluídos no que define “a mesma requisição”. Uma requisição forjada, uma resposta envenenada, e se isso for cacheado, todo visitante seguinte recebe a mesma página envenenada sem precisar repetir o ataque. Se algo assim tivesse acontecido no Intercept, explicaria um sequestro de infraestrutura sem exigir acesso nenhum ao servidor.

Cache deception: o oposto. Enganar a borda pra ela cachear algo que nunca deveria ter sido cacheado, tipicamente uma página de usuário logado, mal identificada como conteúdo estático por causa de um .css ou .jpg no fim da URL. Cache que decide pela extensão em vez de olhar a resposta de verdade pode acabar servindo a página privada de um usuário pra qualquer outro.

A mitigação pros dois é a mesma: ser deliberado sobre o que define uma entrada de cache. Chave de cache explícita e restrita, em vez de confiar nos padrões. Cabeçalho normalizado antes de chegar na lógica de cache. Vary definido de propósito, não como detalhe esquecido. E nunca decidir por extensão de arquivo sozinha.


O IP de origem não é segredo

Tem uma segunda brecha que eu não descartaria, mas talvez menos como causa direta e mais como uma segunda etapa do ataque. Nada da proteção da borda importa se um atacante conseguir simplesmente pular ela e falar direto com o servidor, e chegar lá nem sempre depende só de achar um IP exposto: regra de firewall pode ser burlada, acesso à VPN pode ser comprometido, ou o próprio cluster interno pode estar mal configurado e vazar endpoints que nunca deveriam estar visíveis de fora.

Se o IP de origem do Intercept estivesse simplesmente exposto por descuido, um DDoS direto na origem, sem nem passar pela Cloudflare, derrubava o serviço do mesmo jeito, não importa o que estivesse configurado na borda. Mas essa versão específica eu acho pouco provável: o mínimo que se espera de compliance de segurança numa organização desse porte é uma rede privada virtual isolando o roteamento até o backend. O que acho mais plausível é a segunda etapa, alguém contornando essa VPN ou aproveitando uma falha de configuração no cluster pra alcançar os serviços internos por outro caminho.

A correção, de qualquer forma, é arquitetural, não um patch: firewall de origem aceitando só os ranges de IP publicados pela CDN, VPN com autenticação forte e sem rotas mais amplas do que o necessário, e auditoria de configuração do cluster pra garantir que nenhum endpoint interno fique acessível de fora por descuido. Do lado do Nginx, o módulo real_ip mantém os logs e o rate limit enxergando o IP real do visitante, não o endereço da própria borda.


Cache como resiliência, não só performance

Um detalhe que não sai da minha cabeça, lendo aquele pedido de doação: será que faltou só uma diretiva de configuração barata? Cache serve pra dois motivos bem diferentes. Performance: evitar refazer um trabalho caro à toa. Resiliência: o que o cache faz por você quando a origem está com problema.

Toda entrada de cache tem um prazo de validade, e quando esse prazo vence sem ninguém renovar o conteúdo, ela vira o que se chama de “stale”: vencida, mas ainda ali, guardada. O que o servidor faz com uma entrada vencida nesse intervalo, em vez de simplesmente descartá-la, é definido por duas diretivas de configuração pouco conhecidas fora de times de infraestrutura.

A primeira é stale-while-revalidate, e ela resolve o lado de performance: serve a resposta vencida na hora, sem fazer o visitante esperar, e atualiza o conteúdo em segundo plano. Só o próximo visitante recebe a versão nova, ninguém espera uma renderização ao vivo pra ver a página carregar.

A segunda é stale-if-error, e essa resolve o lado de resiliência, é a diretiva que eu queria saber se estava configurada no Intercept. Quando a origem começa a retornar erro, ela instrui o servidor a continuar servindo a última resposta boa conhecida em cache, em vez de propagar o erro pro visitante. Um leitor com stale-if-error configurado vê a versão de antiga da matéria. Um leitor sem isso vê uma tela de erro em branco, e é justamente essa segunda experiência que vira print, vira reclamação, vira a narrativa de “o site caiu”. Teria mudado o desfecho neste caso específico? Não tenho ideia. Mas é frustrante pensar que pode ter sido algo tão simples de ligar.

A mesma lógica de lidar com conteúdo vencido vale pra quando uma entrada de cache é renovada de fato. Um TTL curto e genérico costuma ser vendido como “frescor”, mas também é a coisa que expira a proteção justamente quando um pico de tráfego mais precisa dela. Purge por evento, um webhook disparado no momento em que um novo artigo é publicado, mantém o conteúdo atualizado sem tirar a rede de segurança durante o exato padrão de tráfego que uma matéria explosiva produz.


Configurando a segunda linha de defesa

Se eu estivesse montando a origem de um site de notícias hoje, sabendo que qualquer matéria pode ser a que viraliza, é mais ou menos assim que eu deixaria a segunda linha de defesa configurada. Conceito sem código não serve pra nada.

http {
    # Define uma "zona" compartilhada de rate limit, indexada pelo IP do cliente.
    # 10m de memória compartilhada dá pra rastrear cerca de 160 mil IPs;
    # 10r/s é a taxa sustentada permitida por IP.
    limit_req_zone $binary_remote_addr zone=per_ip:10m rate=10r/s;

    # Define uma zona compartilhada para limitar conexões simultâneas por IP.
    limit_conn_zone $binary_remote_addr zone=conn_per_ip:10m;

    # Confia no IP da CDN como o salto de proxy, e lê o IP real do
    # visitante a partir do cabeçalho que ela repassa; sem isso, toda
    # requisição nos logs e no rate limit parece vir da própria CDN.
    set_real_ip_from 173.245.48.0/20;   # exemplo: range da Cloudflare
    set_real_ip_from 103.21.244.0/22;   # (repetir para cada range publicado)
    real_ip_header CF-Connecting-IP;
    real_ip_recursive on;

    # Onde as respostas em cache ficam em disco, quanto disso é mantido
    # como índice de chaves em memória (keys_zone), e o TTL padrão
    # para uma resposta bem-sucedida.
    proxy_cache_path /var/cache/nginx levels=1:2 keys_zone=main_cache:10m
                     max_size=1g inactive=60m use_temp_path=off;

    server {
        listen 443 ssl;
        server_name exemplo.com;

        # Só aceita tráfego que chegou através da CDN; uma regra de
        # firewall na origem alcança o mesmo efeito na camada de rede,
        # isto é o equivalente/reforço na camada de aplicação.
        # (lista de allow/deny com os ranges do provedor omitida por brevidade)

        location / {
            # Rejeita clientes que excedem o limite em vez de enfileirá-los
            # indefinidamente; um burst pequeno absorve o comportamento normal
            # do navegador (uma página carregando vários assets de uma vez)
            # sem afrouxar a taxa sustentada.
            limit_req zone=per_ip burst=20 nodelay;
            limit_conn conn_per_ip 20;

            proxy_pass http://app_backend;
            proxy_cache main_cache;

            # A chave de cache é explícita e deliberadamente estreita;
            # ela não inclui cabeçalhos que não deveriam influenciar
            # o que fica em cache (ver: cache poisoning).
            proxy_cache_key "$scheme$request_method$host$request_uri";

            # Se o backend estiver fora do ar, dando erro ou dando timeout,
            # continua servindo a última cópia boa em cache em vez do erro;
            # este é o mecanismo de resiliência, não só um truque de velocidade.
            proxy_cache_use_stale error timeout updating
                                  http_500 http_502 http_503 http_504;

            # Atualiza uma entrada expirada em segundo plano, em vez de
            # fazer o visitante que dispara a atualização esperar por ela.
            proxy_cache_background_update on;
        }
    }
}

Nenhuma dessas diretivas é exótica, vem tudo com o Nginx padrão. O que chama atenção é que elas raramente estão todas ligadas juntas: cada uma isolada parece opcional até o dia em que deixa de ser. Nesse dia o preço não se paga em hora de configuração. Se paga em pedido de doação.


Uma auditoria, não um veredito

Ainda não sei por que o site do Intercept caiu, e este post não tem como objetivo descobrir. O que fica pra mim é a confirmação de algo que já desconfiava: segurança adiada não é segurança economizada. É segurança financiada depois, com juros, na pior hora possível. Às vezes literalmente como um pedido de doação estampado no feed dos próprios leitores.

Fica uma lista curta de perguntas que valem a pena fazer sobre qualquer site atrás de uma borda hoje, inclusive o meu:

  • Se a nossa página mais compartilhada recebesse um pico enorme de tráfego sem nenhum atacante envolvido, a origem sobreviveria, ou toda a proteção assume que a borda segura tudo?
  • Alguém acharia o IP real da nossa origem por histórico de DNS, subdomínio esquecido, ou log de certificado? E se achasse, o nosso firewall notaria?
  • A nossa chave de cache inclui algo que não deveria poder mudar o que é servido pro próximo visitante?
  • Se o nosso backend começasse a dar erro agora, os visitantes veriam uma página desatualizada mas funcionando, ou uma tela de erro em branco?

Nenhuma dessas perguntas exige saber o que aconteceu de fato com o site de outra pessoa. Exige só fazer essa conta antes da fatura chegar, não depois.