Предисловие по поводу используемой в статье версии Tetragon
Изначально в докладе, что послужил основой для этой статьи, было указано, что используется версия 1.4.0, которая была последней на тот момент. Но уже после доклада в ходе более тщательного тестирования было выяснено, что версия 1.4.0 имеет некоторые существенные баги, которые могут ломать логику некоторых политик. Поэтому в статье по возможности будет указываться минимально необходимая версия Tetragon для конкретных возможностей и прочего. Через некоторое время после 1.4.0 была выпущена версия 1.4.1, в которой исправлены вышеупомянутые баги, поэтому рекомендуется использовать её, как самую последнюю.
Основные цели статьи
Сразу определим основные цели статьи:
- уменьшение числа событий до минимально необходимого;
- обзор возможностей Tetragon при составлении политик и возможные ограничения.
Введение
Подсистема auditd закрывала большую часть основных потребностей аудита безопасности многие годы, альтернативой ей были проприетарные решения от вендоров ИБ. Мы также опирались на формат сообщений auditd в решениях класса SIEM/EDR, поэтому для него за годы накоплено много правил и экспертизы, которая актуальна и используются до сих пор. Ситуация изменилась, когда Linux стал основой для платформы контейнеризации приложений сначала в виде Docker, а потом уже и в виде оркестратора Kubernetes. Сразу появилась задача обеспечения безопасности с учётом специфики подобных сред, но сам auditd был не готов к этому, хотя разработчики этой подсистемы знают о проблеме и прорабатывают её решение (ссылка на Issue в репозитории auditd), но системы нужно защищать ещё вчера, а сегодня потребность в этом ещё выше. Поэтому заинтересованные лица пошли другим путём, развивая технологию eBPF в ядре Linux и используя её как основу для мониторинга и аудита системы. Сама технология далеко не нова и уходит корнями в 90-ые с появлением BPF (Berkeley Packet Filter) – виртуальной машины, работающей на уровне сетевого стека для работы с сетевым трафиком. В ядре Linux эту идею улучшили и расширили (e означает extended), предоставляя широкий доступ к сущностям ядра. На базе eBPF с момента его создания появилось значительное число решений начиная от обеспечения сетевой связанности и заканчивая мониторингом ресурсов и аудитом безопасности. В нашем случае для продукта обеспечивающего защиту контейнерных сред нужен был готовый eBPF-движок, который позволит гибко и эффективно обеспечивать мониторинг защищаемых активов, так как решать эти задачи средствами auditd невозомжно.
Сегодня решений использующих eBPF существует достаточно: Tracee, Falco, KubeArmor и т.д. Но из всех них мы выбрали компонент Tetragon, который разработан в рамках проекта Cilium. На этот счёт есть множество причин:
Tetragon активно развивается, что приводит к досадным ошибками и нарушению функционала, так и случилось в версии 1.4.0 (ссылка на Issue, плюс дубликат), когда в ходе подготовки релиза были не учтены некоторые изменения, что привело к искажению некоторых полей в событиях, позже это было исправлено в релизе 1.4.1, но такие проблемы не единичны и периодически всплывают новые (ссылка на Issue);
Но это всё кажется не особо существенной проблемой на фоне того, что существующее и проверенное решение в виде auditd в принципе не поддерживает контейнеры, хотя является достаточно зрелым и документированным решением. Да, мы не можем переиспользовать контент и нам потребуется начать с условного нуля в случае Tetragon, но это уже что-то и обеспечивать безопасность контейнеров с ним можно сегодня, а не в неопределенном будущем. Да, auditd предоставлял удобный и высокоуровневый интерфейс для аудита, а с Tetragon придётся работать с ядром на достаточно низком уровне, что потребует больших компетенций, но с другой стороны это даже в плюс, так как развивать их в любом случае нужно, так как атаки становятся сложнее и старые решения их просто не видят.
Поэтому давайте начнём путь к построению эффективных политик.
- Tetragon не содержит лишнего, он минималистичен, в случае других решений мы будем тащить в защищаемую систему лишние компоненты, которые могут создать дополнительную поверхность атаки, либо конфликтовать с другими решениями ИБ, вызывая сбои;
- также механизм Tracing Policies, что предлагает Tetragon позволяет гибко настраивать политики мониторинга, которые он потом преобразует в нужные eBPF-программы, это отличается от концепции других решений, где существуют уже готовые модули с ограниченными возможностями кастомизации, подразумевая, что основной задачей пользователя будет выбор нужных типов событий, а так как формат сообщений разных решений несовместим, то переезд с одного на другое потребует существенной переработки экспертизы и связанных с ней подсистем;
- по имеющимся в свободном доступе тестам Tetragon имеет самую низкую задержку при генерации события, иными словами он самый быстрый среди конкурентов, естественно что это также зависит от качества разработанных политик для него, но уже хорошо, что он не вносит лишние искажения;
- специфичный плюс для нашего продукта – Tetragon написан на Go, который также используем мы, что позволяет не нести в продукт лишних зависимостей, а также в некоторых случаях позволяет разработчикам продукта выявлять баги в Tetragon и сообщать о них сообществу, которое его развивает.
- Это, однако, не освобождает от минусов, которые у Tetragon тоже есть:
Tetragon активно развивается, что приводит к досадным ошибками и нарушению функционала, так и случилось в версии 1.4.0 (ссылка на Issue, плюс дубликат), когда в ходе подготовки релиза были не учтены некоторые изменения, что привело к искажению некоторых полей в событиях, позже это было исправлено в релизе 1.4.1, но такие проблемы не единичны и периодически всплывают новые (ссылка на Issue);
- также Tetragon может не поддерживать нужные структуры, которые используются в перехватываемых функциях ядра, это приводит к тому, что приходится ждать доработок, либо дорабатывать код самостоятельно (существует и такой
- опыт), прежде чем появится возможность отслеживать определенные угрозы;
- отдельные вопросы есть к документации, она скудная, альтернативных материалов существует не так много, примеры из документации могут не работать в принципе, что приводит к тому, что приходится проводить многочисленные эксперименты, чтобы понять, как правильно работать с Tetragon;
- некоторый функционал (например, uprobes) не особо развивается и предлагает достаточно базовый уровень поддержки.
Но это всё кажется не особо существенной проблемой на фоне того, что существующее и проверенное решение в виде auditd в принципе не поддерживает контейнеры, хотя является достаточно зрелым и документированным решением. Да, мы не можем переиспользовать контент и нам потребуется начать с условного нуля в случае Tetragon, но это уже что-то и обеспечивать безопасность контейнеров с ним можно сегодня, а не в неопределенном будущем. Да, auditd предоставлял удобный и высокоуровневый интерфейс для аудита, а с Tetragon придётся работать с ядром на достаточно низком уровне, что потребует больших компетенций, но с другой стороны это даже в плюс, так как развивать их в любом случае нужно, так как атаки становятся сложнее и старые решения их просто не видят.
Поэтому давайте начнём путь к построению эффективных политик.
Поиск функции ядра и общий анализ безопасности
Очень важный и ключевой момент, который требует целой отдельной статьи. Подробный перечень шагов и нюансы вопроса разобраны в статье Сергея Зюкина (TODO: ссылка на статью Сергея, когда он будет опубликована). Здесь же выделим основные моменты и вопросы, которые вы должны поставить:
- насколько вам нужна защита от продвинутых атак и способов обхода аудита системы?
- важно ли вам наличие возвращаемого значения функции или системного вызова?
- важно ли вам получение не только успешных попыток вызова функции или нужны
- ещё и неудачные?
- сколько производительности узла вы готовы отдать под аудит безопасности?
- есть ли у вас альтернативные средства мониторинга, которые специализируются на конкретных атаках (например, сетевых)?
- поддерживает ли Tetragon нужные структуры, используемые в функции/вызове/LSM-хуке
Дело в том, что достаточно давно аудит системы можно было производить через подсистему auditd, но очень ограничено – только файловые операции и системные вызовы. Последние особо уязвимы к перехвату и замене на модифицированные (TODO: материал по хукингу системных вызовов или сторонние статьи?), либо существуют альтернативные интерфейсы для работы в обход системных вызовов, к примеру тот же интерфейс io_uring (TODO: ссылка на пост? или статью?). Файловые операции также уязвимы к некоторым атакам, которые позволяют обойти правила мониторинга (TODO: стоит ли здесь рассказывать про трюк с жёсткими ссылками?). До появляения eBPF и инструментов на базе этой технологии существовала возможность разработки модуля ядра, работающего с подсистемой LSM (Linux Security Modules), которая имеет на вооружении LSM-хуки, через которые проходят функции ядра прежде чем получить возможность совершить действие. Некоторые вендоры использовали этот подход в своих продуктах и получали преимущество над другими в плане качественного покрытия аудита системы. Tetragon же позволяет более глубоко работать с внутренностями ядра Linux, поэтому можно производить существенно качественный мониторинг без необходимости собирать модуль ядра. Ему также доступны LSM-хуки или любые другие функции ядра за счёт использования механизма kprobes, также можно использовать более привычные варианты вроде работы с системными вызовами или трейспоинтами ядра, но с учётом, что их возможности ограничены и существуют риски.
Также существуют нюансы, связанные с возвращаемым значением фунции, потому что те же функции ядра не особо документируют их значения, чуть лучше ситуация с LSM- хуками, так как они предназначены для разрешения или ограничения доступа к ресурам, поэтому лучше документированы. Лучше всего ситуация с возвращаемым значениям у системных вызовов, так как они являются официальным интерфейсом для программирования в ОС, поэтому хорошо документированы, а также играют роль этакой точки входа, поэтому в теории любой вызов в ОС должен пройти через них.
И тут мы двигаемся к третьему вопросу, так как именно перехват системного вызова может предоставить все попытки его использования, чем ниже мы двигаемся в своеобразной лестнице вызовов внутрь ядра, тем больше теряем информации, поэтому для того же LSM-хука не все вызовы могут дойти, так как будут отброшены либо промежуточной функцией ядра, либо другим LSM-хуком, который вы не собирались использовать, так как он вам не нужен, либо не подходит для мониторинга. С одной стороны это не особо критично, но в некоторых случаях может не соответствовать вашим целям.
И вот тут мы можем вспомнить о производительности. В идеале существует желание производить аудит любого действия в системе, но это технически невозможно, так как счёт их идёт на десятки-сотни тысячи в секунду и миллионы в минуту. Следует просто принять тот факт, что всю систему мониторить невозможно, поэтому нужно определить особо критичные зоны и работать по ним. Некоторые из них также могут выпасть из аудита по причинам производительности. Одна из таких зон – сетевое взаимодействие, ещё при работе с auditd в нашей практике приходилось отключать мониторинг сети полностью, что может показаться не очень приемлемым решением, но тут есть момент, что альтернативные решения по отказу от мониторинга файловой активности ещё хуже, так как теряется ещё больше информации о происходящем в системе.
Плюс существуют отдельные решения для анализа сетевой активности, которые выполняют эту работу лучше и качественее, чем пытаться делать подобное средствами auditd или даже "всемогущего" Tetragon (или другого eBPF-решения). NTA позволяют не только анализировать трафик с нескольких узлов, что строит более полную картину, но и разбирать сетевой трафик до самых высокоуровневых слоёв сетевой модели. Также они могут ловить атаки, которые не характерны для одной ОС и направлены против другой ОС, либо сетевого сервиса, либо даже отдельного приложения. Вряд ли имеет смысл детектировать на Linux-узле атаку в сторону Active Directory, поэтому злоумышленник без проблем её запустит с этого узла и не будет зафиксирован, а вот NTA увидит такую атаку и укзажет откуда она произошла, что даст пищу для дальнейших действий.
Также существуют нюансы, связанные с возвращаемым значением фунции, потому что те же функции ядра не особо документируют их значения, чуть лучше ситуация с LSM- хуками, так как они предназначены для разрешения или ограничения доступа к ресурам, поэтому лучше документированы. Лучше всего ситуация с возвращаемым значениям у системных вызовов, так как они являются официальным интерфейсом для программирования в ОС, поэтому хорошо документированы, а также играют роль этакой точки входа, поэтому в теории любой вызов в ОС должен пройти через них.
И тут мы двигаемся к третьему вопросу, так как именно перехват системного вызова может предоставить все попытки его использования, чем ниже мы двигаемся в своеобразной лестнице вызовов внутрь ядра, тем больше теряем информации, поэтому для того же LSM-хука не все вызовы могут дойти, так как будут отброшены либо промежуточной функцией ядра, либо другим LSM-хуком, который вы не собирались использовать, так как он вам не нужен, либо не подходит для мониторинга. С одной стороны это не особо критично, но в некоторых случаях может не соответствовать вашим целям.
И вот тут мы можем вспомнить о производительности. В идеале существует желание производить аудит любого действия в системе, но это технически невозможно, так как счёт их идёт на десятки-сотни тысячи в секунду и миллионы в минуту. Следует просто принять тот факт, что всю систему мониторить невозможно, поэтому нужно определить особо критичные зоны и работать по ним. Некоторые из них также могут выпасть из аудита по причинам производительности. Одна из таких зон – сетевое взаимодействие, ещё при работе с auditd в нашей практике приходилось отключать мониторинг сети полностью, что может показаться не очень приемлемым решением, но тут есть момент, что альтернативные решения по отказу от мониторинга файловой активности ещё хуже, так как теряется ещё больше информации о происходящем в системе.
Плюс существуют отдельные решения для анализа сетевой активности, которые выполняют эту работу лучше и качественее, чем пытаться делать подобное средствами auditd или даже "всемогущего" Tetragon (или другого eBPF-решения). NTA позволяют не только анализировать трафик с нескольких узлов, что строит более полную картину, но и разбирать сетевой трафик до самых высокоуровневых слоёв сетевой модели. Также они могут ловить атаки, которые не характерны для одной ОС и направлены против другой ОС, либо сетевого сервиса, либо даже отдельного приложения. Вряд ли имеет смысл детектировать на Linux-узле атаку в сторону Active Directory, поэтому злоумышленник без проблем её запустит с этого узла и не будет зафиксирован, а вот NTA увидит такую атаку и укзажет откуда она произошла, что даст пищу для дальнейших действий.
Недоступные для перехвата функции
Стоит отдельно отметить момент, что не все функции доступны к перехвату даже через самый универсальный механизм – krpobes. Такие функции отдельно отмечены меткой notrace и попытка их перехвата будет встречена подобной ошибкой:
trace_kprobe: Could not probe notrace function register_ftrace_functionЕсли при загрузке политики в видите в журнале ядра такую формулировку, то это как раз такая функция. Но есть более простой способ проверить возможность перехвата функции. Достаточно выполнить команду:
grep <имя функции> /sys/kernel/debug/tracing/available_filter_functionsДоступные к перехвату функции будут в этом списке и команда вернёт их название, если же она вернула пустое значение, то такой функции в списке нет, поэтому она не может быть перехвачена:
root@k8s-sn:~# grep security_file_permission
/sys/kernel/debug/tracing/available_filter_functions security_file_permissionВ примере выше функция security_file_permission доступна для перехвата. Дополнительно можно проверить её в ещё одном списке:
grep <имя функции> /proc/kallsymsИ она там тоже есть:
root@k8s-sn:~# grep security_file_permission /proc/kallsyms ffffffffa7749c50 T security_file_permission ffffffffa89624f8 r ksymtab_security_file_permission ffffffffa8979b79 r kstrtabns_security_file_permission ffffffffa8981d0d r kstrtab_security_file_permission
ffffffffa8eb9cfc r BTF_ID func security_file_permission 611882Список доступных для перехвата функций зависит от некоторых опций ядра, которые сложно перечислить в полном объёме, а также в определенных версиях ядра некоторые функции могут быть также запрещены к перехвату уже на уровне кода этого ядра. Поэтому в рамках нашей работы производится проверка выбранной функции на всех LTS- ядрах, которые поддерживаются на тот момент.
Виды политик
У Tetragon существует два вида политик:
Второй вид политик выделяется учётом сущностей Kubernetes и должен быть привязан к конкретному неймспейсу Kubernetes. Но для обоих видов политик также возможна фильтрация по метке пода или имени контейнера, Tetragon возьмёт на себя сопоставление нужных идентификаторов. Минусом второго вида политик является необходимость загружать её для каждого из неймспейсов, что потребует отдельно загружать её несколько раз. Ниже будут рассмотрены альтернативные варианты на базе обычной политики.
Обычные и namespaced-политики могут иметь одно название, фактически неймспейс будет элементом имени, поэтому возможно использование одного и того же имени у политик. Но это может вызвать путаницу при работе с ними и анализе событий, генерируемых этими политиками. Поэтому имеет смысл давать политикам уникальные имена без учёта неймспейса, и даже в случае namespaced-политик давать им отдельные имена.
- обычная;
- с привязкой к неймспейсу Kubernetes;
Второй вид политик выделяется учётом сущностей Kubernetes и должен быть привязан к конкретному неймспейсу Kubernetes. Но для обоих видов политик также возможна фильтрация по метке пода или имени контейнера, Tetragon возьмёт на себя сопоставление нужных идентификаторов. Минусом второго вида политик является необходимость загружать её для каждого из неймспейсов, что потребует отдельно загружать её несколько раз. Ниже будут рассмотрены альтернативные варианты на базе обычной политики.
Обычные и namespaced-политики могут иметь одно название, фактически неймспейс будет элементом имени, поэтому возможно использование одного и того же имени у политик. Но это может вызвать путаницу при работе с ними и анализе событий, генерируемых этими политиками. Поэтому имеет смысл давать политикам уникальные имена без учёта неймспейса, и даже в случае namespaced-политик давать им отдельные имена.
Основные блоки политики
Для удобства разделим политику на несколько крупных блоков, в каждом из них может быть один и больше подблоков, это не разделение самого Tetragon, а просто логическое представление, чтобы в рамках статьи было легче работать с этим. Они будут следующими:
Минимальная политика будет состоять из сервисного блока и блока привязки, последний из которых является ключевым и задаёт все параметры для мониторинга.
- сервисные блоки;
- опциональные блоки;
- блок привязки.
Минимальная политика будет состоять из сервисного блока и блока привязки, последний из которых является ключевым и задаёт все параметры для мониторинга.
Сервисные блоки
Заголовок
В зависимости от вида политики у нас будут разные заголовки. Обычная выглядит следующим образом:
apiVersion: cilium.io/v1alpha1 kind: TracingPolicyNamespaces-политика имеет следующий заголовок:
apiVersion: cilium.io/v1alpha1 kind: TracingPolicyNamespacedМетаинформация
В этом блоке указываются некоторые сервисные поля, которые использует Tetragon для идентификации или привязки к неймспейсу (для namespaced-политик).
Поля:
Лучше это будет показать на примере namespaced-политики:
Поля:
- name – название политики;
- namespace – неймспейс Kubernetes, к которому будет привязана политика (только для namespaced-политик).
Лучше это будет показать на примере namespaced-политики:
metadata:
name: "test-policy" namespace: "default"Соответственно у обычной политики тут будет заполнено только название. В примере выше namespaced-политика с названием test-policy будет привязана в неймспейсу default.
Опциональные блоки
Опции
Дальше после директивы spec начинается сама спецификация политики, которая содержит как обязательные блоки, без которых политика просто не загрузится, так и множество опциональных. Раздел опций – один из таких блоков. Скорее всего он практически никогда не пригодится, но стоит помнить, что он есть. Каждое значение в этом блоке задаётся через пару ключ-значение, соответствующие полям name и value, то есть примерный вид подобного блока может быть таким:
spec:
options:
- name: "disable-kprobe-multi" value: "1"Где мы для опции disable-kprobe-multi устанавливаем значение 1.
Селектор пода
Ещё один опциональный блок, с помощью которого можно распространить действие политики только на конкретные поды. Производится это на основании блока меток labels у пода, это стоит учитывать, так как не всегда он может быть заполнен. Изначально был доступен только один способ сопоставления меток – matchLabels, но позже был добавлен более универсальный matchExpressions. Разберем каждый из них.
Директива matchLabels производит полное сравение значения метки, что соответствует оператору In у директивы matchExpressions, сама метка указывается как ключ, то есть этот блок состоит из пар ключ-значение. Таких пар может быть несколько, но при выборке они объединяются логическим И, то есть у искомого пода должны быть заполнены и соответствовать все метки. Пример:
spec:
podSelector: matchLabels:
app: "test-pod-debian"В данном случае политика будет применена ко всем подам, у которых метка app имеет значение test-pod-debian.
Директива matchExpressions делает почти то же самое, но позволяет использовать другие операторы сравнения. В качестве примера проще будет переписать предыдущий блок, чтобы объяснить различие, но в то же время добавим ещё одно значение, чтобы показать различие с предыдущим вариантом:
Директива matchExpressions делает почти то же самое, но позволяет использовать другие операторы сравнения. В качестве примера проще будет переписать предыдущий блок, чтобы объяснить различие, но в то же время добавим ещё одно значение, чтобы показать различие с предыдущим вариантом:
spec:
podSelector: matchExpressions:
key: "app" operator: "In" value:
"test-pod-debian"
"test-pod-ubuntu"Теперь значений может быть несколько, тут действует логическое ИЛИ, то есть значение метки app должно соответствовать одному из указанных. Но это ещё не всё, теперь помимо проверки соответствия значения мы можем через оператор NotIn:
spec:
podSelector: matchExpressions:
key: "app" operator: "NotIn" value:
"test-pod-debian"
"test-pod-ubuntu"Теперь уже искомые поды не должны иметь в метке перечисленные значения. Также теперь мы можем проверять на наличие определенной метки, массив значений в таком случае должен быть пустым:
spec:
podSelector: matchExpressions:
key: "app" operator: "Exist"В этом случае будут выбраны поды, у которых есть метка app. Как и в случае с предыдущей парой операторов, тут также можно сделать отрицание условия:
spec:
podSelector: matchExpressions:
key: "app"
operator: "DoesNotExist"Теперь будут выбраны поды, у которых нет метки app.
За счёт этого механизма можно выбирать конкретные поды для мониторинга в политике, что уменьшит число событий.
За счёт этого механизма можно выбирать конкретные поды для мониторинга в политике, что уменьшит число событий.
Селектор контейнера
Когда требуется ещё более гранулярная выборка. Работает по аналогии с podSelector, только изначально для контейнеров использовалась директива matchExpressions, для выборки поддерживается пока только название контейнера:
Когда требуется ещё более гранулярная выборка. Работает по аналогии с podSelector, только изначально для контейнеров использовалась директива matchExpressions, для выборки поддерживается пока только название контейнера:
spec:
containerSelector: matchExpressions:
key: "name" operator: "In" value:
"db"
"nginx"То есть ищем контейнеры с названием db или nginx, можно сделать и наоброт:
spec:
containerSelector: matchExpressions:
key: "name" operator: "NotIn" value:
"db"
"nginx"
Теперь выбираем контейнеры, которые не носят название db или nginx.
Блоки lists и enforcers
Существуют для специфического случая, когда нет поддержки множественных kprobes, тогда в lists перечисляются нужные системные вызовы, в enforcers указывается этот
список, а также указывается метод NotifyEnforcer в фильтре селектора matchActions. Пример из документации:
Существуют для специфического случая, когда нет поддержки множественных kprobes, тогда в lists перечисляются нужные системные вызовы, в enforcers указывается этот
список, а также указывается метод NotifyEnforcer в фильтре селектора matchActions. Пример из документации:
spec:
lists:
name: "dups" type: "syscalls" values:
"sys_dup"
"sys_dup2" enforcers:
calls:
"list:dups" tracepoints:
subsystem: "raw_syscalls" event: "sys_enter"
args:
index: 4
type: "syscall64" selectors:
matchArgs:
index: 0 operator: "InMap" values:
"list:dups" matchBinaries:
operator: "In" values:
"/usr/bin/bash" matchActions:
action: "NotifyEnforcer" argSig: 9Так как всё это реализуется через трейспоинты ядра, то нельзя назвать это надёжным вариантом.