На первый взгляд задача кажется простой: нужно зафиксировать событие и предоставить интерфейс для его просмотра. Но на практике за этим стоит множество интересных инженерных решений: как отбирать события, хранить их, масштабировать систему, не потерять данные в случае аварии. И как сделать сервис полезным и для ИБ-специалистов, и для простых пользователей.

Привет! Я Владимир Атасунц, руководитель направления Security Services в MWS Cloud Platform. В этой статье расскажу о сервисе аудитных логов — базовом инструменте облака для контроля действий с ресурсами и анализа изменений в инфраструктуре.

Разберу:

  • зачем конкретно нужен такой сервис и какие сценарии использования предусматривает;

  • как он выглядит с точки зрения продуктовой модели;

  • какие требования легли в основу системного дизайна;

  • как жизненный цикл события нашел отражение в архитектуре и из каких компонентов она в итоге состоит.

Пойдём последовательно — от бизнес-сценариев к технической реализации.

Что такое сервис аудитных логов

Начнём с простого примера. Представьте, что вы — администратор облачного пространства. Утром вы создали виртуальную машину. Вечером возвращаетесь — а с ней что-то произошло: кто-то добавил ядра, кто-то её выключил. В команде есть коллеги, доступов много, действий — ещё больше.

Возникают базовые вопросы: что именно произошло и кто это сделал? Ответ на это и дают аудитные логи.

Аудитные логи — это запись о том:

  • какое действие было выполнено;

  • кто выполнил действие;

  • когда это произошло;

  • и чем всё закончилось (успехом или ошибкой).

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

Теперь поговорим про основных потребителей сервиса — по нашему мнению, это специалисты по информационной безопасности: ИБ-инженеры, аналитики, архитекторы, SOC-команды. Для них это рабочий инструмент для выполнения регуляторных требований, мониторинга событий безопасности, разбора инцидентов постфактум. 

При этом аудитные логи важны не только для ИБ. Ими пользуются и инженеры, которым нужно быстро понять причину изменений в инфраструктуре — например, кто и каким действием остановил кластер или изменил конфигурацию ресурса.

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

Продуктовое видение: базовая модель сервиса аудитных логов

Сегодня сервис аудитных логов — это must-have для любого облака. Такие решения есть у западных, азиатских и российских провайдеров. Мы не стали изобретать велосипед: посмотрели на существующие подходы, выбрали наиболее зрелую модель и адаптировали её под российские реалии, нашу архитектуру и продуктовые сценарии. 

В самом простом виде задача сервиса выглядит так: в облаке есть множество источников событий и их нужно куда-то собирать и где-то хранить. Внутри мы называем это common storage — общее хранилище, куда стекаются события со всего облака. Нюансы начинаются дальше — когда нужно дать пользователям управляемый доступ к этим событиям. Для этого мы ввели две отдельные сущности — коллектор и хранилище.   

Коллектор (Collector). Нужен, чтобы пользователь мог управлять тем, какие события собирать. Позволяет выбрать из всего объёма доступных событий только те, которые действительно интересны пользователю. В Google Cloud аналогичный ресурс называется Sink.

Пример сценария — события создания виртуальных машин пользователю не важны, а вот удаление ВМ или Kubernetes-кластера — критично.

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

Мы сознательно выделили хранилище в отдельную сущность и не стали объединять его с коллектором. Коллектор отвечает за отбор событий, а хранилище за их дальнейшее хранение или отгрузку. Такое разделение даёт гибкость: можно независимо развивать логику маршрутизации событий и способы их хранения. Через конфигурацию коллектора пользователь может сначала выбрать хранение данных в хранилище аудитных логов, а при желании через некоторое время переключить хранение на внешнюю систему.

В Google Cloud аналог называется log bucket, но нам не понравилось пересечение с сущностью S3 bucket сервиса Object Storage — S3-совместимое хранилище облака MWS Cloud Platform, поэтому мы упростили нейминг.

Требования к системному дизайну сервиса аудитных логов

Приступив к проработке системного дизайна, мы опирались на технические требования, бизнес-сценарии, продуктовую модель. В архитектуре облака мы разделяем Control Plane и Data Plane.

  • Control Plane отвечает за хранение конфигурации объектов, реализацию API и ресурсной модели.

  • Data Plane отвечает за выполнение действий. Например, Control Plane сохраняет конфигурацию виртуальной машины и передаёт её в Data Plane, который уже создаёт ВМ.

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

Технические требования

Чтобы понимать, что происходит в инфраструктуре, события нужно надёжно генерировать, доставлять и хранить. У нас были следующие требования:

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

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

Масштаб и низкая стоимость хранения. Источников событий много: сервисов много, пользовательских ресурсов ещё больше, пользователей — ещё больше. Все что-то делают. При этом хранить эти данные нужно долго, например для целей compliance. Значит, решение должно обеспечивать низкую стоимость хранения.

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

