Nginx: Rate Limiting e Protecção contra Abusos

Uma PME portuguesa expõe o GLPI, o Nextcloud e o Grafana por port forwarding no IP público do acesso — e cada serviço fica, do minuto seguinte, exposto a brute force de login, scraping e DoS aplicacional. O nginx tem a resposta embutida: dois módulos oficiais, ngx_http_limit_req_module e ngx_http_limit_conn_module, que limitam pedidos e ligações por chave — tipicamente o IP do cliente — com o método leaky bucket. Este guia configura os dois, trata os bursts sem partir a experiência dos utilizadores legítimos, e integra os logs com o fail2ban.

Neste artigo

  1. Dois limites: pedidos e ligações
  2. limit_req: o leaky bucket
  3. Bursts e nodelay
  4. limit_conn: ligações por IP
  5. Excepções por IP (escritório/VPN)
  6. Logs e fail2ban

1. Dois limites: pedidos e ligações

Os dois módulos resolvem abusos diferentes. O limit_req controla a taxa — quantos pedidos por segundo um cliente pode fazer — e é a defesa contra brute force e scraping. O limit_conn controla quantas ligações simultâneas um cliente pode manter — e corta downloads paralelos em massa e DoS de conexões. Os dois aplicam-se em conjunto: um no http com zonas de memória partilhadas, o outro nos location que precisam.

Ambos devolvem por omissão o HTTP 503 ao pedido rejeitado (limit_req_status e limit_conn_status configuram outro código, 429 é a alternativa semântica para “too many requests”).

2. limit_req: o leaky bucket

O exemplo oficial do módulo:


http {
    limit_req_zone $binary_remote_addr zone=one:10m rate=1r/s;

    server {
        location /search/ {
            limit_req zone=one burst=5;
        }
    }
}

A directiva limit_req_zone define a zona: a chave $binary_remote_addr (o IP em binário — mais compacto que o textual), uma zona de 10 MB que guarda o estado, e a taxa rate=1r/s. O limit_req aplica a zona num location.

A capacidade de uma zona é fixa por estado, independente do tamanho da chave: 1 MB guarda cerca de 16 mil estados de 64 bytes ou 8 mil de 128 bytes. No limit_req em 64 bits o estado ocupa 128 bytes, por isso uma zona de 10 MB chega a cerca de 80.000 endereços — e no limit_conn, com estados de 64 bytes, cerca de 160.000. Para uma PME, ambos os valores ficam folgados. Quando a zona esgota, o comportamento difere: o limit_req recicla o estado mais antigo e continua a limitar, enquanto o limit_conn devolve erro a todos os pedidos seguintes até libertar um estado.

3. Bursts e nodelay

Sem burst, cada pedido acima da taxa devolve 503 imediatamente — e o primeiro utilizador que a página carregue 12 recursos num instante vê erros. O burst=5 cria uma fila: cinco pedidos excessivos são atrasados para caber na taxa, e só o sexto é rejeitado. Com nodelay, os pedidos do burst são servidos de imediato — mas os slots que ocupam continuam ocupados e libertam-se à taxa definida, por isso uma segunda rajada logo a seguir é rejeitada:


location /api/ {
    limit_req zone=one burst=10 nodelay;
}

A regra prática: burst com nodelay para APIs e páginas que carregam em rajadas legítimas (o Nextcloud a sincronizar, o GLPI a abrir um dashboard), sem nodelay onde atrasar é aceitável. O delay=N é o meio-termo — os primeiros N do burst passam imediato, o resto atrasado.

O log regista os atrasos e as recusas com níveis distintos (limit_req_log_level ajusta): as recusas registam-se no nível configurado (error por omissão) e os atrasos um nível abaixo.

⚠ Atenção

O limit_req herda-se de http para server para location — e só a directiva do nível mais interno conta. Um location sem a sua própria directiva herda a do servidor, não soma.

4. limit_conn: ligações por IP

O módulo de ligações segue a mesma estrutura — a diferença é o que conta: uma ligação só conta quando o servidor está a processar um pedido com o cabeçalho já lido:


http {
    limit_conn_zone $binary_remote_addr zone=addr:10m;

    server {
        location /download/ {
            limit_conn addr 1;
        }
    }
}

