Короткий ответ

Если access.log на site.example (10.0.20.40) забит несуществующими URL, xmlrpc.php, wp-login.php с разных /16 — это не «нужно больше RAM». Режьте до PHP: limit_req / limit_conn в nginx, бан по логу в fail2ban, грубые CIDR на nft/ufw. PHP pm.max_children только удерживает очередь, не убирает мусор. Не ufw disable и не «откройте всё, потом закроем».

Легитимных поисковиков отличают по PTR/диапазонам и умеренному RPS, не по строке Googlebot в UA — её легко подделать.

Симптомы и как отличить

  • Load average растёт, php-fpm все в R.
  • Люди получают 502/504, боты — тоже, но их больше.
  • Топ URI: /wp-login.php, /xmlrpc.php, .env, /vendor/phpunit.
КартинаНе боты
Один URL медленный у всехSQL/PHP
Только /wp-login паролиbrute force
Трафик с офиса 10.0.20.0/24свой сканер/мониторинг

Возможные причины

  1. Публичный WordPress без rate limit на login/xmlrpc.
  2. Открытый xmlrpc.php для pingback-амплификации.
  3. Сломанный SEO-плагин отдал бесконечные URL, краулер добросовестно бьёт.
  4. Нет robots.txt / неправильный sitemap.
  5. Слабый WAF/CDN или его нет.
  6. После смены IP старые боты продолжают бить 10.0.20.40.

Диагностика

1. Кто и куда

sudo awk '{print $1}' /var/log/nginx/access.log | sort | uniq -c | sort -nr | head
sudo awk '{print $7}' /var/log/nginx/access.log | sort | uniq -c | sort -nr | head
sudo awk -F'"' '{print $6}' /var/log/nginx/access.log | sort | uniq -c | sort -nr | head

Apache: $1 и %r в combined.

2. PHP vs статика

sudo awk '$7 ~ /\.php/ {c++} END {print c+0}' /var/log/nginx/access.log
systemctl status php8.3-fpm --no-pager | head

3. Не задеть мониторинг

Белый список: 10.0.20.0/24, IP uptime-проверки. Сохраните список до банов.

4. Является ли UA настоящим Googlebot

Проверка PTR и A — по документации Google, не по форуму. Не блокируйте целые AS Google вслепую.

Решение

sudo tar -C /etc -czf /root/nginx-bot-$(date +%F).tar.gz nginx

Сценарий A. nginx limit_req / limit_conn

В http {}:

limit_req_zone $binary_remote_addr zone=php:10m rate=5r/s;
limit_conn_zone $binary_remote_addr zone=addr:10m;

В server для PHP:

location ~ \.php$ {
    limit_req zone=php burst=20 nodelay;
    limit_conn addr 10;
    include snippets/fastcgi-php.conf;
    fastcgi_pass unix:/run/php/php8.3-fpm.sock;
}

limit_req отдаёт 503 по умолчанию; можно limit_req_status 429. Проверьте nginx -t. Burst подберите так, чтобы офисный NAT 10.0.20.0/24 не резало: для NAT лучше geo + map на ключ $limit пустой для белых.

Пример исключения:

geo $limit_bot {
    default 1;
    10.0.20.0/24 0;
}
map $limit_bot $limit_key {
    0 "";
    1 $binary_remote_addr;
}
limit_req_zone $limit_key zone=php:10m rate=5r/s;

Пустой ключ — лимит не применяется.

Сценарий B. Закрыть xmlrpc, если не нужен

location = /xmlrpc.php {
    deny all;
}

Или limit только на него. WordPress Jetpack может требовать xmlrpc — сверьтесь с бизнесом.

Сценарий C. fail2ban по access.log

Jail на wp-login и 404-сканеры. Не баньте навсегда /24 интернета. Типичный bantime 1h, findtime 10m. Не отключайте банлист «чтобы проверить сайт» без ignoreip.

Сценарий D. ufw/nft грубые сети

Только после доказательства. Режьте конкретные IP/префиксы сканера, не ufw default deny incoming без allow 80/443/SSH с jump-хоста.

sudo ufw status numbered
sudo nft list ruleset | head

Сценарий E. Apache

mod_evasive / mod_ratelimit / Require not ip. Синтаксис Apache 2.4 — Require, не Allow from как единственный стиль 2.2.

Сценарий F. location = /wp-login.php vs сканер по всему дереву

Лимит только на login не спасёт, если бот бьёт /index.php?p=1..N или REST /wp-json/. Тогда limit_req на location ~ \.php$ как в сценарии A. Поисковый бот с нормальным RPS и верным PTR не должен попасть под 5r/s вместе с атакой: вынесите geo для известных краулеров или пустите их в CDN cache. Не блокируйте User-Agent Mozilla — это все.

На Apache 2.4 mod_remoteip обязателен за балансировщиком, иначе limit/Require смотрят IP прокси. Логируйте %a после remoteip, не %h сырой.

Если PHP-FPM уже в свопе от ботов, сначала лимит, потом reload FPM — не наоборот: иначе очередь снова заполнится за секунды.

Как проверить, что проблема устранена

С атакующего тестового IP (не из офиса) серия запросов к /wp-login.php должна получить 429/503 после порога. С 10.0.20.40 (если это сам сервер) и с белого офиса — сайт 200. php-fpm children не на потолке. Access log: доля мусорных URI падает.

curl -s -o /dev/null -w '%{http_code}\n' --resolve site.example:443:10.0.20.40 https://site.example/

Если не помогло

  • Боты с CDN: origin видит IP Cloudflare, нужен модуль реального IP и лимит по $http_cf_connecting_ip только если заголовок ставит ваш CDN.
  • Layer-7 на канале: это уже апстрим-провайдер/анти-DDoS, не max_children.
  • Легитимный краулер после кривого sitemap — чините sitemap, не баньте Google целиком.
  • NPM перед сайтом: лимиты ставьте там или на origin, не два противоречивых.
  • Если limit_req 503, а PHP всё равно 100%: лимит не попал в тот server/default_server, который реально отвечает на Host.

Профилактика

  • С первого дня: limit на login/xmlrpc, админка.
  • Мониторинг RPS и 5xx.
  • Не светить phpinfo и .env.
  • Заголовки безопасности: headers.

FAQ

503 от limit_req — это поломка nginx?

Нет, это штатный отказ лишнего RPS. Смотрите error.log limiting requests.

Можно ли резать по User-Agent curl?

Сломаете свой мониторинг. Лучше IP + URI.

deny all на весь /wp-admin?

Только если админы ходят с VPN/офиса. Иначе отдельная статья.

Apache и nginx оба на хосте

Лимиты на том, кто принимает 80/443. Второй как бэкенд лимиты не видит клиентский IP без X-Forwarded-For и осторожности к спуфингу.

Нужен ли captcha сразу?

Сначала rate limit. Captcha — на login, не на всю витрину.