Buzeli
buzeliSoluções Digitais
Segurança

Rebootando 2× ao dia: o rate-limit que não funcionava porque o nginx via o IP errado (real_ip atrás de CDN)

Publicado em 24 de julho de 2026

Um servidor reiniciando sob enxurrada de requisições, com um limitador contando o IP errado (o da CDN) em vez do atacante

O sintoma: reboots diários sob scraping

Um EC2 t4g.medium (2 vCPU) servindo um WordPress multisite atrás de uma CDN reiniciava duas vezes por dia. Sob carga, os 10 workers do PHP-FPM saturavam a ~100% de CPU, o load subia para 4,70 (numa máquina de 2 vCPU) e o tempo de resposta médio ia a 15,77s. Era scraping distribuído — muitos IPs, poucas requisições cada — e o rate-limit não barrava nada.

A causa: o nginx contava o IP da CDN

Havia uma zona de rate-limit de 12 req/min por $binary_remote_addr. O problema: atrás de uma CDN, o $remote_addr que o nginx enxerga é o IP do PoP da CDN, não o do cliente. Como todo o tráfego chega "da CDN", o limite agrega milhares de clientes (e atacantes) sob poucos IPs — e nunca dispara corretamente. A CDN tinha adicionado 2 PoPs novos cujos ranges não estavam no set_real_ip_from, então o real_ip nem era reconstruído para eles.

Copiar
# nginx precisa confiar nos ranges da CDN p/ ler o IP real do X-Forwarded-For
set_real_ip_from 198.51.100.0/24;   # PoP da CDN (exemplo)
set_real_ip_from 203.0.113.0/24;    # PoP novo que faltava → real_ip não reconstruído
real_ip_header   X-Forwarded-For;
real_ip_recursive on;

# só então o rate-limit conta o cliente certo:
limit_req_zone $binary_remote_addr zone=perip:10m rate=12r/m;
Rate-limit por IP atrás de CDN só funciona se o nginx reconstruir o IP real primeiro. Sem set_real_ip_from cobrindo TODOS os PoPs, você limita a CDN — não o atacante.

O erro que cometemos: bloquear a própria CDN

Na pressa, uma tentativa foi bloquear as faixas de onde vinha "o tráfego" — que eram os PoPs da CDN. Isso derruba o site inteiro para todo mundo. A lição: antes de bloquear um range, confirme se ele é do atacante ou da sua própria borda. Com o real_ip correto, os IPs de origem reais aparecem e o bloqueio mira o lugar certo.

O segundo ajuste: regex de paginação

O scraping batia em URLs de paginação profundas. A regra que detectava o padrão usava {3,} (3+ dígitos), deixando passar as páginas de 2 dígitos. Apertar para {2,} pegou a maior parte do abuso sem afetar navegação legítima:

Copiar
# antes: só pegava /page/100+;  depois: pega /page/10+
location ~* "/page/[0-9]{2,}" { limit_req zone=perip burst=5 nodelay; }

O resultado

Copiar
Tempo de resposta médio : 15,77s  ->  < 1s
Load (2 vCPU)           : 4,70    ->  0,24
PHP-FPM CPU             : ~100%   ->  normal
Reboots/dia             : 2       ->  0

Detalhe operacional: o journald não era persistente, então os logs dos crashes anteriores se perderam. Persistir o journal (Storage=persistent) foi parte do fix — sem log, cada reboot era um mistério novo.

Lições

1. set_real_ip_from precisa cobrir TODOS os PoPs da CDN. Um PoP novo fora da lista quebra o real_ip e cega o rate-limit.

2. Nunca bloqueie um range sem confirmar a origem. Bloquear os IPs da própria CDN derruba o site para todos.

3. journald persistente ou você perde a evidência. Sem Storage=persistent, cada reboot apaga o motivo do reboot anterior.

Conclusão

Rate-limit é tão bom quanto o IP que ele conta. Atrás de uma CDN, o nginx vê o PoP — e um limite por IP vira inútil até você reconstruir o IP real com set_real_ip_from para todos os ranges da borda. Com o IP certo, 12 req/min voltou a significar 12 req/min por cliente, e o servidor parou de reiniciar.