Безопасность сайта

Как закрыть wp-admin от посторонних: три рабочих способа

Ограничение админки по IP, basic-auth на уровне сервера и смена адреса входа. Что из этого реально помогает против ботов и что ломает сайт.

12 минут чтения 3 Дмитрий Акиндинов
Содержание
  1. Кто стучится в вашу админку круглые сутки
  2. Способ 1: ограничение по IP в htaccess
  3. Способ 2: basic-auth перед админкой
  4. Способ 3: смена адреса входа
  5. Что нельзя закрывать
  6. Дополнительные меры к трём способам
  7. Проверка после настройки
  8. Что я ставлю клиентам по умолчанию
  9. Частые вопросы

Админку закрывают тремя способами: фильтр по IP, пароль на уровне веб-сервера и смена адреса входа. Первые два надёжные, третий это шумоподавление, но он ставится за две минуты и убирает почти весь мусорный трафик ботов.

Ограничить доступ к wp-admin просят обычно после того, как хостинг пришлёт письмо про превышение лимита CPU, а в логах обнаружатся десять тысяч POST-запросов на wp-login.php за сутки. Способов закрыть админку три: фильтр по IP, basic-auth на уровне веб-сервера и смена адреса входа. Работают они по-разному, ломают сайт тоже по-разному. Разберу каждый с конкретными конфигами, покажу, что нельзя закрывать ни в коем случае, и дам чек-лист проверки после настройки.

Окно админки WordPress за щитом с фильтром по IP-адресам

Сразу оговорка про приоритеты. Закрытие админки это не защита от взлома, а снижение шума и отсечение самой массовой автоматики. Дыру в устаревшем плагине оно не закрывает, залитый шелл в uploads не лечит. Но перебор паролей и нагрузку от ботов убирает почти полностью, а это половина инцидентов у малого бизнеса.

Кто стучится в вашу админку круглые сутки

Возьмите любой сайт на WordPress, который живёт в интернете больше месяца, и посмотрите access-лог. У клиента с визиткой на 200 посетителей в месяц я недавно насчитал 4 100 обращений к wp-login.php за неделю. Ни один из них не был человеком.

