Привет, Хабр! Помните мою историю про Mac mini и Proxmox? Там я экспериментировал с железом. В этот раз интересная проблема пришла из сети, и пришла массово.
Проект из-за которого погрузился, не высоконагруженный, но коммерческий. И мне важно понимать, что сервер тратит ресурсы на клиентов, а не на сканеры.
В какой-то момент в отчетах появилась нетипичная динамика: всплеск визитов при нулевом росте конверсии. При анализе логов стало видно, что основная масса запросов приходит из регионов, не связанных с целевой аудиторией сайта.
Конверсия: 0%
Отказы: 99%
Цель визита: .env, wp-login.php, config.php и прочие «чёрные ходы».
Для проекта на Битриксе это:
лишняя нагрузка на PHP-FPM,
раздутые логи,
и риск, что под этим фоном пропустят реальную атаку.
Так началась эволюция моего собственного WAF.
Почему не внешний WAF или CDN?
Первый логичный вопрос, зачем писать своё решение, если есть готовые WAF и CDN?
Ответ в архитектурных приоритетах проекта.
Контроль и прозрачность. Мне важно видеть, какой IP заблокирован, по какому правилу и на какой срок. Без «чёрного ящика» между мной и сервером.
Блокировка до приложения. Любая защита на уровне PHP уже поздняя. Если бот дошёл до nginx или PHP-FPM, ресурсы уже потрачены.
В моём случае блокировка происходит на уровне ядра (iptables + ipset), до передачи запроса приложению.Гибкость под специфику проекта. Типовые сигнатуры не всегда учитывают особенности конкретного Bitrix-проекта. Иногда нужно быстро добавить правило под конкретный паттерн и сделать это за несколько минут.
Простота архитектуры. Проект локальный, без глобальной аудитории. Добавление внешнего слоя ради фонового сканирования, избыточное усложнение.
Важно, что решения принимаются на основе анализа HTTP-логов (L7), но сам дроп пакетов выполняется на уровне L3/L4 в ядре Linux. Это позволяет убрать мусор до nginx и PHP.
Я не противопоставляю этот подход внешним WAF. Для крупных или международных проектов CDN и облачные решения, абсолютно логичный выбор. В моём случае достаточно аккуратно выстроенной фильтрации на своей стороне.
Архитектура: эшелонированная оборона

Как это работает:
Первый рубеж (блокировка стран). Пакеты из стран, где у нас нет интересов, дропаются сразу. HTTP даже не парсится.
Второй рубеж (автоблокировка подозрительных IP). Если IP прошёл геоблок, но начал искать .env, .git или wp-login.php, он отправляется в бан на 2 часа.
Третий рубеж (черный список CrowdSec). Если IP уже «отметился» где-то в мире он может быть заблокирован превентивно.
Эволюция системы
Система не появилась в финальном виде. Это была последовательная доработка.
Этап | Архитектура | Управляемость | Риск ложных блокировок | Обслуживание |
До v1 | Разрозненные iptables-правила | Низкая | Повышенный | Ручные правки |
v1 | iptables + ipset + suspicious | Средняя | Контролируемый | Частичная автоматизация |
v2 | Модульная логика (country / suspicious) | Высокая | Ниже | Независимые модули |
v2 + CrowdSec | Сигнатурный слой + кастом | Максимальная | Минимизирован | Автообновляемая база |
Ключевое изменение во второй версии модули стали независимыми. Можно включать и отключать блокировку стран, подозрительные или CrowdSec отдельно. Это упростило отладку и снизило риск ложных блокировок, добавило больше управляемости.
Почему ipset - фундамент фильтрации
Если просто добавлять IP в iptables, то каждое правило это отдельная строка в цепочке.
Когда приходит пакет, ядро проходит по правилам сверху вниз, пока не найдет совпадение. Если правил 10, это незаметно. Если правил 10 000, начинается линейный перебор.
То есть фактически:
больше IP → длиннее цепочка → больше сравнений
сложность проверки - O(n)
Под небольшой нагрузкой это может быть терпимо. Под волной сканирования уже нет.
ipset работает иначе. Вместо тысячи отдельных правил в iptables остаётся одно:
-m set --match-set blocked_ips src
А сами IP лежат внутри набора (hash:ip или hash:net). Проверка происходит через хэш-таблицу, а не перебор списка. Это значит:
1 IP в наборе или 100 000, разницы почти нет
проверка выполняется за O(1)
всё происходит на уровне ядра
Именно поэтому ipset база всей схемы. Без него она бы не масштабировалась.
В первых версиях я обновлял наборы так:
ipset flush blocked_ips ipset add blocked_ips
И очень быстро столкнулся с проблемой, что в этот момент сервер фактически открыт. Так как между flush и заполнением список пустой.
Решил это обновлением набора атомарно, через временный список и swap. Сначала создаем временный набор, далее наполняем его, выполняется ipset swap и удаляется старый набор.
Так как swap выполняется мгновенно на уровне ядра. Никакого промежутка с пустым списком нет. Фильтрация не прерывается мы радуемся.
Примерно это выглядит так:
ipset create "${country}_temp" hash:net 2>/dev/null || ipset flush "${country}_temp" if [[ -s "$temp_file" ]]; then while read network; do ipset add "${country}_temp" "$network" done < "$temp_file" ipset swap "${country}_temp" "$country" fi ipset destroy "${country}_temp"
Ловим сканеров по намерению
auto-block-suspicious.sh не банит за обычные 404. Он ищет сомнительные намерения со стороны заходящего. Примеры паттернов по которым это определяю:
wp-login.php на проекте Битрикс
shell.php, shoha.php
.env, .git, .aws/credentials
Логи читаются через tail -n, а не перечитываются целиком. Скрипт сам определяет, где лежат access-логи или можно прописать путь до нужных в момент установки.

