Короткий ответ
Если 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 | свой сканер/мониторинг |
Возможные причины
- Публичный WordPress без rate limit на login/xmlrpc.
- Открытый
xmlrpc.phpдля pingback-амплификации. - Сломанный SEO-плагин отдал бесконечные URL, краулер добросовестно бьёт.
- Нет
robots.txt/ неправильный sitemap. - Слабый WAF/CDN или его нет.
- После смены 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 | headApache: $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 | head3. Не задеть мониторинг
Белый список: 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_req503, а 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, не на всю витрину.