Работают три типа ботов. Первый перебирает связку admin плюс словарь из миллиона паролей, вслепую, по спискам доменов из зон .ru и .com. Второй сканирует сайт на предмет известных уязвимостей: дёргает readme.html, wp-content/plugins/*/readme.txt, пытается определить версии и найти плагин с публичной CVE. Третий эксплуатирует уже найденную дыру и заливает файл.

Посмотреть свою картину можно одной командой на хостинге:

# топ IP, которые долбятся в форму входа
grep 'wp-login.php' ~/logs/access.log | awk '{print $1}' | sort | uniq -c | sort -rn | head -20

# сколько всего попыток за файл лога
grep -c 'POST /wp-login.php' ~/logs/access.log

# кто перебирает XML-RPC (второй по популярности вектор)
grep -c 'POST /xmlrpc.php' ~/logs/access.log

Если во второй строке цифра больше пары сотен, вы уже под перебором. Если больше нескольких тысяч, скорее всего хостинг уже прислал предупреждение по CPU: PHP на каждую попытку входа поднимает полный WordPress, дёргает базу, считает хеш пароля. Тысяча попыток в час на shared-тарифе съедает лимит быстрее, чем реальные посетители.

Именно поэтому фильтровать перебор надо на уровне веб-сервера, до запуска PHP. Плагины типа Limit Login Attempts тоже нужны, но они срабатывают уже внутри WordPress, то есть нагрузку не снимают. Подробнее про плагинную часть я писал в статье про лимит попыток входа.

Способ 1: ограничение по IP в htaccess

Самый надёжный вариант. Пускаем в админку только с известных адресов, все остальные получают 403 до того, как PHP успеет проснуться.

Файл кладём отдельный, в саму папку wp-admin, чтобы не мешать корневому .htaccess с правилами перезаписи:

# /wp-admin/.htaccess
<RequireAll>
    Require ip 95.108.213.14
    Require ip 178.176.0.0/16
    Require ip 2a02:6b8::/32
</RequireAll>

Отдельно закрываем wp-login.php, он лежит в корне и под правила wp-admin не попадает. В корневой .htaccess, выше блока # BEGIN WordPress:

<Files wp-login.php>
    <RequireAll>
        Require ip 95.108.213.14
        Require ip 178.176.0.0/16
    </RequireAll>
</Files>

# заодно xmlrpc, если не пользуетесь приложением WordPress и Jetpack
<Files xmlrpc.php>
    Require all denied
</Files>

Синтаксис выше для Apache 2.4. На старых 2.2 (встречается на древних тарифах) работает связка Order deny,allow плюс Deny from all и Allow from IP. Смешивать их нельзя, сервер вернёт 500. Проверить версию: apachectl -v либо посмотреть в панели хостинга.

На чистом nginx без Apache это выглядит так, но менять конфиг вам придётся руками через панель или просить поддержку:

location ~ ^/(wp-admin|wp-login.php) {
    allow 95.108.213.14;
    allow 178.176.0.0/16;
    deny all;
}

Когда фильтр по IP не подходит

Проблема одна, зато большая: динамический адрес. У домашнего провайдера IP меняется при переподключении роутера, у мобильного интернета он вообще плавает по сотне подсетей оператора. Закрылись по своему адресу вечером, утром провайдер выдал новый, и вы заперты снаружи собственного сайта.

Как это обходят на практике:

  • Разрешаем не адрес, а подсеть провайдера. Найти её можно на whois-сервисе по своему текущему IP, там будет строка вида inetnum: 178.176.0.0 - 178.176.255.255. Точность падает (внутри подсети сидят все абоненты оператора), но перебор из Азии и Латинской Америки всё равно отсекается.
  • Держим VPN с фиксированным адресом. VPS у российского провайдера обойдётся в районе 300-500 рублей в месяц, статический IP на нём есть по умолчанию. Для агентства или подрядчика это правильное решение: один адрес в белом списке на все клиентские сайты.
  • Оставляем аварийный вход по basic-auth (следующий раздел) на случай, когда IP слетел, а править сайт надо сейчас.
  • Держим доступ к файловому менеджеру хостинга. Пока вы можете отредактировать .htaccess через панель, вы не заперты насмерть. Это, кстати, главная причина не хранить пароль от панели хостинга в одной корзине с паролем от админки.

Клиентам с офисом и статическим IP от провайдера я ставлю фильтр по адресу почти всегда: это две минуты работы и минус 99 процентов мусорного трафика. Клиентам, которые правят сайт с телефона в дороге, приходится идти вторым или третьим путём.

Способ 2: basic-auth перед админкой

Второй пароль на уровне веб-сервера. Браузер спрашивает логин и пароль в системном окне, ещё до того как запрос дойдёт до PHP. Бот, который умеет только слать POST на wp-login.php, упирается в 401 и уходит.

Генерируем файл с паролем. По SSH:

# создаём файл вне docroot, чтобы его нельзя было скачать
htpasswd -c /home/user/.htpasswd-wp admin_site
# спросит пароль дважды

# если htpasswd на хостинге нет, хеш можно получить так
openssl passwd -apr1 'ваш_пароль_тут'
# результат вставить в файл строкой вида: admin_site:$apr1$...

Подключаем в /wp-admin/.htaccess:

AuthType Basic
AuthName "Restricted"
AuthUserFile /home/user/.htpasswd-wp
Require valid-user

# без этой строки сломается загрузка файлов и AJAX, см. ниже
<Files admin-ajax.php>
    Require all granted
    Satisfy any
</Files>

Важные детали, на которых спотыкаются почти все:

  1. AuthUserFile требует абсолютный путь. Относительный не работает, ошибка при этом будет 500 и без внятного текста, придётся лезть в error.log.
  2. Файл .htpasswd кладите выше корня сайта. Если он лежит в docroot и не закрыт правилами, его можно скачать по прямой ссылке и брутить хеш офлайн.
  3. Панель хостинга часто умеет ставить basic-auth в два клика через раздел вида «Защита паролем директории». Если такой пункт есть, пользуйтесь им: путь к файлу пароля панель пропишет сама и не ошибётся.
  4. Пароль для basic-auth должен отличаться от пароля администратора WordPress. Смысл двух замков теряется, если ключ один. Про то, как выбирать и хранить эти пароли, есть отдельный разбор в статье про пароли от админки.

Что ломается от basic-auth

Список короткий, но лечится он до, а не после жалоб клиента.

Что ломаетсяСимптомЛечение
admin-ajax.phpФормы на фронте молча не отправляются, корзина не обновляетсяИсключить файл из авторизации (блок Files выше)
Загрузка медиаМедиабиблиотека виснет на 0 процентовОбычно лечится тем же исключением admin-ajax
REST APIГутенберг не сохраняет запись, ошибка «Ответ не является корректным JSON»Не закрывать /wp-json/, авторизация только на wp-admin
Приложение WordPress на телефонеНе подключается к сайтуОтказаться от приложения либо от basic-auth, вместе они не живут
Плагины с внешними вебхукамиПлатёжка или CRM перестают присылать колбэкиВебхуки обычно идут не в wp-admin, но проверить логи обязательно

Отдельно про мобильные браузеры: Safari на iOS иногда забывает сохранённые креденшлы basic-auth после обновления системы, и владелец сайта звонит с формулировкой «у меня всё пропало». Предупредите заранее, что окно с логином это нормально.

Способ 3: смена адреса входа

Самый популярный совет из интернета и самый переоценённый. Ставим плагин (WPS Hide Login, Loginizer, All-In-One Security), задаём адрес вида /vhod-2026/, стандартный wp-login.php начинает отдавать 404.

Что это реально даёт: массовые боты, которые ходят строго по wp-login.php, перестают попадать в форму. Нагрузка падает, лог перестаёт распухать. Для сайта, который не интересен никому персонально, эффект заметный.

Что это не даёт. Адрес входа утекает десятком способов, и ни один из них не требует квалификации:

  • REST API отдаёт список авторов по /wp-json/wp/v2/users, оттуда берутся логины;
  • ссылки на автора /?author=1 редиректят на /author/имя-логина/, если ЧПУ включён;
  • плагин, который забыли обновить, может сам светить настоящий путь в HTML или в своих скриптах;
  • при ошибке в формах WordPress иногда отдаёт редирект на реальный адрес входа;
  • в бэкапе или в экспортированной базе новый адрес лежит открытым текстом.

Поэтому смена адреса это маскировка, а не замок. Ставить её можно и нужно как дешёвый способ снять шум, но считать защитой нельзя. Я обычно ставлю смену адреса вместе с фильтром по IP: первое отсекает случайных ботов, второе держит настоящий периметр.

Отдельно про сам плагин. Любой хайд-логин ломается при смене темы, при конфликте с кэширующим плагином и при переносе сайта на другой хостинг. Записывайте новый адрес входа в паспорт сайта рядом с доступами, иначе через полгода будете вспоминать его через базу:

-- вытащить адрес входа, который прописал WPS Hide Login
SELECT option_name, option_value FROM wp_options
WHERE option_name IN ('whl_page', 'whl_redirect_admin');

Что нельзя закрывать

Здесь ломается больше всего сайтов. Файл wp-admin/admin-ajax.php лежит внутри админки, но вызывается публично, с фронта, у неавторизованных посетителей. Через него работают формы обратной связи, фильтры каталога, подгрузка товаров, корзина WooCommerce, счётчики и половина плагинов с динамикой.

Закрыли wp-admin целиком и не сделали исключения: форма на сайте перестаёт отправляться, но визуально всё выглядит рабочим. Клиент неделю думает, что просто нет заявок. Видел такое трижды, и каждый раз это стоило владельцу больше, чем весь годовой тариф на обслуживание.

Короткий список того, что должно остаться открытым:

  • wp-admin/admin-ajax.php (AJAX с фронта);
  • /wp-json/ (REST API, на нём живёт редактор Гутенберг и часть интеграций);
  • wp-cron.php, если крон вызывается извне по расписанию с хостинга;
  • wp-content/uploads/ (картинки), wp-content/themes/, wp-content/plugins/ в части CSS и JS. Закрывать плагины целиком нельзя, слетит вёрстка;
  • файлы подтверждения владения для Яндекс Вебмастера и Search Console в корне;
  • .well-known/acme-challenge/, иначе не продлится сертификат Let’s Encrypt.

А вот что закрыть полезно и почти безопасно: xmlrpc.php (если не пользуетесь мобильным приложением и Jetpack), readme.html, wp-config-sample.php, install.php, исполняемый PHP в папке uploads (именно оттуда чаще всего запускается залитый шелл).

Дополнительные меры к трём способам

Ограничение доступа хорошо работает в паре с несколькими вещами внутри WordPress. Каждая из них ставится за 10-15 минут.

Лимит попыток входа. Плагин блокирует IP после 3-5 неудачных попыток на 15-60 минут. Нагрузку не снимает (PHP всё равно запускается), но словарный перебор делает бессмысленным по времени.

Капча на форме входа. Работает, но выбирайте вариант без Google reCAPTCHA: на российских сайтах он периодически отваливается вместе с блокировками, и владелец остаётся перед неработающей формой. Рабочие альтернативы: Yandex SmartCaptcha, простая арифметическая проверка, honeypot-поле.

Двухфакторная аутентификация. Самая полезная мера из всех дополнительных, потому что она защищает даже при утёкшем пароле. Настройка через приложение-аутентификатор занимает пять минут на пользователя, разбор есть в статье про двухфакторку для админки.

Журнал входов. Плагин типа Simple History или WP Activity Log пишет, кто и когда зашёл, что менял, какой плагин обновил. Без журнала после инцидента вы не восстановите картину и будете гадать.

Отключение подсказок при ошибке входа. По умолчанию WordPress честно сообщает «Неверный пароль для пользователя admin», то есть подтверждает существование логина. Одна строка в functions.php темы или в mu-plugin убирает эту подсказку:

// mu-plugins/login-errors.php
add_filter('login_errors', function () {
    return 'Неверные данные для входа.';
});

И ещё: не оставляйте пользователя с логином admin и ролью администратора. Создайте нового администратора с непредсказуемым логином, войдите под ним, старого удалите с передачей записей. Половина ботов перебирает пароли только к admin, для них сайт становится нерешаемой задачей.

Проверка после настройки

Настроили и ушли это худший сценарий. Всё ломается не в момент правки конфига, а через день, когда клиент попробует загрузить фото в новость. Прогоняйте чек-лист сразу, он занимает минут десять.

  • [ ] Вход в админку с разрешённого IP работает, страница входа отдаёт 200.
  • [ ] Вход с запрещённого адреса даёт 403 или 401. Проверять с мобильного интернета, выключив Wi-Fi, это надёжнее любого прокси.
  • [ ] Форма обратной связи на фронте отправляется, заявка приходит в почту и в Telegram.
  • [ ] Загрузка картинки в медиабиблиотеку доходит до 100 процентов.
  • [ ] Автосохранение записи в редакторе работает, кнопка «Обновить» не выдаёт ошибку JSON.
  • [ ] Корзина и фильтры каталога живы (для магазина проверить оформление тестового заказа целиком).
  • [ ] Плагины, которые ходят наружу, не сыплют ошибками в логе.
  • [ ] Робот Яндекса и Google по-прежнему получают 200 на главной и на sitemap.xml.

Быстрая проверка кодов ответа одной командой:

for url in / /wp-login.php /wp-admin/ /wp-admin/admin-ajax.php /wp-json/ /sitemap_index.xml; do
  printf '%-32s %sn' "$url" "$(curl -s -o /dev/null -w '%{http_code}' https://example.ru$url)"
done

Ожидаемая картина: главная 200, wp-login.php 403 или 401 с чужого адреса, admin-ajax.php 200 либо 400 (это нормально, он ждёт параметр action), wp-json 200, sitemap 200. Если admin-ajax.php отдаёт 401 или 403, вы только что сломали формы, вернитесь к разделу про исключения.

И проверьте логи через сутки после настройки. Количество POST на wp-login.php должно упасть до единиц. Если не упало, значит правило не применилось: частая причина в том, что .htaccess положили в wp-admin, а хостинг работает на nginx без Apache, где эти файлы просто игнорируются. Как читать логи и искать в них следы, разбирал в статье про поиск точки взлома по логам.

Что я ставлю клиентам по умолчанию

Схема, к которой пришёл за несколько лет обслуживания чужих сайтов, выглядит так. Для клиента со статическим IP в офисе: фильтр по адресу плюс двухфакторка плюс журнал входов. Для клиента, который работает из дома и с телефона: basic-auth с исключением admin-ajax, смена адреса входа и лимит попыток. Смену адреса без остальных мер не ставлю никогда, толку мало.

Времени на настройку уходит 40-60 минут вместе с проверкой по чек-листу, если сайт типовой. Столько же может уйти на разбор последствий, если закрыть админку без исключений и обнаружить это через неделю по пропавшим заявкам.

Если не хотите разбираться с конфигами Apache и рисковать формами, эта работа входит в абонентское обслуживание вместе с обновлениями, бэкапами и мониторингом. Разовой настройкой периметра тоже занимаюсь, разработка и дизайн в такие работы не входят. Напишите через форму на главной, опишите хостинг и CMS, скажу за полчаса, что именно у вас применимо, а что сломает сайт.

Частые вопросы

Как закрыть wp-admin от посторонних

Если у вас статический IP, фильтром по адресу. Если нет, паролем на уровне веб-сервера перед формой входа. Плюс смена адреса входа как дополнительный слой.

Что нельзя закрывать

Файл обработки асинхронных запросов: на нём держатся загрузка медиа и часть работы плагинов на публичной части. Закрыв его целиком, вы сломаете сайт для посетителей.

Помогает ли смена адреса входа

От целевой атаки нет, от массового перебора да. Боты ходят по типовым адресам, и смена убирает подавляющую часть мусорных запросов и нагрузки на процессор.

Что делать без статического IP

Ставить пароль на уровне веб-сервера или ограничивать вход по стране. Ещё вариант это доступ через свой сервер, но для одного сайта это уже перебор.

Передайте техническую часть в надёжные руки.

Слежу за хостингом, бэкапами и обновлениями. Когда что-то пойдёт не так – звоните мне, не своему программисту, не в спортзал и не маме.

Короткие вопросы по теме

Все вопросы по теме "Безопасность сайта"

Заявка

Обсудим задачу

Напишите имя и телефон, выберите удобный канал. Перезвоню или напишу в течение часа в рабочее время.

Куда удобнее ответить?