Nginx

Rate limit no wp-login.php com nginx: barrar força bruta antes do PHP

O servidor sobe de CPU do nada e o access.log está entupido de POST /wp-login.php. Plugin de limite de tentativas não resolve esse caso: ele só age depois que o PHP montou o WordPress inteiro, e é justamente esse carregamento que está comendo o processador. O corte tem que ser no nginx.

A zona e o location exato

A zona vive no bloco http e é compartilhada por todos os sites. Dez megabytes seguram algo perto de 160 mil endereços:

# em /etc/nginx/nginx.conf, dentro de http { }
limit_req_zone $binary_remote_addr zone=login:10m rate=2r/m;

# no server { } do site
location = /wp-login.php {
    limit_req zone=login burst=3 nodelay;
    limit_req_status 429;

    include snippets/fastcgi-php.conf;
    fastcgi_pass unix:/run/php/php8.3-fpm.sock;
}

O rate=2r/m libera uma requisição a cada 30 segundos por IP. O burst=3 guarda três excedentes numa fila e o nodelay manda essas três na hora, em vez de segurar. Quem passar disso leva 429 e nunca encosta no FPM.

Conferindo o efeito

A requisição barrada aparece sim no access.log, com status 429 (campo 9 no formato combined). O motivo, com IP e nome da zona, o nginx joga no log de erro, porque limit_req_log_level vem em error por padrão:

sudo nginx -t && sudo systemctl reload nginx
sudo awk '$9 == 429' /var/log/nginx/access.log | tail -20
sudo grep "limiting requests" /var/log/nginx/error.log | tail -20

Duas pegadinhas. A primeira: location = /wp-login.php é match exato e vence o location ~ .php$, então se você esquecer o fastcgi_pass ali dentro o nginx passa a oferecer o arquivo como download e a tela de login some. A segunda: atrás de Cloudflare ou de um balanceador, $binary_remote_addr é o IP do proxy, e sem o módulo real_ip configurado você limita todo mundo de uma vez.

Deixe rodando um dia e releia o error.log. Se aparecer IP de gente de verdade sendo cortada, afrouxe pra 5r/m.

Post Recentes

Anonimizar IP no nginx: mascarar o último octeto do log para a LGPD

Seu access.log guarda o IP inteiro dos visitantes há meses sem necessidade. Mascare o último octeto com map e log_format…

15 horas atrás

Prompt injection em campo de usuário: isole a observação do pedido

A observação do pedido chegou com ordem embutida para gerar cupom. Delimite o texto do cliente e trate a resposta…

1 dia atrás

Preço promocional em massa no WooCommerce: agendar a Black Friday pelo WP-CLI

Um CSV com SKU e preço, um wp eval-file que agenda início e fim da promoção guardando o estado anterior,…

1 dia atrás

Too many connections no MySQL: achar quem segura as conexões antes de subir o limite

O erro 1040 aparece quando todas as conexões estão ocupadas. Antes de subir o max_connections, veja quem está segurando cada…

2 dias atrás

WP_MEMORY_LIMIT no WordPress: resolver memória esgotada no PHP 8.3

Allowed memory size exhausted no painel: qual das duas constantes do wp-config.php mexer, onde conferir o valor real e por…

2 dias atrás

Converter data dd/mm/aaaa para o MySQL com DateTime::createFromFormat

strtotime com data brasileira devolve false e o INSERT quebra no MySQL 8. Use createFromFormat com getLastErrors antes de mandar…

4 dias atrás