Мы построили собственную систему мониторинга сетевой безопасности для интернет-провайдера. Она в реальном времени анализирует трафик, помогает видеть атаки на публичные подсети, находит SSH-брутфорс, сканирование портов, попытки эксплуатации уязвимостей, подозрительную активность и автоматически блокирует наиболее агрессивные IP на пограничном маршрутизаторе Cisco.
За сутки система видит около 13 млн соединений от примерно 170 тыс. уникальных IP. Только попыток SSH-брутфорса может быть порядка 144 тыс. в день.
В этой статье я расскажу не только о том, из каких компонентов состоит система, но и о том, почему мы пришли именно к такой архитектуре. Будут Zeek, Suricata, Vector, Redis, OpenSearch, ClickHouse, Python-детекторы, Telegram-алерты, Cisco ACL и несколько неприятных граблей, на которые мы наступили по дороге.
Статья будет полезна тем, кто хочет построить NSM/SOC-систему своими руками и не покупать коммерческий SIEM за космические деньги.
С чего всё началось
У нас есть сеть интернет-провайдера: несколько публичных подсетей, пограничный маршрутизатор Cisco ASR1000 и сервер, на который зеркалируется трафик через SPAN-порт.
Задача на первый взгляд звучит просто:
-
видеть, кто атакует наши хосты;
-
понимать, какие именно атаки происходят;
-
находить SSH-брутфорс, сканирование портов, веб-атаки и попытки эксплуатации CVE;
-
автоматически блокировать наиболее агрессивные IP;
-
хранить историю для расследований;
-
получать алерты в Telegram;
-
строить отчёты;
-
и при этом не разориться на лицензиях.
Ограничение тоже было вполне реальное: один сервер, 17 ядер, 32 ГБ RAM. Без кластера, без облака, без «давайте просто докинем ещё три ноды».
То есть нужно было сделать систему, которая будет работать на обычном железе, но при этом выдерживать провайдерский объём трафика.
Общая архитектура
В итоге поток данных у нас проходит четыре основные стадии:
-
Захват трафика
-
Парсинг и обогащение
-
Хранение
-
Аналитика и реакция
Схематично это выглядит так:
SPAN-порт | ------------------- | | Zeek Suricata метаданные IDS-алерты | | | Redis | | -------- Vector --- | ----------------- | | OpenSearch ClickHouse real-time история | | ------- Python ------- | ----------------------- | | | Cisco ACL Telegram Dashboard
На первый взгляд компонентов много, но каждый из них решает свою задачу.
Zeek даёт детальную сетевую телеметрию: кто с кем общался, по каким портам, какие DNS-запросы были, какие TLS-сессии, какие SSH-попытки.
Suricata ловит известные плохие паттерны по сигнатурам: эксплойты, сканеры, C2, suspicious traffic, правила ET Open и так далее.
Vector забирает события, нормализует их, обогащает GeoIP/ASN и отправляет дальше.
OpenSearch используется для горячих данных и быстрых real-time запросов.
ClickHouse хранит большую историю и отлично справляется с тяжёлыми агрегациями.
Python-слой отвечает за детекторы, корреляцию событий, блокировки и алерты.
Почему Zeek и Suricata нужны вместе
Одна из первых развилок: использовать Zeek или Suricata?
На практике это неправильный вопрос. Они не заменяют друг друга. Они смотрят на сеть с разных сторон.
Suricata
Suricata — это классический IDS/IPS-движок. Она отвечает на вопрос:
Видели ли мы что-то известное плохое?
Например:
-
сигнатуру эксплойта;
-
обращение к подозрительному домену;
-
попытку эксплуатации CVE;
-
характерный паттерн сканера;
-
подозрительный payload;
-
срабатывание правила из ET Open.
Это очень полезно, но есть ограничение: Suricata хорошо ловит то, что описано правилами.
Zeek
Zeek работает иначе. Это не IDS в привычном смысле, а движок сетевой телеметрии.
Он отвечает на другой вопрос:
Что вообще происходило в сети?
Zeek пишет структурные логи:
-
conn.log— соединения; -
dns.log— DNS-запросы; -
http.log— HTTP; -
ssl.log/tls.log— TLS; -
ssh.log— SSH-сессии; -
notice.log; -
weird.log; -
и другие.
Именно Zeek позволяет строить поведенческую аналитику.
Например:
-
один IP за 5 минут постучался на 200 портов;
-
один источник пытается подключаться к сотням наших хостов;
-
к одному серверу идёт резкий всплеск SSH-попыток;
-
наш внутренний хост вдруг начал много ходить наружу;
-
появились странные DNS-запросы;
-
выросло число неуспешных SSH-авторизаций.
Такую активность не всегда поймаешь сигнатурой. Но её можно вычислить по метаданным.
Поэтому в нашей архитектуре Zeek и Suricata работают вместе:
-
Suricata ловит известные угрозы;
-
Zeek даёт данные для поведенческого анализа.
Грабли №1. Zeek в standalone-режиме быстро упирается в потолок
Сначала Zeek был запущен в стандартном standalone-режиме.
Это означает: один процесс, один поток.
На небольшом трафике такой вариант живёт нормально. Но на провайдерском зеркале всё быстро стало плохо: один поток упирался в 100% CPU, а Zeek начинал терять пакеты.
В статистике мы видели больше 100 тысяч dropped packets за пятиминутку.
Для системы безопасности это критично. Если вы теряете пакеты, значит, вы видите не всю картину. А если вы видите не всю картину, то любые выводы становятся условными.
Можно думать, что «атак нет», но на самом деле они просто не попали в анализ.
Решение: Zeek cluster + PF_RING
Мы перевели Zeek в кластерный режим.
Вместо одного процесса появились роли:
-
manager — управляет кластером;
-
logger — пишет логи;
-
proxy — помогает обмениваться состоянием;
-
workers — разбирают трафик.
Основную работу делают workers. Их можно запускать несколько, распределяя нагрузку по ядрам.
Для балансировки мы использовали PF_RING. Он позволяет распределять пакеты с одного интерфейса между несколькими worker-процессами и при этом сохранять целостность потоков: пакеты одного TCP-соединения попадают в один и тот же worker.
В итоге мы запустили 8 workers.
Результат был очень заметный: дропы упали со 100 тысяч до примерно 70 пакетов за пятиминутку. То есть практически до нуля.
Отдельно про Suricata
Suricata тоже пришлось ограничить по потокам.
По умолчанию она старалась использовать всё доступное железо и в пике могла съедать около 3,5 ГБ RAM, конкурируя с Zeek за CPU.
После явного ограничения числа потоков поведение стало предсказуемым, а система — стабильнее.
Вывод
На провайдерском трафике однопоточный захват — это не мониторинг, а иллюзия мониторинга.
Если Zeek теряет пакеты, то детекторы работают по неполным данным. Поэтому кластерный режим и нормальное распределение нагрузки — обязательная часть архитектуры.
Vector: транспорт, нормализация и GeoIP
После захвата данные нужно доставить в хранилища.
Для этого мы выбрали Vector.
Можно было использовать Logstash или Filebeat, но Vector оказался проще и легче. Он потребляет меньше ресурсов, хорошо работает с потоками данных и имеет удобный язык трансформаций VRL.
В нашей схеме Vector делает три основные вещи.
1. Читает входные данные
Zeek пишет логи в файлы. Vector читает эти файлы построчно.
Suricata сначала тоже писала в файл eve.json, но позже мы ушли от этого варианта и перевели её на Redis. Об этом ниже.
2. Нормализует события
Сырые логи не всегда удобно отправлять в базы как есть.
Например, у Zeek поля могут называться так:
id.orig_hid.resp_hid.orig_pid.resp_p
Точки в именах полей не всегда хорошо переживаются разными хранилищами и инструментами визуализации.
Поэтому мы приводим имена к более удобному виду:
id_orig_hid_resp_hid_orig_pid_resp_p
Также Vector приводит типы:
-
порты — к числам;
-
timestamps — к единому формату;
-
пустые значения — к
nullили удалению поля; -
IP-поля — к корректному виду для OpenSearch.
3. Обогащает GeoIP и ASN
Один IP сам по себе мало что говорит.
Гораздо полезнее видеть:
-
страну;
-
ASN;
-
организацию;
-
провайдера;
-
принадлежность к хостингу или облаку.
Поэтому Vector обогащает события через локальные базы MaxMind.
В результате вместо просто 1.2.3.4 мы видим, например, что это хостинг в определённой стране и конкретной автономной системе.
Для расследований это очень помогает. Когда в топе атакующих внезапно появляется много IP из одного ASN, это уже повод смотреть внимательнее.
Грабли №2. Файл-посредник между процессами — плохая идея
Изначально Suricata писала все события в файл eve.json, а Vector читал его как обычный file source.
На бумаге всё выглядело просто.
На практике начались проблемы.
Файл быстро рос до нескольких гигабайт. Мы настроили logrotate, и после этого периодически появлялась неприятная ситуация: сервисы живы, ошибок нет, Suricata работает, Vector работает, OpenSearch работает, ClickHouse работает — но новые события не появляются.
То есть всё «зелёное», но данных нет.
Причина была в ротации файла и checkpoint-позиции Vector.
Vector запоминает, до какого места он дочитал файл. После ротации могла возникнуть рассинхронизация: Vector считал, что файл всё ещё старого размера, пытался читать с позиции, которой уже нет, и новые события не доходили до хранилища.
Можно было долго тюнинговать logrotate:
-
copytruncate; -
postrotate hook;
-
переоткрытие файла;
-
сброс checkpoint;
-
аккуратную ротацию по inode.
Но в какой-то момент стало понятно: мы лечим не причину, а следствие.
Проблема была в самом файле-посреднике.
Решение: Redis между Suricata и Vector
Suricata умеет писать EVE-события напрямую в Redis.
Мы переключили поток на такую схему:
Suricata -> Redis list -> Vector -> OpenSearch / ClickHouse
Suricata кладёт JSON-события в Redis-список через lpush, а Vector забирает их из Redis как источник данных.
Что это дало:
-
исчезла проблема ротации
eve.json; -
исчезли проблемы с inode и checkpoint;
-
снизилась нагрузка на диск;
-
Redis стал буфером между Suricata и Vector;
-
если Vector перезапускается, события не теряются, а копятся в очереди.
Это оказалось гораздо надёжнее, чем файл.
Вывод
Файл как канал обмена между процессами выглядит простым решением, но в продакшене приносит много скрытых проблем: ротация, позиции чтения, inode, дисковый I/O, рассинхронизация.
Если оба компонента умеют работать через очередь или брокер — лучше использовать очередь.
Redis, Kafka, NATS — в зависимости от масштаба и требований. В нашем случае Redis оказался достаточно простым и практичным решением.
Грабли №3. Один плохой документ может уронить весь batch
Ещё одна проблема появилась при отправке данных в OpenSearch.
Vector отправляет события батчами. И если в батче есть один документ, который не проходит маппинг OpenSearch, можно потерять не только его, но и соседние события из этого же батча.
У нас это проявилось на IP-полях.
В OpenSearch поле было объявлено как тип ip, а в некоторых событиях Zeek туда прилетала пустая строка.
Пустая строка — это не валидный IP. OpenSearch отклонял документ с ошибкой парсинга.
Проблема в том, что вместе с ним мог отвалиться весь batch.
Решение
Мы добавили защитную нормализацию в Vector.
Если IP-поле пустое, мы не отправляем пустую строку, а удаляем поле целиком или приводим его к корректному null, в зависимости от схемы.
То есть вместо такого:
{ "id_resp_h": ""}
мы отправляем событие без этого поля.
Вывод
На границе со строго типизированным хранилищем нельзя доверять сырым данным.
Любые поля типа ip, integer, date, boolean нужно нормализовать до отправки. Один грязный документ не должен ломать загрузку соседних нормальных событий.
Почему мы используем две базы данных
Это, пожалуй, главное архитектурное решение всей системы.
Сначала мы пытались жить только на OpenSearch.
OpenSearch хорош для:
-
поиска;
-
фильтрации;
-
near real-time данных;
-
расследований по конкретному IP;
-
просмотра событий;
-
быстрых запросов за последние минуты или часы.
Но когда данных стало много, историческая аналитика начала становиться тяжёлой.
Запросы за 7, 14 или 30 дней по сотням миллионов документов либо выполнялись долго, либо требовали слишком много ресурсов.
При этом нам нужны были регулярные вопросы такого типа:
-
топ атакующих IP за месяц;
-
топ стран;
-
топ ASN;
-
динамика SSH-брутфорса по дням;
-
количество уникальных источников;
-
распределение атак по категориям;
-
статистика Suricata alerts за неделю;
-
отчёты для руководства.
Для этого OpenSearch не идеален.
Поэтому мы добавили ClickHouse.
Разделение ролей
|
Задача |
Где выполняем |
|---|---|
|
Что происходит прямо сейчас |
OpenSearch |
|
Поиск по конкретному IP |
OpenSearch |
|
Последние алерты |
OpenSearch |
|
Данные за 5 минут / 1 час |
OpenSearch |
|
Суточная статистика |
ClickHouse |
|
Недельные отчёты |
ClickHouse |
|
Месячные агрегации |
ClickHouse |
|
Топы по сотням миллионов строк |
ClickHouse |
Принцип простой:
короткое окно — OpenSearch, длинное окно — ClickHouse.
В ClickHouse у нас накопились сотни миллионов записей. Например, порядка 330 млн записей соединений и 670 млн алертов Suricata.
И при этом запросы вроде «топ атакующих за 30 дней» выполняются за секунды.
Причина в том, что ClickHouse — колоночная база. Если нужно посчитать count(), uniqExact() и сделать GROUP BY, он читает только нужные колонки, а не весь документ целиком.
Для аналитики это огромная разница.
Вывод
Не существует одной базы, которая одинаково хорошо закрывает:
-
real-time поиск;
-
полнотекст;
-
расследования;
-
тяжёлую историческую аналитику;
-
отчёты по сотням миллионов строк.
Попытка использовать одну базу для всего приводит либо к медленным отчётам, либо к раздуванию железа.
Разделение на горячее хранилище и историческое аналитическое хранилище оказалось очень удачным решением.
Аналитика и детекторы
Поверх OpenSearch и ClickHouse работает Python-слой.
Он периодически забирает свежие данные, прогоняет их через набор детекторов, коррелирует события и принимает решения: просто отправить алерт или уже блокировать IP.
Детекторы можно разделить на несколько групп.
Поведенческие детекторы
Они строятся в основном на данных Zeek.
Примеры:
-
всплеск числа соединений с одного IP;
-
сканирование портов;
-
сканирование большого количества наших хостов;
-
SSH-брутфорс;
-
HTTP-брутфорс;
-
рост количества ответов 401;
-
DNS NXDOMAIN flood;
-
подозрительное количество коротких соединений;
-
необычные исходящие соединения от наших хостов.
Zeek здесь незаменим, потому что даёт не только алерты, а саму сетевую картину.
Сигнатурные детекторы
Они используют Suricata alerts.
Например:
-
попытки эксплуатации CVE;
-
веб-атаки;
-
обращения к C2;
-
trojan activity;
-
suspicious user-agent;
-
RDP/FTP/SSH scanning;
-
ET Open signatures.
Suricata даёт хороший сигнал, если событие уже известно и описано правилом.
Детекторы компрометации наших хостов
Отдельный важный класс — это подозрительная активность не снаружи внутрь, а наоборот.
Например:
-
наш сервер начинает ходить к большому числу внешних IP;
-
появляется странный исходящий трафик;
-
резко растёт объём исходящих соединений;
-
хост начинает вести себя как сканер;
-
сервер обращается к подозрительным доменам.
Это может быть признаком компрометации.
Такой тип событий нельзя ловить той же логикой, что входящие атаки. Здесь важно учитывать направление трафика и принадлежность IP к нашим подсетям.
Фильтрация своих и чужих IP
Это звучит банально, но без этого система быстро начнёт стрелять себе в ногу.
На сервер мониторинга приходит зеркалированный трафик. В нём есть:
-
атаки на наши хосты;
-
обычный трафик клиентов;
-
исходящий трафик наших серверов;
-
транзитный шум;
-
легитимные проверки;
-
служебная активность.
Если просто считать «много соединений = атака», можно начать блокировать своих клиентов или даже собственную инфраструктуру.
Поэтому в детекторах используется несколько правил:
-
Не блокировать наши IP как источники атаки
-
Фокусироваться на трафике, направленном на наши публичные подсети
-
Иметь централизованный protected list
-
Проверять IP перед блокировкой на уровне модуля Cisco ACL
Даже если детектор ошибётся, модуль блокировки физически не должен уметь заблокировать наши собственные адреса.
Это важный предохранитель.
Автоматическая блокировка на Cisco ASR1000
Когда IP превышает заданный порог, система может автоматически добавить его в ACL на Cisco ASR1000.
Примеры событий, после которых возможна блокировка:
-
массовый SSH-брутфорс;
-
multi-vector атака;
-
подтверждённая попытка эксплуатации;
-
агрессивное сканирование портов;
-
повторяющиеся Suricata alerts высокого уровня;
-
сочетание поведенческих и сигнатурных признаков.
Блокировка выполняется по SSH: Python-скрипт подключается к маршрутизатору и добавляет deny-запись в расширенный ACL.
TTL для блокировок
Блокировки не вечные.
Для разных типов угроз задаётся разный TTL:
-
обычный сканер портов — несколько дней;
-
SSH-брутфорс — дольше;
-
подтверждённая попытка эксплуатации — до месяца;
-
multi-vector атака — также повышенный срок.
Отдельный демон периодически проверяет истёкшие блокировки и снимает их с ACL.
Это важно, чтобы ACL не рос бесконечно и чтобы случайные адреса, например за NAT, не оставались заблокированными навсегда.
Грабли №4. Состояние в базе и на роутере обязательно разойдётся
Информация о блокировках хранится локально.
Сначала это был JSON-файл, позже мы переехали на SQLite.
Но фактический источник правды — это маршрутизатор. Именно он реально режет трафик.
Со временем состояние в базе и состояние на Cisco начали расходиться.
Например:
-
IP есть в ACL, но его нет в базе;
-
IP есть в базе, но пропал из ACL;
-
старый код добавил блокировку, а новый код о ней не знает;
-
кто-то вручную изменил ACL;
-
демон не смог корректно снять блокировку.
В итоге появлялись странные ситуации: Telegram-команда «разблокировать IP» отвечала, что IP не найден, хотя на маршрутизаторе он был заблокирован.
Решение: регулярная синхронизация
Мы добавили механизм сверки.
Он периодически:
-
читает ACL с маршрутизатора;
-
сравнивает его с локальной базой;
-
добавляет в базу то, что есть на Cisco, но отсутствует локально;
-
восстанавливает ACL-записи, если они есть в базе, но пропали на роутере;
-
удаляет из ACL адреса, которые не должны быть заблокированы;
-
отдельно проверяет protected list.
Вывод
Если состояние хранится в двух местах, оно рано или поздно разойдётся.
Нужно не надеяться, что «всё всегда будет писаться атомарно», а проектировать явный механизм сверки.
От JSON-файла к SQLite
Первый вариант хранения состояния был максимально простой: JSON-файл со списком заблокированных IP.
Для прототипа это нормально.
Но потом появились проблемы:
-
файл читают несколько процессов;
-
один процесс пишет;
-
другой удаляет истёкшие блокировки;
-
дашборд параллельно читает;
-
нужно искать по истории;
-
нужно фильтровать по типу угроз;
-
нужно строить отчёты.
JSON быстро стал неудобен.
Он не даёт нормальных транзакций, индексов, конкурентного доступа и запросов.
Поэтому состояние переехало в SQLite.
Для такой задачи SQLite оказался почти идеальным вариантом:
-
один файл;
-
не нужен отдельный сервер БД;
-
есть транзакции;
-
есть индексы;
-
есть SQL;
-
можно включить WAL;
-
несколько читателей не мешают одному писателю;
-
удобно хранить историю блокировок.
Для локального состояния одного сервиса это намного лучше, чем JSON.
Внешние reputation API и лимиты
Для подозрительных IP мы дополнительно используем внешние reputation API, например сервисы уровня AbuseIPDB.
И тут быстро выяснилось: наивный подход не работает.
Если каждый подозрительный IP отправлять во внешний API, лимит бесплатного тарифа сгорает за минуты.
При 170 тысячах уникальных IP в день это особенно заметно.
Решение: кэш и контроль лимитов
Мы добавили:
-
кэш результатов по IP;
-
TTL кэша;
-
счётчик использованных запросов за сутки;
-
ограничение на обращения при приближении к лимиту;
-
предупреждения в Telegram;
-
fallback на уже накопленные данные.
После этого расход API-запросов упал с тысяч до сотни-двух в сутки.
Вывод
Любой внешний API в продакшене нужно сразу оборачивать в кэш, лимиты и fallback-логику.
Не потом, когда всё начнёт падать, а сразу.
Telegram-алерты
Telegram используется как основной канал оперативных уведомлений.
Но здесь тоже есть важный момент: если отправлять сообщение на каждое событие, канал быстро превратится в мусорку.
Алертов может быть очень много. Если человек получает сотни одинаковых сообщений, он просто перестаёт их читать.
Поэтому мы добавили дедупликацию.
Если по одному и тому же IP уже было сообщение, повторные срабатывания не создают новый спам, а обновляют существующее состояние: сколько раз повторилось, когда было последнее событие, какие детекторы сработали.
При этом критичные события всё равно отправляются отдельно:
-
подтверждённый exploit;
-
успешный SSH-login;
-
multi-vector атака;
-
факт блокировки;
-
ошибка синхронизации;
-
проблемы с поступлением данных;
-
исчерпание лимита внешнего API.
Telegram в такой схеме становится не просто «уведомлялкой», а рабочим интерфейсом для оперативной реакции.
Дашборд
Для визуального контроля используется веб-дашборд.
Он показывает:
-
общее количество событий;
-
динамику атак;
-
топ атакующих IP;
-
топ стран;
-
топ ASN;
-
топ портов;
-
последние алерты;
-
активные блокировки;
-
историю по IP;
-
статистику по сигнатурам;
-
распределение угроз по категориям.
По клику на IP можно посмотреть карточку:
-
страна;
-
ASN;
-
репутация;
-
какие детекторы сработали;
-
куда он подключался;
-
когда был впервые замечен;
-
был ли заблокирован;
-
история повторных срабатываний.
Важная особенность: дашборд сам выбирает источник данных.
Если выбран короткий период — берёт OpenSearch.
Если выбран период за неделю или месяц — идёт в ClickHouse.
Так мы не заставляем OpenSearch выполнять тяжёлые исторические агрегации, а ClickHouse не используем там, где удобнее быстрый real-time поиск.
Еженедельные отчёты
Помимо real-time алертов, система формирует еженедельный PDF-отчёт.
В отчёте:
-
общая статистика за неделю;
-
количество уникальных атакующих IP;
-
топ стран;
-
топ ASN;
-
топ портов;
-
топ типов угроз;
-
динамика SSH-брутфорса;
-
количество заблокированных IP;
-
самые активные источники;
-
наиболее частые Suricata signatures;
-
краткие выводы по неделе.
PDF отправляется на почту и в Telegram.
Тяжёлые агрегации для отчёта идут из ClickHouse. Это как раз тот случай, где колоночная база раскрывается лучше всего.
Watchdog: проверять нужно не только процессы
Ещё один важный компонент — watchdog.
Сначала кажется, что достаточно systemd: если процесс упал, systemd его поднимет.
Но в системах мониторинга безопасности этого мало.
Главный вопрос не только в том, жив ли процесс.
Главный вопрос:
Доходят ли данные?
Может быть такая ситуация:
-
Zeek работает;
-
Suricata работает;
-
Vector работает;
-
Redis работает;
-
OpenSearch работает;
-
ClickHouse работает;
но свежих событий в хранилище нет.
С точки зрения обычного мониторинга всё зелёное. С точки зрения безопасности система слепая.
Поэтому watchdog проверяет именно поток данных:
-
когда было последнее событие Zeek;
-
когда был последний Suricata alert;
-
растёт ли очередь Redis;
-
появляются ли свежие документы в OpenSearch;
-
появляются ли новые строки в ClickHouse;
-
нет ли задержки между источником и хранилищем.
Если данные перестают поступать, watchdog отправляет алерт.
Это помогает ловить самые неприятные проблемы: когда ничего не упало, но система перестала видеть сеть.
Что получилось в итоге
В итоге мы получили систему, которая:
-
работает на одном сервере;
-
использует open-source компоненты;
-
анализирует провайдерский трафик;
-
хранит историю;
-
даёт real-time поиск;
-
строит тяжёлую аналитику;
-
автоматически блокирует агрессивные IP;
-
отправляет алерты в Telegram;
-
формирует еженедельные отчёты;
-
позволяет расследовать инциденты по IP, портам, странам, ASN и сигнатурам.
Это не коммерческий SIEM «из коробки».
Но зато система полностью понятна, контролируема и принадлежит нам. Мы знаем, где какие данные лежат, как они обрабатываются, какие детекторы работают и почему IP был заблокирован.
Для инфраструктуры провайдера это очень важно.
Главные выводы
Если коротко собрать опыт в несколько пунктов:
1. Zeek и Suricata нужно использовать вместе
Suricata ловит известные угрозы по сигнатурам.
Zeek даёт сетевую телеметрию и позволяет строить поведенческие детекторы.
Вместе они закрывают гораздо больше сценариев, чем по отдельности.
2. Standalone Zeek не подходит для серьёзного трафика
Один поток быстро упирается в CPU и начинает терять пакеты.
Кластерный режим Zeek и балансировка через PF_RING сильно снижают потери и позволяют использовать несколько ядер.
3. Файл-посредник — источник проблем
Suricata → eve.json → Vector выглядело просто, но принесло проблемы с ротацией, checkpoint и потерей данных.
Redis между Suricata и Vector оказался надёжнее и удобнее.
4. OpenSearch и ClickHouse решают разные задачи
OpenSearch хорош для real-time поиска и расследований.
ClickHouse хорош для исторической аналитики и агрегаций по сотням миллионов строк.
Использовать одну базу для всего — плохой компромисс.
5. Данные нужно нормализовать до отправки в хранилище
Пустая строка в IP-поле может сломать batch в OpenSearch.
Грязные данные нужно чистить на входе.
6. Нельзя блокировать без protected list
Автоблокировка должна иметь жёсткую защиту от блокировки собственных IP.
Даже если детектор ошибся, модуль блокировки не должен уметь заблокировать свои подсети.
7. Состояние в двух местах обязательно разойдётся
Если блокировки есть и в базе, и на маршрутизаторе, нужна регулярная сверка.
Без неё рано или поздно появятся расхождения.
8. Внешние API нужно кэшировать
Reputation API полезны, но лимиты заканчиваются быстро.
Кэш, TTL и контроль расхода запросов обязательны.
9. Мониторить нужно поток данных, а не только процессы
Самая опасная ситуация — когда все сервисы «зелёные», но данные не поступают.
Watchdog должен проверять свежесть событий в хранилищах.
Вместо заключения
Эта система не появилась сразу в финальном виде.
Сначала были файлы, потом Redis. Сначала был только OpenSearch, потом появился ClickHouse. Сначала блокировки жили в JSON, потом переехали в SQLite. Сначала мы смотрели только на процессы, потом начали проверять сам поток данных.
Именно через эти итерации архитектура стала устойчивой.
Главный вывод простой: сетевой мониторинг безопасности — это не один инструмент и не одна база данных. Это конвейер, где каждый компонент должен делать свою работу и не пытаться быть универсальным решением для всего.
В нашем случае связка Zeek + Suricata + Vector + Redis + OpenSearch + ClickHouse + Python + Cisco ACL оказалась рабочим и недорогим фундаментом для собственной NSM/SOC-системы.
Если будет интерес, в следующей статье можно отдельно разобрать один из блоков подробнее: например, настройку Zeek cluster с PF_RING, схему таблиц ClickHouse, Vector-конфиг или логику детекторов и автоблокировки.
ссылка на оригинал статьи https://habr.com/ru/articles/1045372/