mail@remadmin.com +375 44 516-34-24
РемАдмин РемАдмин
  • Главная
  • Услуги
    • Администрирование серверов
    • Компьютерная помощь
    • Поддержка сайтов
    • Ремонт компьютеров
  • Сервисы
    • Проверка скорости интернета
    • DNS Lookup
  • Блог
  • Контакты
Оформить заказ

Nginx: лимиты на wp-login и xmlrpc без отстрела нормальных пользователей

Серверная стойка и сетевой экран ночью — ограничение частоты запросов к WordPress
Вебмастеру
  • 06.09.2026
  • Комментариев: 0
Содержание статьи:
1. Что бьют на самом деле, и почему это две разные двери
2. Почему общий limit_req на php убивает нормальных людей
3. Две зоны, не одна
4. location: только эти два файла, тот же php, что у остальных
5. Настоящий IP, иначе режете Cloudflare, а не бота
6. Где лимит бесполезен: system.multicall
7. Как проверить, что своих не задело
8. Что я не кладу в эти зоны
9. fail2ban рядом, не вместо
10. Что оставляю на обычном сайте

Типичная заявка: «сайт лежит, nginx отдаёт 503, в access.log сплошной wp-login.php». Открываю лог — не DDoS на главную, а брутфорс. Сотни POST в минуту на wp-login.php и xmlrpc.php с пары десятков адресов. Человек уже воткнул limit_req на весь location ~ \.php$ «на всякий случай». Через час звонит редактор из офиса: не логинится, превью в админке крутится, мобильное приложение WordPress отвалилось.

Лимит сработал. Просто он отстрелил своих вместе с ботами. Офис сидит за одним белым IP, xmlrpc дергает приложение каждые несколько секунд, а общий php-location обслуживает и логин, и admin-ajax.php, и оформление заказа.

Ниже — как я режу именно логин и xmlrpc, оставляя сайт живым, и где limit_req бесполезен, сколько его ни крути. Без «поставьте плагин лимита логинов» и без слепого deny all на xmlrpc, пока не ясно, кто им пользуется.

Что бьют на самом деле, и почему это две разные двери

На типичном WordPress без WAF смотрят три URL:

  • /wp-login.php — форма входа. GET рисует форму, POST проверяет пароль.
  • /xmlrpc.php — старый API. С него удобно перебирать пароли пачками через system.multicall и дёргать pingback.
  • Иногда /wp-admin/ и REST /wp-json/ — это уже другие истории, их в ту же зону, что логин, я не кладу.

GET на логин — не атака. Человек открыл форму, браузер подтянул редирект, плагин 2FA нарисовал второй шаг. Если лимитировать любой метод, первый же заход «съедает» burst, а POST с паролем уже ловит 429. Поэтому ключ зоны я завязываю на POST, не на URL целиком.

xmlrpc почти всегда POST. Закрывать его наглухо можно, если им никто не пользуется: нет официального приложения WP, нет Jetpack, нет старых клиентов публикации. Если приложение нужно — return 403 хуже брутфорса: свои отвалятся сразу, боты просто перейдут на wp-login.php.

Почему общий limit_req на php убивает нормальных людей

admin-ajax.php на живом сайте стучит постоянно: пульс админки, автосохранение записи, корзина, фильтры каталога. Один редактор за минуту легко делает десятки POST. Посадили его в зону 1r/s burst=3 вместе с логином — получите «сайт тормозит», хотя CPU спокойный, а в логе 503 на ajax.

Вторая ловушка — общий IP. Офис, коворкинг, 4G-оператор с CGNAT, гостиница. Двадцать человек за одним адресом. Лимит «один запрос в секунду на IP» для брутфорса с ботнета ещё терпим, для офиса это коллективный бан после трёх неудачных паролей подряд.

Третья — статус по умолчанию. Nginx на превышение отдаёт 503. Мониторинг орёт «сайт мёртв», человек лезет в php-fpm и диск. Это не падение бэкенда, это отказ лимитера. Ставлю limit_req_status 429 и в логах сразу видно, что именно отсекли, а не «бэкенд не отвечает».

Две зоны, не одна

Зоны объявляю в http, не в server. Размер 10m для $binary_remote_addr — десятки тысяч IP, на обычный сайт с головой. Rate считаю отдельно:

# http { ... }
limit_req_zone $limit_key_wplogin zone=wplogin:10m rate=6r/m;
limit_req_zone $limit_key_xmlrpc  zone=xmlrpc:10m  rate=1r/s;
limit_req_status 429;
limit_req_log_level warn;