Ресурсная модель

Следующий шаг — спроектировать ресурсную модель сервиса и понять, как объекты будут взаимодействовать между собой.

Событие, главный объект. Их может быть очень много. В событии хранится вся необходимая информация: кто, что, когда и над каким ресурсом сделал.

Хранилище. Я уже о нём писал, но здесь важно зафиксировать характеристики. Storage может иметь ограничение по объёму — а может не иметь. Это зависит от потребностей клиента. Также можно ограничить срок хранения. Например: максимум 10 ГБ в течение 10 дней.

Коллектор — определяет, какие события нужно собрать и в какое хранилище их отправить.

С ресурсной моделью самого сервиса разобрались — теперь надо понять, как она ложится на модель облака.

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

Ниже находится проект — основной рабочий контейнер для пользователей. Именно внутри проекта разворачиваются виртуальные машины, Kubernetes-кластеры, а также создаются наши хранилища и коллекторы.

Есть и сценарий выгрузки во внешнее хранилище. Например, в Object Storage нашего облака. Это достаточно простой вариант, и мы его поддерживаем.

Ресурсная модель. Базовый сценарий
Ресурсная модель. Базовый сценарий

Потребность в централизованном хранилище

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

Например, можно создать отдельный проект безопасности — условный Project Sec — и разместить в нём общее хранилище. Туда будут отправляться события из других проектов. Доступ к этому проекту можно выдать только команде безопасности, которая занимается мониторингом и аналитикой. С точки зрения ресурсной модели это всё то же хранилище , просто в него попадает больше событий.

При этом возникает вопрос кросс-проектного доступа. Не любой коллектор может читать события в соседнем проекте или писать в чужое хранилище. Поэтому у коллектора появляется дополнительная характеристика — сервисный аккаунт, от имени которого выполняются операции чтения и записи. Это позволяет корректно разграничить доступ. Логика аналогична Object Storage: писать может только тот, у кого есть права.

Ресурсная модель. Кросс-проектный сценарий
Ресурсная модель. Кросс-проектный сценарий

Отображение жизненного цикла события в архитектуре сервиса аудитных логов

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

  1. Генерация.

  2. Сбор и доставка.

  3. Хранение.

  4. Доступ.

Генерация событий

С генерации всё начинается. Кто-то должен сообщить сервису, что произошло событие. Причём делать это нужно в едином формате и с полным набором данных — это не два-три поля, а довольно большой объём информации.

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

На этапе системного дизайна мы решили реализовать собственную библиотеку — по сути SDK, которую подключают сервисы облака. Можно было, например, пойти по классическому пути с REST API и отправкой событий напрямую. Но на решение повлияли особенности нашей инфраструктуры.

Мы работаем в Kubernetes, сервисы развёрнуты в нескольких кластерах. Возможны аварии, сетевые проблемы, перезапуски нод. Нам нужно минимизировать влияние таких ситуаций на качество сервиса. 

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

В итоге мы выбрали подход с локальной записью событий на диск. Сервис через библиотеку записывает событие в файл, а отдельный компонент считывает эти файлы и отправляет данные дальше. Так мы отделяем пользовательскую операцию от сетевой доставки. Библиотека инкапсулирует всю эту логику — сервисам не нужно реализовывать её самостоятельно.

Сбор и доставка событий

После генерации событий возникает следующая задача — собрать данные из разных кластеров и доставить их в единое хранилище.

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

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

  • аккумулировать поток событий со всех кластеров;

  • выстроить контролируемую модель потребления;

  • обеспечить высокую производительность записи.

Хранение событий

Теперь нужно было решить, где и как хранить данные. Мы помнили про два ключевых требования: низкая стоимость хранения и высокая производительность на запись. Сразу стало понятно, что хранить всё на локальных дисках — дорого. Поэтому уже на этапе системного дизайна мы приняли решение строить хранилище поверх Object Storage нашего облака. Дальше начался выбор инструмента.

Первым кандидатом был ClickHouse — привычный и понятный инструмент с поддержкой S3-совместимых хранилищ и зрелой экосистемой. Он позволяет строить таблицы с использованием Object Storage, однако в классическом self-managed сценарии ClickHouse копирует данные на диск, что не соответствовало нашей цели отказа от дисков.

Мы также рассматривали zero-copy replication, где между репликами синхронизируются в основном метаданные, а не сами данные, но на момент нашего системного дизайна этот режим в документации был помечен как «not production ready». Рисковать мы не стали.

Дальше мы посмотрели в сторону Apache Iceberg. Важно понимать, что Iceberg — это не полноценная база данных, а слой управления табличными данными поверх Object Storage. Он определяет, как хранить файлы, как вести метаданные и как обеспечивать консистентность.

