Вступление
Всем привет, меня зовут Денис, и я старший инженер инфраструктурной безопасности в Ozon. Эта статья — продолжение цикла про osquery и Fleet.
Предыдущие статьи вы можете почитать здесь и здесь.

В статье хочу поделиться радостью и «болью» опыта эксплуатации связки Fleet и osquery в масштабе e-commerce/highload.
Этот опыт будет полезен тем, кто ещё только думает об этой связке и планирует её внедрять, а также тем, кто уже внедрил и эксплуатирует.
Osquery мы эксплуатируем на рабочих станциях и серверах под управлением операционных систем MacOS, Windows и Linux.
Для начала стоит напомнить, что такое osquery и Fleet и почему они так классно друг друга дополняют.
Osquery — opensource, написанный на С++, представляет собой агента, запущенного на хостовой (и не только) ОС, который может предоставить большое количество информации о вашей системе и событиях в виде СУБД.
Также osquery имеет два вида запросов:
по расписанию (scheduled query), когда определённый запрос выполняется на агенте и результат отправляется в виде json в http коллектор/kafka topic;
по требованию (live-query/distributed query), когда запрос выставляется в сервисе Fleet в endpoint /api/1/osquery/distributed/read. Osquery его читает/забирает (каждые N секунд), далее выполняет и шлёт ответ в /api/v/1/osquery/distributed/write, который становится доступным во Fleet.
Fleet — софт, существующий в двух редакциях — бесплатной и платной, написанный на Go и представляющий собой монолит FE и BE в одной коробке. Отлично деплоится в Kubernetes и позволяет управлять «флотом» osquery:
распространять на них конфиг;
запланировать запросы;
выполнять live-query.
Какие кейсы мы решаем с помощью osquery/Fleet
События ОС
Osquery выполняет здесь две задачи: извлечение событий и транспорт событий.
Это классный пойнт, когда в одном инструменте можно объединить обе эти функции при решении задач по доставке событий из kernel audit одновременно с событиями в shell или по контролю целостности конкретных файлов, получив их в одном удобно читаемом виде json.
Наш стандартный набор событий, который мы собираем:
- process events,
- socket events,
- file events,
- user events (logons, shell history),
- file integrity.Управление уязвимостями
Благодаря таблицам osquery, которые содержат информацию об операционной системе и пакетах/приложениях, установленных на ней, мы имеем крутую возможность оперативно собирать наши «активы» в рамках процесса Vulnerability Management. И при выходе очередной уязвимости автоматизированно соотносить её с активами, тем самым существенно разгружая L1 SOC при триаже уязвимостей и принятии дальнейших решений.Device Compliance (проверка устройства на соответствие требованиям безопасности)
На основе таблиц osquery, которые содержат информацию о состоянии тех или иных компонентах ОС, мы имеем возможность оценить «базовую безопасность» устройства, с которого происходит процесс аутентификации на границе нашего периметра инфраструктуры.Расследование инцидентов
Благодаря возможности osquery выполнить «немедленный»/live-запрос, инженеры группы расследования инцидентов могут оперативно искать IOC (индикаторы компрометации) на инстансах параллельно триажу логов SIEM. Это существенно уменьшает время оценки компрометации инфраструктуры и также увеличивает гибкость и оперативность перебора векторов атак, в случае если предполагаемые IOC находятся в «слепой зоне» уже сформированных корреляций в SIEM.
Как мы себя успешно ддосили
Пожалуй, это самая интересная часть статьи, а именно на какие вещи нужно обязательно обратить внимание при «приготовлении» osquery и Fleet, чтобы в случае непредвиденных ситуаций не «положить» собственную инфраструктуру.
На моей практике было четыре кейса, когда неверные настройки агента osquery могли «положить» инфраструктуру:
Утилизация сетевых каналов — линейное увеличение трафика (в течение 7 часов), успели дойти до 4 Гбит/с. Дневной пик при нормальной работе тогда составлял 150 Мбит/с. ЦОДы не пострадали, но мы «положили» каналы до наших сортировочных центров и складов, парализовав операции.
Утилизация сетевых каналов — всплеск трафика (в течение 5 минут), успели дойти до 12 Гбит/с. Дневной пик при нормальной работе тогда составлял 200 Мбит/с. ЦОДы не пострадали, но влияние было, как и в первом кейсе.
Утилизация блочных устройств — линейное увеличение размеров локальной базы данных событий osquery до 1 Гбайт на группе инстансов. Влияния на хосты не было, так как это вовремя заметили. Когда рассчитали дефолтные настройки, увидели, что риск занять всё свободное место и вывести из эксплуатации большое количество инстансов был не нулевым. Но мы (SOC) потеряли события.
Утилизация блочных устройств — линейное увеличение размеров логов rsyslog (в течение суток), успели дойти до 10 Гбайт. Так как под /var/log на той группе инстансов выделялся отдельный раздел на 10 Гбайт, мы успешно «положили» кластер, к счастью, это был не прод.
Забегая вперёд, кратко опишу нашу инфру, чтобы вы понимали больше деталей и было меньше вопросов.
Транспорт событий: osquery --> LB --> Fleet --> kafka-rest-proxy --> kafka --> ETL --> SIEM.
Первый кейс. Проблема с отправкой запланированных запросов
Что случилось
Погибшая база данных не позволяла аутентифицировать каждое событие, отправляемое от osquery. Тем самым каждое не отправленное событие osquery ставил в очередь и в рамках интервала пытался отправить повторно.
Как исправляли
На момент инцидента, опираясь на документацию, у разработчиков osquery не было рабочего решения проблемы failed re-try для событий. Я пошёл в официальный канал Slack разработчика и рассказал о проблеме с приведением графиков/метрик, надеясь на то, что я где-то проглядел документацию. Но мне ничего толком не ответили. И на том этапе инцидента мы лишь «укрепили» наши базы данных, увеличили интервалы отправки событий и количество событий, отправляемых за один интервал.
Но спустя пару месяцев, читая changelog новых релизов osquery, увидел новую фичу logger_tls_backoff_max, которая вышла в релизе 5.12.1 (25 марта) и полностью решала нашу проблему. До конца неизвестно, послужил ли мой репорт/просьбы о помощи в Slack для разработки данной фичи. Мы сразу же принялись тестировать эту фичу и на тестах полностью подтвердили, что она решает наши проблемы. На скрине слева — без этой фичи, справа — с ней. В обеих ситуациях мы специально ломали функциональность Fleet, чтобы он не мог принять запрос. На графиках LB наглядно видна разница, и это всего лишь один хост.