O exemplo oficial limita cada IP a uma ligação simultânea em /download/. Para um Nextcloud com sincronização de desktop e mobile, limit_conn addr 10 é mais realista — mas atenção: em HTTP/2 e HTTP/3 cada pedido concorrente conta como uma ligação para o limit_conn, e um browser moderno abre dezenas em paralelo. Com http2 activo no nginx, 10 pode ficar apertado para os clientes de sync: mede no log com o limit_conn_status 429 antes de apertar mais.

Os dois módulos combinam-se no mesmo location: pedidos por segundo no /api/, ligações simultâneas no /download/.

5. Excepções por IP (escritório/VPN)

O rate limiting por $binary_remote_addr apanha o escritório inteiro atrás do mesmo IP público como um só cliente — e o fail2ban baniria esse IP por um pico legítimo de um só utilizador. O módulo suporta chaves vazias: quando a variável avaliar a vazio, o limite não se aplica — e é assim que se constrói uma whitelist com geo/map:


geo $limit {
    default         1;
    10.8.0.0/24     0;   # VPN WireGuard
    203.0.113.10    0;   # IP público do escritório (exemplo)
}

map $limit $limit_key {
    0       "";
    1       $binary_remote_addr;
}

limit_req_zone $limit_key zone=one:10m rate=1r/s;

Os IPs da VPN e do escritório ficam fora do limite, e o resto da internet fica dentro. O mesmo padrão serve para o limit_conn_zone.

Nota: este guia parte de port forwarding directo. Atrás de um proxy ou CDN, a $binary_remote_addr chega o IP do proxy e não o do cliente — nesse cenário é preciso configurar o módulo ngx_http_realip_module para restaurar o IP real antes das zonas.

6. Logs e fail2ban

O limit_req regista cada rejeição no error log com a chave e o motivo — o fail2ban consome exactamente esse log. Um jail mínimo para brute force contra um GLPI atrás do nginx:


[nginx-limit-req]
enabled  = true
filter   = nginx-limit-req
logpath  = /var/log/nginx/error.log
maxretry = 10
findtime = 60
bantime  = 3600

A cadeia completa: o nginx devolve 503 ao abuso no minuto zero e regista, o fail2ban conta as rejeições por IP e bloqueia no firewall à décima em 60 segundos, e o abuso pesado nunca chega ao PHP do GLPI. A combinação é complementar — o nginx absorve o volume, o fail2ban corta o IP recidivista, e os logs ficam para a investigação.

Duas verificações antes de confiar no jail: o logpath tem de apontar para o error log que a tua directiva error_log do nginx realmente usa (pode não ser /var/log/nginx/error.log), e o filtro oficial nginx-limit-req casa as linhas “limiting requests, excess: … by zone …” — testa com fail2ban-regex contra uma linha real do log antes de activar a bantime. Se o log mostrar “delaying request” em vez de “limiting”, o mesmo filtro apanha ambos.

Erros Comuns

Erro Causa provável Correcção
503 para utilizadores legítimos burst ausente ou taxa demasiado baixa Adicionar burst=10 nodelay; medir no log antes de apertar
Rate limiting não aplica limit_req fora do location correcto Confirmar que a directiva está no bloco servido
Escritório inteiro bloqueado Todos atrás do mesmo IP público Exceção por geo/map com a rede do escritório
fail2ban não bane Logpath errado ou filtro sem match Testar com fail2ban-regex /var/log/nginx/error.log /etc/fail2ban/filter.d/nginx-limit-req.conf
Zona esgota mais rápido em IPv6 Cada cliente IPv6 pode usar muitos endereços da sua /64 Zona maior ou chave por subnet $binary_remote_addr truncado

Checklist

  • [ ] limit_req_zone com $binary_remote_addr e taxa por zona
  • [ ] limit_req com burst e nodelay nos endpoints expostos
  • [ ] limit_conn_zone e limit_conn nos downloads/sync
  • [ ] Excepções por IP para escritório e VPN (geo + map)
  • [ ] limit_req_status 429 avaliado (separar de erros reais 503)
  • [ ] Error log do nginx a alimentar o fail2ban
  • [ ] Jail nginx-limit-req activa e testada com fail2ban-regex
  • [ ] Métricas de 503/429 a chegar à monitorização

Artigos Relacionados

Fontes Oficiais