Архитектуру Iceberg можно условно разделить на четыре компонента.

  • Первый — собственно Object Storage, где лежат файлы данных. Мы выбрали формат Parquet — колоночный, удобный и хорошо поддерживаемый.

  • Второй — каталог метаданных. Он хранит информацию о таблицах, версиях и связях между файлами. Мы рассматривали несколько вариантов и в итоге выбрали Nessie — по нашему мнению, на момент проработки архитектуры это был наиболее зрелый и удобный для production-сценариев каталог.

  • Третий — сервисы обслуживания таблиц. Это процессы, которые удаляют устаревшие файлы, объединяют мелкие файлы в более крупные и обновляют метаданные. Это важно как для экономии места, так и для производительности. Для этих задач мы используем Spark — проверенный инструмент, который хорошо подходит для фонового обслуживания таблиц и обработки данных в Iceberg.

  • Четвёртый — движок для выполнения запросов, который работает с данными и метаданными Iceberg-таблиц. Здесь мы рассматривали три варианта: Trino, StarRocks и ClickHouse. Мы выбрали StarRocks, о причинах расскажу дальше. 

Trino — мощный и зрелый query engine с поддержкой Iceberg и инструментами обслуживания таблиц. Но у него есть архитектурная особенность: координатор не масштабируется из коробки. Решения существуют, но они достаточно сложные.

StarRocks — полноценная аналитическая база данных, которая умеет работать поверх Iceberg и хорошо масштабируется. В её архитектуре есть frontend-ноды, которые планируют выполнение запроса, и compute-ноды, которые выполняют его. При нехватке ресурсов архитектура позволяет горизонтально добавлять необходимые ноды.

ClickHouse, несмотря на симпатию к инструменту, на момент проработки архитектуры не предоставлял нужного нам набора операций поверх Iceberg для реализации полного пайплайна. 

В результате мы остановились на StarRocks.

Пользовательский доступ к данным

Финальный этап жизненного цикла события — предоставить пользователю удобный доступ к данным. Для этого у нас есть Control Plane, написанный на Kotlin. Он реализует API, поддерживает ресурсную модель и позволяет выполнять запросы с заданными фильтрами.

Пользователь взаимодействует именно с этим уровнем. Внутри Control Plane обращается к query engine и возвращает результат. Возможно, в будущем добавим собственный язык запросов — по аналогии с тем, как это реализовано в Google. Пока же мы ограничились фильтрами и преднастроенными возможностями поиска.

Архитектура сервиса аудитных логов: реализация на практике

Как я уже писал, архитектура сервиса отражает жизненный цикл события. Ниже расскажу, как она выглядит на практике и из каких компонентов состоит. 

Генерация аудитных событий

Как я уже упоминал выше, у нас есть библиотека, с помощью которой сервисы записывают аудитные события. Backend, живущий в Kubernetes, подключает эту библиотеку, генерирует события и записывает их на диск. Библиотеку мы реализовали под основные языки нашего облака — в первую очередь под Kotlin и Go.

Помимо базовой функциональности «записать событие», мы предусмотрели middleware, которую можно подключить в Control Plane на уровне обработчиков запросов. Middleware автоматически собирает большую часть данных, необходимых для аудита. Сервису остаётся передать только специфическую часть: какой ресурс, какая операция и чем она завершилась.

События записываются в файл в каталоге на диске, подключённом через HostPath. Дальше возникает задача корректно прочитать эти данные и отправить их дальше.

Для корректной обработки файлов, в которые идет активная запись, мы реализовали отдельный сервис, условно названный logrotate (это наш собственный компонент, просто одноимённый с известной утилитой). Этот компонент вместе с библиотекой выводит файлы из режима записи и помечает их как готовые к дальнейшей обработке. После этого файлы можно забирать на сбор. Logrotate развёрнут как DaemonSet во всех кластерах, поэтому обслуживает все сервисы.

Архитектура сервиса аудитных логов. Генерация событий
Архитектура сервиса аудитных логов. Генерация событий

Локальный сбор и архив данных

Следующий компонент — сервис, который читает подготовленные файлы и отправляет их дальше. Этот сервис мы называем коллектором. Он выполняет две задачи:

  1. Отправляет данные дальше в пайплайн.

  2. Сохраняет сжатые данные во временный локальный архив.

Зачем нужен локальный архив? Ответ — надёжность. Теоретически сеть может упасть на час или два. В такой ситуации события не должны теряться. Поэтому локальный архив хранит данные до тех пор, пока они не будут успешно доставлены в целевое хранилище. Очевидная проблема — архив не бесконечный. Поэтому со старта разработки сервиса мы настроили метрики и алертинг и следим за потреблением архива, чтобы не допустить его переполнения.

