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

Админка https://site.example/wp-admin/ и wp-login.php — самый шумный URL в логах. База: HTTPS, сильные пароли, 2FA на administrator, обновления ядра/плагинов, запрет PHP в uploads, rate limit. Усиление: доступ к /wp-admin/ и wp-login.php только с VPN/офиса 10.0.20.0/24. Смена URL логина («hide login») — слабый слой, ломает сервисы, не заменяет 2FA. Origin /var/www/site.example. Если после allow-list словите 403 с дома — это ожидаемо, откройте VPN, не allow all.

Brute: отдельная статья. Инцидент: плагин.

Смена URL логина («hide login») — слабый слой. Основа: HTTPS, 2FA администраторов, allow-list или VPN, обновления, rate limit на wp-login.php. Проверяйте с IP вне офиса 10.0.20.0/24, иначе не увидите 403. WP-CLI (wp user list, core verify-checksums) оставьте запасным входом, если закроете UI. Не открывайте :81 панелей и phpMyAdmin в интернет «на время настройки». phpMyAdmin — отдельный vhost только с VPN, не на том же server_name site.example.

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

Вы настраиваете защиту, не обязательно после взлома. Признаки «голой» админки: wp-login.php 200 из интернета, xmlrpc pingback, нет плагина 2FA, plugin list с outdated.

403 на админке после ваших правок — смотрите 403, не отключайте правило навсегда.

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

  1. Админка в интернете без 2FA.
  2. Слабый пароль admin / пользователь admin.
  3. xmlrpc.php для перебора.
  4. Установка плагинов из UI на проде.
  5. Нет обновлений.
  6. HTTP на login (cookie без Secure).
  7. «Hide login» + забытый URL в закладках как единственная мера.

Диагностика

curl -sI --resolve site.example:443:10.0.20.40 https://site.example/wp-login.php | head
curl -sI --resolve site.example:443:10.0.20.40 https://site.example/xmlrpc.php | head
sudo -u www-data wp --path=/var/www/site.example core check-update
sudo -u www-data wp --path=/var/www/site.example plugin status
sudo -u www-data wp --path=/var/www/site.example user list --fields=user_login,roles

В nginx ищите уже существующие location = /wp-login.php.

Решение

Сайт только HTTPS, 301. В wp-config.php:

define('FORCE_SSL_ADMIN', true);

2. Rate limit на login (nginx)

limit_req_zone $binary_remote_addr zone=wplogin:10m rate=1r/s;

location = /wp-login.php {
    limit_req zone=wplogin burst=5 nodelay;
    include snippets/fastcgi-php.conf;
    fastcgi_pass unix:/run/php/php8.3-fpm.sock;
}

Не забудьте include как у остальных PHP. Подробнее пороги — статья brute force.

3. Allow-list IP (если есть VPN/офис)

location = /wp-login.php {
    allow 10.0.20.0/24;
    deny all;
    include snippets/fastcgi-php.conf;
    fastcgi_pass unix:/run/php/php8.3-fpm.sock;
}
location /wp-admin/ {
    allow 10.0.20.0/24;
    deny all;
    location ~ \.php$ {
        include snippets/fastcgi-php.conf;
        fastcgi_pass unix:/run/php/php8.3-fpm.sock;
    }
}

Исключение для AJAX, если нужно с витрины:

location = /wp-admin/admin-ajax.php {
    include snippets/fastcgi-php.conf;
    fastcgi_pass unix:/run/php/php8.3-fpm.sock;
}

nginx -t && reload. Проверьте с IP вне офиса: 403. С офиса: форма логина. Заголовок allow за CDN: нужен real IP, иначе все клиенты — IP CDN и правило бессмысленно или закроет всех.

Apache:

<Location /wp-login.php>
    Require ip 10.0.20.0/24
</Location>

4. 2FA

Плагин 2FA из wordpress.org с TOTP, политика: administrator обязателен. Сохраните backup-коды вне webroot. Не SMS как единственный фактор, если есть TOTP.

5. Обновления и файлы

sudo -u www-data wp --path=/var/www/site.example plugin update --all
sudo -u www-data wp --path=/var/www/site.example core update

На проде с git:

define('DISALLOW_FILE_MODS', true);
define('DISALLOW_FILE_EDIT', true);

Обновления тогда через пайплайн, не UI.

6. xmlrpc

Если Jetpack/мобильное приложение не нужны:

location = /xmlrpc.php { deny all; }

7. PHP в uploads — deny (см. инцидент-статьи)

8. Пользователь admin

Не используйте логин admin. Создайте именного administrator, старого понизьте/удалите после проверки.

Сценарий: REST и application passwords

/wp-json/wp/v2/users иногда светит логины. Закройте enumeration плагином/nginx для неавторизованных, если нет нужды в публичном REST. Application Passwords для интеграций — отдельные, с отзывом при увольнении, не пароль администратора в wp-config константе.

DISALLOW_FILE_MODS ломает обновление из UI — это цель на проде с git. На staging оставьте UI. Документируйте, кто катает обновления WP-CLI:

sudo -u www-data wp --path=/var/www/site.example core update
sudo -u www-data wp --path=/var/www/site.example plugin update --all

После обновления — core verify-checksums. Скрытый логин (login-slug) не отменяет 2FA: сканеры всё равно бьют wp-login.php и получают 404, нагрузка остаётся. Держите rate limit на этот URI даже если «спрятали».

Проверьте, что allow 10.0.20.0/24 не забыть для IPv6 офиса (allow 2001:db8:... не выдумывайте — возьмите фактический префикс). Иначе коллеги на v6 получат 403 при живом v4.

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

  • Из интернета (LTE): wp-login.php 403 или 429 при флуде, не бесконечный 200.
  • Из 10.0.20.0/24: логин + 2FA проходит.
  • xmlrpc.php 403 если решили закрыть.
  • wp core check-update — нет критичных дыр ядра.
  • Витрина и admin-ajax (если нужен) работают.
curl -sI --resolve site.example:443:10.0.20.40 https://site.example/wp-login.php | head

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

  • WooCommerce и REST /wp-json/ — не админка, отдельная аутентификация. Не режьте REST без понимания приложения.
  • Мобильные сотрудники без VPN: либо VPN, либо 2FA+rate limit без IP-allow.
  • Плагин hide-login + ваши location на старый URL — 404, поправьте location.

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

  • Квартальный plugin list outdated.
  • Отдельная учётка для контента без administrator.
  • Заголовки.
  • Бэкап до обновлений.
  • Раз в квартал: wp user list --role=administrator, 2FA включена, xmlrpc закрыт или обоснован, verify-checksums ядра. IP-allow пересматривайте, когда меняется VPN. Проверка защиты — с LTE, не только с 10.0.20.0/24.

FAQ

Достаточно ли капчи на login?

Лучше rate limit + 2FA. Капча мешает и обходится ботами выборочно.

wp-admin basic auth поверх WP

Допустимый слой (auth_basic), пароль не тот же, что WP. Усложняет REST. Документируйте.

Нужен ли WAF?

Полезен как слой, не вместо обновлений.

App passwords для REST

Отзывайте при увольнении, не путайте с паролем входа.

Можно ли открыть админку только на отдельном hostname adm.site.example?

Да, отдельный vhost + DNS только внутри VPN. Основной сайт без wp-login location. Следите, чтобы cookie домена не разъехались (COOKIE_DOMAIN).