Шесть POST в минуту на логин — человек с кривым паролем ещё влезет, словарь на тысячи попыток уже нет. xmlrpc чуть свободнее по частоте HTTP-запросов: живое приложение иногда стучит пачкой. Это не дыра — дыра в xmlrpc не частота, а толстое тело, про это ниже.

Ключ пустой строкой означает «этого запроса в зоне нет». Так я выключаю лимит для GET и для белого списка.

map $request_method $limit_key_wplogin {
    POST $binary_remote_addr;
    default "";
}

# xmlrpc почти весь POST; GET на него мне не жалко тоже резать
map $request_method $limit_key_xmlrpc {
    default $binary_remote_addr;
}

Если офис сидит на постоянном адресе, вычитаю его из ключа через geo. В примере — документационный диапазон, не чей-то боевой:

geo $offnet {
    default 0;
    203.0.113.0/24 1;   # офис, подставьте свой
}

map $offnet $limit_key_wplogin {
    1      "";
    default $limit_wplogin_ip;
}

map $request_method $limit_wplogin_ip {
    POST $binary_remote_addr;
    default "";
}

Два map на одну переменную nginx не даст — ключ собираю в одном месте. Рабочий вариант без сюрпризов:

geo $is_office {
    default 0;
    203.0.113.0/24 1;
}

map "$is_office:$request_method" $limit_key_wplogin {
    "1:POST" "";
    "0:POST" $binary_remote_addr;
    default  "";
}

map $is_office $limit_key_xmlrpc {
    1       "";
    default $binary_remote_addr;
}

Белый список — не «интернет офиса целиком». Только тот префикс, с которого реально заходят в админку. Домашний динамический IP туда не тащу: через месяц он уедет соседу.

location: только эти два файла, тот же php, что у остальных

location = с точным именем бьёт точнее regex. Обработчик php не выдумываю — копирую то, что уже работает для *.php. На Debian это часто snippets/fastcgi-php.conf плюс ваш fastcgi_pass. Сокет не копируйте из чужой статьи: у вас может быть другой пул.

server {
    # ...

    location = /wp-login.php {
        limit_req zone=wplogin burst=4 nodelay;
        include snippets/fastcgi-php.conf;
        fastcgi_pass unix:/run/php/php-fpm.sock;  # как у вас в общем php
    }

    location = /xmlrpc.php {
        limit_req zone=xmlrpc burst=8 nodelay;
        include snippets/fastcgi-php.conf;
        fastcgi_pass unix:/run/php/php-fpm.sock;
    }

    # общий php — БЕЗ limit_req этих зон
    location ~ \.php$ {
        include snippets/fastcgi-php.conf;
        fastcgi_pass unix:/run/php/php-fpm.sock;
    }
}

burst=4 nodelay на логине: четыре лишних POST проходят сразу, дальше 429, без очереди. Без nodelay nginx начинает задерживать запросы — форма «думает» секундами, человек жмёт ещё раз, вы сами себе множите POST. Для логина я предпочитаю жёсткий отказ, не задержку.

На xmlrpc burst больше: приложение может открыть несколько соединений подряд при синхронизации. Если xmlrpc вам не нужен совсем — не лимитируйте, закройте:

location = /xmlrpc.php {
    return 403;
}

Проверяю необходимость просто: grep xmlrpc access.log | awk '{print $1}' | sort | uniq -c | sort -rn | head и user-agent. Если неделю стучат только словари и Hello world — закрываю. Если есть wp-iphone / wp-android с адресов редакции — оставляю зону.

Настоящий IP, иначе режете Cloudflare, а не бота

$binary_remote_addr — это TCP-пир. Стоит сайт за Cloudflare, nginx-прокси или балансировщиком хостера — пир один на всех. Лимит превращается в глобальный рубильник: первый бот выжигает burst, посетители с того же эджа ловят 429.

Смотрю, откуда приходит запрос:

log_format debugip '$remote_addr cf="$http_cf_connecting_ip" xff="$http_x_forwarded_for"';

Дальше set_real_ip_from на префиксы своего прокси и real_ip_header — тот заголовок, которому вы доверяете. Не доверяю X-Forwarded-For с интернета без фильтра: его подставляет кто угодно. Для Cloudflare — их диапазоны и CF-Connecting-IP. Для своего nginx-прокси — только его адрес в set_real_ip_from.