Коллекторы также запущены как DaemonSet во всех кластерах. В итоге все коллекторы отправляют данные в Kafka для дальнейшей обработки.

Архитектура сервиса аудитных логов. Локальный сбор данных
Архитектура сервиса аудитных логов. Локальный сбор данных

Очередь событий

Kafka в пайплайне сбора событий — это центральная точка агрегации. После этого начинается этап обработки, который делится на две части:

  • внешняя выгрузка;

  • внутренняя запись в наше хранилище.

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

Внешняя выгрузка данных

Внешний экспортер читает данные из Kafka. Дальше происходит следующее:

  1. События группируются по проектам и организациям.

  2. Экспортер обращается к Control Plane по внутреннему API.

  3. Получает информацию о коллекторах, работающих с этими проектами и организациями.

  4. Определяет:

    • какие события нужно оставить;

    • куда их выгружать;

    • от имени какого сервисного аккаунта выполнять операцию.

После этого экспортер выгружает данные в сконфигурированные пользователем внешнее хранилище. Здесь мы подумали про надёжность и предусмотрели плохие сценарии, например:

  • проблемы с сетью;

  • некорректно настроенный сервисный аккаунт;

  • удалённый пользователем бакет.

Поэтому мы сохраняем данные во временном хранилище и реализуем механизм повторной отправки в течение заданного времени.

Архитектура сервиса аудитных логов. Внешняя выгрузка данных
Архитектура сервиса аудитных логов. Внешняя выгрузка данных

Внутреннее хранилище данных

Внутренний экспортер (Iceberg Exporter) также читает данные из Kafka.

Он реализует два сценария:

  1. Запись полного набора событий для внутренних нужд в Common Storage.

  2. Запись в пользовательские хранилища.

В качестве Common Storage мы используем то же хранилище, который предоставляем пользователям. Разница лишь в конфигурации: для внутреннего хранения используется другой коллектор и другие параметры хранения.

Архитектура сервиса аудитных логов. Внутреннее хранилище
Архитектура сервиса аудитных логов. Внутреннее хранилище
Архитектура сервиса аудитных логов. Полная схема
Архитектура сервиса аудитных логов. Полная схема

Ниже расскажу про некоторые особенности нашего внутреннего хранилища данных:

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

Чтобы избежать этой проблемы, мы добавили прослойку-агрегатор, которую назвали Coordinator. Она накапливает данные в течение заданного интервала (например, двух минут) и выполняет единый commit. Это своего рода координатор записи.

Хранилище внутри Iceberg. Внутри нашего Iceberg-хранилища пользовательское хранилище — это, по сути, отдельная таблица.

Мы рассматривали другие варианты:

  • использование view;

  • комбинированную модель (общая таблица + view для каждого хранилища).

Но оба варианта не подошли. И вот почему: 

  • view дают худшую производительность;

  • комбинированный вариант сложнее в реализации и затрудняет контроль объёма данных.

Таблица оказалась самым простым и предсказуемым решением.

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

Ограничения и производительность чтения. Напомню, одно из требований — адекватная производительность на чтение.

Событий может быть много, глубина хранения — большой. Технически обеспечить высокую производительность на больших объёмах сложно. Поэтому здесь вступает продуктовый подход.

У хранилища есть две характеристики:

  • максимальный объём;

  • максимальное время хранения.

В инфраструктуре large enterprise десятки ГБ могут заполниться очень быстро — это неудобно. Поэтому объём решили не ограничивать. А вот срок хранения мы ограничили — 30 дней.

По нашим исследованиям, этого достаточно для реализации основных сценариев: расследований, диагностики, мониторинга.

Если требуется более длительное хранение, пользователь может настроить выгрузку в Object Storage и строить аналитику самостоятельно.

Для внутренних хранилищ мы предоставляем интерфейс просмотра. Для внешних выгрузок (Object Storage и другие будущие направления) интерфейса просмотра не предусмотрено — в том числе из-за требований к производительности.

Что в итоге

Сервис аудитных логов — важный компонент облака, базовый инструмент контроля и анализа событий.  «Посмотреть логи» — простая и понятная потребность пользователя, но за ней стоит большая и довольно интересная работа по проектированию: переложить жизненный цикл события на архитектуру сервиса и выбрать оптимальные решения для каждого его компонента. 

Сервис продолжит развиваться. Сейчас аудитные логи доступны в Preview — сервис предоставляется бесплатно и предназначен для тестирования и знакомства с его возможностями. 

В ближайших планах — кросс-проектная выгрузка событий, подключение новых сервисов как источников аудитных логов, интеграция с SIEM через отдельный Collector, а также развитие интерфейса и более гибких фильтров в UI. 

Попробуйте сервис и поделитесь с нами обратной связью в сообществе MWS Cloud Platform.