На что обратить внимание
Logger_tls_backoff_max — время, в рамках которого logger_tls_period будет экcпоненциально увеличиваться при ошибке отправки событий. А по истечении этого времени и при отсутствии успешной отправки накопившиеся события будут удалены. При этом интервалы отправок начнутся с первоначально установленного. Дефолтное значение — 3600 секунд, мы же используем 10800 секунд (3 часа).
Второй кейс. Проблема с отправкой live-запросов
Что случилось
Fleet каждый час выставляет для всех агентов osquery свой live-запрос, в рамках которого он обновляет всю информацию о хосте: информация об ОС, софт, пользователи, соответствие политикам.
Просевший перформанс баз данных сыграл злую шутку над liveness/readiness-проверками в Kubernetes для Fleet, и он как бы упал, но не до конца. А именно он успевал отдать live-запрос (/api/v1/osquery/distributed/read). Агент osquery его получал, а вот принять payload обратно(/api/v1/osquery/distributed/write) уже не мог. Он выполнял 3 попытки отправки и ждал следующего интервала, который равнялся 10 секундам. Таким образом, пока мы не сняли всю нагрузку с Fleet, он не смог пройти все проверки Kubernetes и не вернулся к былому перформансу. Добавление подов, как вы понимаете, только ухудшало ситуацию (дополнительные коннекты/запросы в БД, которая и так просела в перформансе).
Как исправляли
Увеличили интервал для live-запросов до 60 секунд. Уменьшили количество попыток отправки событий с 3 до 1.
Изменили параметры для liveness/readiness-проверок в Kubernetes для Fleet.
В очередной раз «укрепили» базы данных.
На что обратить внимание
- distributed_interval — интервал проверки новых live-запросов, дефолтное значение — 60 секунд, но мы использовали 10, вернулись на 60 секунд.
- distributed_tls_max_attempts — количество попыток агента osquery получить запрос / отправить результат за один интервал, дефолтное значение — 3, мы уменьшили до 1.
Третий кейс. Неверный расчёт количества отправляемых событий с общим количеством событий, генерируемых агентом
Что случилось
Время шло, количество запланированных запросов росло и, как следствие, количество отправляемых событий тоже. В один прекрасный момент мы «подпрыгнули» от алерта SOC о том, что событий нет, точнее, не все инстансы в онлайне их шлют. Сначала мы не знали, на что смотреть, ведь по нашим сетевым метрикам ничего не поменялось. Но потом коллеги из SOC подсказали, что у большинства хостов заголовок дата/время в событии сильно отличаются от текущих. А так как такие события по регламентам нашего SOC не принимаются для долговременного хранения получилось, что мы эти события теряли.
Мы взяли первый попавшийся хост, который «не отправлял» события, и посмотрели, что же всё-таки он отправляет и отправляет ли. Нашему удивлению не было предела, когда увидели, что хост послушно отправляет события, просто это события 2-недельной давности. Оказалось, что количество событий, генерируемых хостом за единицу времени, равную интервалу (logger_tls_period), не помещалось в количество событий, отправляемых за интервал (logger_tls_max_lines). Таким образом, события складывались в локальный дисковый буфер, увеличивали занимаемое место и ждали своей очереди отправки, которая просто увеличивалась. Проблема с «распуханием» локального буфера на блочном устройстве, разумеется, тоже была, но на фоне потери событий выглядела не первостепенной.
Как исправляли
Временно (на 30 минут) выставили во Fleet значение параметра buffered_log_max = 1000, чтобы «опустошить» локальные буферы событий всех агентов, которые были в тот момент онлайн.
На постоянной основе выставили buffered_log_max = 60 000 (что равнялось количеству событий, сгенерированных за 2 часа работы агента у нас) вместо дефолтного 1 миллиона. Пересчитали количество событий, генерируемых хостом, и поменяли logger_tls_period и logger_tls_max_lines.
Для отправки событий мы перешли от подхода «редко, но помногу» к «часто, но по чуть-чуть». Таким образом, через пару часов идеально ровных сетевых графиков утилизаций каналов мы увидели «зубчики». Предположительно, это означало, что очередь успешно опустошилась и больше не заполнялась. Метрики из SIEM подтверждали наши предположения.
На что обратить внимание
- Logger_tls_period — интервал проверки буфера накопленных событий и их отправка. Дефолтным значением являются 3 секунды. По нашему мнению, это слишком экстремально малый интервал как с точки зрения производительности хоста, на котором работает osquery, так и для нашей инфраструктуры. У нас были опыты с увеличением этого значения до 300 секунд, но замечали частые всплески каждые 5 минут. Опытным путём мы пришли к значению 30 секунд, когда всплесков минимум и утилизация ресурсов равномернее.
- Logger_tls_max_lines — количество событий, отправляемых за интервал (logger_tls_period). Учитывая дефолтное значение для logger_tls_max_linesize («вес» одного события) в 1 Мбайт, а дефолтное значение для logger_tls_max_lines в 1024 события, это достаточно высокое значение, которое мы уменьшили до 256 путём тестов и наблюдений.
Четвертый кейс. x3-дублирование событий из kernel audit
Что случилось
Время шло, количество покрываемых хостов росло, и была произведена очередная волна раскатки osquery на серверы Linux.
Через сутки мы «положили» кластер одного приложения. Когда стали разбираться, оказалось, что катастрофически распух /var/log/syslog. Но мы долго не понимали своей причастности к произошедшему.
Оказалось, что у systemd есть компонент systemd-journald-audit.socket, который при запуске родительского systemd подписывается на netlink socket на «особых» правах, а именно не занимая квоту в 1 «подписанта» на этот сокет.
Благодаря параметру audit_allow_config=true при своём запуске osquery меняет правила kernel audit, по которым он будет отправлять события в netlink.
И таким образом osquery начинает получать события от kernel audit, и параллельно systemd-journald-audit.socket как компонент journald. У rsyslog есть модуль imjournal, который позволяет сохранять обратную совместимость с journald и писать логи в привычные файлы. Теперь события от kernel audit идут к трём получателям:
osquery,
journald (через модуль systemd-journald-audit.socket),
rsyslog (через модуль imjournal от journald).
Если с osquery всё понятно — ведь мы и так хотели получать события, — то с остальными двумя надо было разбираться. C journald оказалось всё не так страшно, ведь он имеет встроенный триммер логов и не даёт разрастись локально больше, чем указано в параметре SystemMaxUse. А вот с rsyslog, как вы понимаете, было сложнее. Там отдельно надо настраивать logrotate на более частый тримминг логов, иначе беда. Что, в принципе, и случилось.
Как исправляли
В пакет osquery в «pre-install» scripts зашили отключение systemd-journald-audit.socket, в «post-uninstall» — включение.
На что обратить внимание
Systemd-journald-audit.socket — если вы не планируете получать события в journald, то выключить. Если же вы всё-таки хотите получать события от kernel audit в journald/rsyslog — обратите внимание на их настройку.
Заключение
Несмотря на те проблемы, с которыми мы столкнулись, osquery остаётся классным инструментом для решения задач ИБ, и мы планируем ещё большее покрытие данным агентом нашей инфраструктуры. Надеюсь, в статье вы нашли для себя полезный опыт, который убережёт ваши инфраструктуры от подобных проблем и инцидентов.