Пока real_ip не настроен, limit_req на логин я не включаю в боевой server. Иначе «защита» — это лотерея, кто первый постучал в этот воркер.

Где лимит бесполезен: system.multicall

Брутфорс по xmlrpc часто укладывается в один HTTP POST. Внутри system.multicall — сотни wp.getUsersBlogs с разными паролями. Для nginx это один запрос. Зона 1r/s его пропускает, php-fpm честно пережёвывает пачку, в auth.log WordPress — сотни неудачных логинов, в access.log — одна строка 200.

Поэтому «я поставил rate limit, xmlrpc больше не брутфорсят» — самообман, если multicall жив. Nginx $request_body в if на практике пустой: тело ещё не прочитано в той фазе, конфиг из гугла с if ($request_body ~ multicall) просто не срабатывает. Я это не рисую как рабочую схему.

Что работает без магии:

  • xmlrpc не нужен — return 403 на location, плюс фильтр в WordPress на всякий случай;
  • нужен только pingback — всё равно спорно, pingback сам по себе источник шума; чаще его выключают;
  • нужно приложение — оставляю xmlrpc под limit_req и отдельно отключаю multicall на стороне PHP (му-плагин на xmlrpc_methods выкидывает system.multicall), либо режу на WAF, который тело реально видит;
  • Application Passwords и REST — другая дверь, xmlrpc для них не обязателен. Если мобильный вход уже через REST, xmlrpc я закрываю, не лимитирую.

Лимит по запросам и разбор методов xmlrpc — два разных слоя. Первый без второго на этой двери недоделан.

Как проверить, что своих не задело

Не тестирую с того же ноутбука, с которого админю. Иначе сам себя выкину и буду чинить по аварийной консоли. С другого адреса, лучше с маленького VPS:

# форма должна открываться сколько угодно
for i in $(seq 1 20); do curl -s -o /dev/null -w "%{http_code}\n" https://example.com/wp-login.php; done

# POST — должны пойти 429 после burst
for i in $(seq 1 20); do
  curl -s -o /dev/null -w "%{http_code}\n" -X POST \
    -d "log=nosuchuser&pwd=wrong&wp-submit=Log+In" \
    https://example.com/wp-login.php
done

# ajax и главная не в этой зоне
for i in $(seq 1 30); do
  curl -s -o /dev/null -w "%{http_code}\n" -X POST https://example.com/wp-admin/admin-ajax.php
done

Ожидание: GET логина — сплошные 200 (или 302, если уже есть редирект). POST — несколько 200/302, потом 429, не 503. ajax — без 429. Главная и карточки товара — без 429.

Потом то же с офисного IP, если он в geo: POST не должен упираться в 429, иначе белый список не попал в ключ. Частая ошибка — geo по $remote_addr до real_ip, и офис виден как адрес Cloudflare.

В логе ищу не «много 404», а явно статус и зону. С limit_req_log_level warn в error.log будет limiting requests, excess: ... zone=wplogin. Если зоны в логе нет, а 503 есть — вы смотрите не на лимитер, а на php-fpm/upstream.

Что я не кладу в эти зоны
  • admin-ajax.php, async-upload.php, cron wp-cron.php — это не брутфорс логина.
  • REST /wp-json/ целиком. Там и легитимный редактор, и Application Passwords. Если резать — отдельные location на конкретные маршруты, не оптом.
  • wp-admin/ GET статики и admin-post.php с авторизованной сессией.
  • Статику и wp-content/uploads — к логину отношения не имеет.

Отдельно: плагины «кастомной страницы входа» часто POST-ят не на /wp-login.php, а на / или на /my-account/. Тогда мой location логин не защищает, зато WooCommerce-чекаут внезапно оказывается под лимитом, если кто-то прикрутил зону на location /. Смотрю в access.log реальный URI при неудачном входе, не название плагина на коробке.

fail2ban рядом, не вместо

Nginx лимитер не помнит «этот IP вчера перебирал пароли». Он держит короткое окно. fail2ban по error.log с zone=wplogin или по auth-логу WordPress закрывает упёртых на часы. Но jail, который банит после трёх 429, снова отстрелит офис. Либо офис в ignoreip, либо порог заметно выше человеческого «забыл пароль, нажал пять раз».

Я не баняю 429 с ajax и не баняю 404 на xmlrpc.php, если сами его закрыли 403: боты будут стучать вечно, jail раздует ipset, а толку ноль — они и так получают отказ.

Что оставляю на обычном сайте

