Перейти к содержимому

Защита Vps от сканеров: бан подозрительных запросов к .env и попыток входа по Ssh

Сделал вышибалу для VPS: подозрительные запросы к `.env` и SSH заканчиваются баном

Логи небольшого VPS редко бывают скучными. На сервере могут работать всего пара сайтов, Caddy и SSH, но access-журнал быстро заполняется одинаковыми запросами: `/wp-admin`, `/.env`, `/phpmyadmin`, `/config.php.bak` и другими файлами, которых на сервере никогда не существовало.

Параллельно в `systemd-journal` появляются записи о попытках входа под пользователями `admin`, `oracle`, `test` и прочими случайными именами. Это не случайные посетители, ошибшиеся адресом. Обычно так работают автоматические сканеры, которые перебирают распространённые пути и учётные данные в надежде найти плохо настроенный сервер.

Почему я не стал использовать Fail2ban

Fail2ban - нормальный и полезный инструмент, особенно для защиты SSH. Но лично у меня он довольно быстро превратился в отдельный проект. Для каждого сценария требовалось разбираться с jail, фильтрами, действиями, перезапусками и причинами, по которым правило вдруг не сработало.

С SSH ситуация ещё относительно понятная. А вот для веб-сервера всё сложнее. Можно заблокировать IP за большое число ответов `404`, но тогда есть риск наказать обычного пользователя, который несколько раз ошибся в адресе. Другой вариант - составлять длинные регулярные выражения для известных вредоносных путей. Однако списки таких путей постоянно меняются, а поддерживать коллекцию чужих шаблонов не хотелось.

Кроме того, на сервере уже были Caddy, журналы и сетевой фильтр. Добавлять ещё один стек ради достаточно простой логики показалось избыточным. Хотелось получить небольшую самостоятельную программу с понятным конфигом, которая не просто записывает в журнал сообщение "адрес заблокирован", а действительно перестаёт пропускать трафик.

Так появился небольшой pet-проект. Сначала он назывался CaddyBan, позднее получил имя Bouncer. Идея проста: поставить на входе "вышибалу", который различает обычные обращения и очевидную автоматическую разведку.

Как программа понимает, какие URL нормальные

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

Программа обходит сайт, начиная с главной страницы, переходит по ссылкам из HTML и сохраняет найденные адреса. При этом задаются ограничения по глубине обхода и максимальному числу страниц. Это важно: без ограничений неудачный парсер или циклические ссылки способны создать лишнюю нагрузку на собственный сервер.

Одного автоматического обхода недостаточно. Некоторые ресурсы не отображаются в HTML: API, служебные endpoints, страницы одностраничного приложения, `robots.txt` и другие специальные адреса. Поэтому нужные исключения можно явно добавить в конфигурацию.

Дальше обработка запроса выглядит так:

1. Из access-лога берутся IP, путь и HTTP-код.
2. Если путь входит в список известных, запрос считается нормальным.
3. Если адрес отсутствует в списке и сервер вернул `404`, для IP фиксируется промах.
4. После достижения заданного порога адрес добавляется в набор блокировки.
5. Запрет действует ограниченное время - например, один час.

Порог нужен для защиты от ложных срабатываний. Один ошибочный URL ничего не доказывает: пользователь мог вручную опечататься или открыть старую ссылку. Но несколько неизвестных путей подряд за короткий промежуток уже похожи на перебор каталогов.

Отдельные ловушки банятся сразу

Для некоторых адресов ожидание порога не имеет смысла. Если сайт статический и на нём нет WordPress, обращение к `/wp-admin` или `/wp-login.php` почти наверняка является автоматическим сканированием. То же относится к попыткам получить `/.env`, резервные конфигурации, дампы баз данных и другие потенциально чувствительные файлы.

Такие пути можно объявить ловушками. При обращении к ним IP блокируется немедленно, без необходимости ждать пять или десять ошибок.

При этом важно не превращать список ловушек в случайный набор слов. В него стоит добавлять только адреса, которые точно не используются конкретным проектом. Если сервер действительно обслуживает WordPress, блокировка `/wp-admin` сделает работу сайта невозможной.

Учёт попыток построен на временном окне

Для каждого IP хранится список времени последних подозрительных событий. Когда приходит новый промах, программа добавляет текущий момент, а устаревшие записи удаляет с начала списка.

Количество оставшихся записей и есть число нарушений за заданный период. Например, можно настроить пять неизвестных URL за две минуты. После превышения лимита адрес попадает в набор `nftables`.

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

Старые записи намеренно не обрабатываются

Есть важная деталь: после запуска программа начинает читать журнал только с новых строк. Она не проходит файл с самого начала.

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

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

Отдельная логика для SSH

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

Если считать обе записи независимо, одно действие окажется двойным нарушением. Это приводит к преждевременной блокировке или заставляет писать дополнительные сложные фильтры.

Для несуществующих пользователей используется немедленная блокировка. На рабочем сервере обращения к `admin`, `oracle` и похожим именам обычно не имеют легитимного объяснения. Попытки входа под `root` обрабатываются мягче: для них задаётся порог событий за определённое время.

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

Почему используется nftables

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

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

Но перед включением такого механизма нужно проверить доступ к серверу. Лучше заранее разрешить собственный постоянный IP, использовать отдельную консоль провайдера и начать с короткого срока блокировки. Ошибка в правиле firewall способна заблокировать не только нарушителя, но и самого администратора.

Что такая защита не решает

Bouncer не заменяет обновление системы, безопасную конфигурацию SSH, резервное копирование и базовые меры защиты. Он не предотвращает распределённые атаки с постоянно меняющимися адресами и не спасает от перегрузки канала, если злоумышленник отправляет большой объём трафика.

Также нельзя считать отсутствие подозрительных строк доказательством безопасности. Хорошо подготовленный атакующий может действовать медленно, использовать разрешённые URL или имитировать обычного браузера. Локальный бан по IP эффективен прежде всего против массовых автоматических сканеров и примитивного перебора.

Минимальный набор дополнительных мер выглядит так:

- отключить вход по паролю в SSH;
- использовать ключи и запретить прямой вход под `root`;
- ограничить доступ к SSH по IP, VPN или отдельному сетевому интерфейсу;
- регулярно обновлять операционную систему и веб-сервер;
- не хранить секреты в общедоступном каталоге;
- настроить ротацию и контроль заполнения логов;
- заранее проверить восстановление из резервной копии.

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

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

Прокрутить вверх