IP попадает в временный бан на 2 часа, затем автоматически удаляется. Реальный случай, в начале этого года на сайт под управлением битрикс, пошла волна запросов к /wp-login.php. За несколько минут, более 200 попыток с десятков IP. После включения модуля проверки подозрительных IP, адреса начали уходить в бан в течение 20–30 секунд, а нагрузка вернулась к нормальному уровню практически сразу.
=== Статистика системы блокировки === Блокировки по странам: China: 8802 IP Всего заблокировано по странам: 8802 IP Автоматическая блокировка подозрительных IP: Состояние: ✓ ВКЛЮЧЕН Всего: 10 IP Примеры заблокированных IP: 4.193.97.168 timeout 4972 195.178.110.109 timeout 3472 195.178.110.246 timeout 3472 144.91.93.174 timeout 4972 20.211.1.210 timeout 4972 130.12.180.34 timeout 4972 45.148.10.238 timeout 4972 195.178.110.199 timeout 4972 89.248.168.239 timeout 7071 94.26.88.31 timeout 4972
CrowdSec как базовый слой
CrowdSec подключён через bouncer, который добавляет IP в ipset crowdsec_blacklist с TTL.Таким образом:
глобальные сигнатуры закрываются автоматически,
решения синхронизируются,
бан снимается по таймауту без ручного вмешательства.
CrowdSec не заменяет кастомную логику. Он закрывает известные паттерны, а модули country и suspicious, проектную специфику. Это ещё один барьер в нашей обороне.
Метрики: что изменилось
Период сравнения: cентябрь–декабрь 2025, без защиты, январь–февраль 2026,после внедрения WAF
Показатель | Без защиты | С WAF |
Мусорный трафик | ~12% | < 0.3% |
Средний LA | 1.8–2.5 | 0.9–1.2 |
Отказы | До 40% | 12–15% |
Аналитика | Шум и боты | Чистые данные |
Пики сканирования, доходившие до десятков RPS, теперь обрываются на уровне ядра и не доходят до nginx.

Наглядно видно что когда включается блокировка по странам, количество обращений резко снижается.
Где это решение не поможет:
Не спасёт от атаки большим потоком трафика по сети (L4)
Не заменяет обновления Битрикса и других CMS
Не защищает от атак, которые идут через обычные домашние IP-адреса
Не нужен, если у вас глобальный проект с полноценным CDN
Это помогает отсеивать обычный мусор и мелкие атаки, но не заменяет полноценную безопасность (SOC).
Что дальше
Сейчас управление идёт через консоль:
auto-block-control.sh status
ipset list blocked_ips
Хочется сделать лёгкий дашборд:
карта заблокированных стран,
топ IP-агрессоров,
кнопка «помиловать».
Если у кого-то есть идеи по лёгкому UI-стеку для такой задачи, буду рад обсудить.
Заключение
В итоге получилась понятная и управляемая система фильтрации. Она не заменяет обновление Битрикса (других CMS) и закрытие дыр, но она убирает фоновый шум. Трафик проходит через несколько уровней, решения принимаются прозрачно, обновления выполняются атомарно, а вся логика остаётся под контролем. Сейчас сервер обрабатывает то, ради чего он запущен, реальные запросы пользователей.
Всю обвязку я выложил на GitHub. Там есть установщик, который настроит всё за вас.
Буду рад, если поделитесь своими паттернами для блокировки или расскажете, как вы выживаете в условиях локальных блокировок.