Рабочий минимум, которым я заканчиваю, если это один WordPress на VPS без приложения:

  • real_ip настроен, в логе виден клиент, не эдж;
  • wp-login.php — POST-only зона ~6r/m, burst 4 nodelay, статус 429;
  • xmlrpc.php — 403, если за неделю нет своих клиентов;
  • если клиент есть — зона 1r/s + выключенный system.multicall в PHP;
  • офисный префикс в geo, не «весь провайдер»;
  • общий php-location без этих зон;
  • проверка циклом curl с чужого IP и глазами error.log, что падает зона, а не 503 от fpm.

Плагин лимита логинов в WordPress я не считаю заменой: он срабатывает уже внутри PHP, после того как fpm взял воркер. Под словарём это дороже, чем отказ на nginx. Плагин имеет смысл как второй слой (капча, временная блокировка учётки), не как единственный.

Если после включения 429 сыпятся на живых редакторов — не поднимаю rate «пока не пройдёт». Сначала смотрю, какой URI и какой IP. Почти всегда это либо общий php-location, либо NAT офиса, либо xmlrpc приложения, которое я забыл, что оно существует.

Нужна профессиональная удалённая помощь с сервером, сайтом, компьютером или ноутбуком?

Свяжитесь со мной любым удобным для вас способом, и получите её быстро и не дорого.

Обсудить задачу
    Другие статьи по теме:
  • Критическая уязвимость в Elementor Pro – популярном плагине WordPress
  • Contact Form 7 – удаляем значок Google reCaptcha со страниц сайта.
  • Exim – мониторинг почтовой очереди для выявления исходящего спама
  • WP2Shell — критическая уязвимость в ядре WordPress (CVE-2026-63030)
  • Nginx – блокируем вредных ботов.
  • Битрикс Веб-окружение и проблемы с Вебвизор Яндекс Метрики из-за заголовка X-Frame-Options

Помогла статья? Поблагодари автора!

Остались вопросы, или есть что добавить? Добро пожаловать в комментарии.

Угостить автора чашечкой кофе
Тэги:
NginxWordpresswp-loginxmlrpcбезопасностьспам
Поделиться:

Рубрики

  • Linux
  • Windows
  • Авто
  • Вебмастеру
  • Новости IT
  • Полезное
  • Ремонты
  • Умный дом

Последние записи

  • Скандинавский свет на Opel Astra H: UEC Variant 5 + REC Variant 1
  • Nginx: лимиты на wp-login и xmlrpc без отстрела нормальных пользователей
  • journald съедает диск: SystemMaxUse, persistent vs volatile и что смотреть overnight
  • WP2Shell — критическая уязвимость в ядре WordPress (CVE-2026-63030)
  • ILSERBY — мой каталог сетевых инструментов в одном месте
  • Инкрементальный бэкап через rsync –link-dest: полные слепки, hardlink и проверка restore
  • Ремонт проводки задних дверей Opel Zafira A (левая и правая)
  • Steal time и странный load на VPS: как отличить нехватку CPU у гипервизора от реальной нагрузки

cron curlftpfs CVE-2026-64638 Daytime Running Light DNS exim hardlink ilserby InternetExplorer journald Mysql Nginx OP-COM Opel Opel Astra H REC rsync ssh SSL systemd UEC VPS Windows11 Wordpress wp-login xmlrpc XSS2Shell Zafira Битрикс МНС Мобильное приложение Полезное с Aliexpress Правки реестра Резервное копирование Уязвимость Яндекс Алиса безопасность гофра логи онлайн-инструменты сеть скандинавский свет спам стеклоподъёмник шпаргалка

Силин Виталий Петрович
УНП: EA3254844

Зарегистрирован В МНС Республики Беларусь в качестве плательщика налога на профессиональный доход.

Последнее в блоге
  • Скандинавский свет Opel Astra H
    06.09.2026 Скандинавский свет на Opel Astra H: UEC Variant 5 + REC Variant 1
  • Серверная стойка и сетевой экран ночью — ограничение частоты запросов к WordPress
    06.09.2026 Nginx: лимиты на wp-login и xmlrpc без отстрела нормальных пользователей
Контактная информация
  • 247264, Республика Беларусь Гомельская область, Рогачёвский район, п. Ильич, улица Садовая, дом 10-1
  • mail@remadmin.com
  • +375 44 516-34-24
Copyright © 2016 - 2026 REMADMIN.
  • Контакты
  • Политика конфиденциальности
Введите текст для поиска...