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
- Dois limites: pedidos e ligações
- limit_req: o leaky bucket
- Bursts e nodelay
- limit_conn: ligações por IP
- Excepções por IP (escritório/VPN)
- 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_zonecom$binary_remote_addre taxa por zona - [ ]
limit_reqcomburstenodelaynos endpoints expostos - [ ]
limit_conn_zoneelimit_connnos downloads/sync - [ ] Excepções por IP para escritório e VPN (geo + map)
- [ ]
limit_req_status 429avaliado (separar de erros reais 503) - [ ] Error log do nginx a alimentar o fail2ban
- [ ] Jail
nginx-limit-reqactiva e testada comfail2ban-regex - [ ] Métricas de 503/429 a chegar à monitorização
Artigos Relacionados
- Nginx: Reverse Proxy com TLS para Serviços Self-Hosted — a base sobre a qual este hardening se aplica
- Fail2ban: Protecção contra Brute Force em Linux — o consumidor dos logs de rejeição
- Caddy: Reverse Proxy com TLS Automático — a alternativa com TLS automático
- Authentik: SSO Self-Hosted com OIDC e SAML — a autenticação na entrada que reduz o brute force