
Аудит-политика Kubernetes — один из тех артефактов, который пишется один раз при настройке кластера, а потом годами живёт «как есть», обрастая исключениями для новых компонентов и почти никогда не пересматривается целиком. В какой-то момент это уже не политика безопасности, а археологический слой из комментариев вида «# temporary, TODO remove» трёхлетней давности.
Недавно я сел перечитать собственный конфиг на ~580 строк — собирал его из нескольких источников, в первую очередь опираясь на Kubernetes Threat Matrix (это объясняет, почему часть правил — не просто «гасим шум», а прицельно ловит security-значимые действия вроде изменений RBAC или удаления events). Хочу поделиться не столько конкретными находками (они специфичны для конкретного кластера), сколько принципами и типовыми ловушками, которые всплыли в процессе ревью. Если вы пишете или ревьюите Audit Policy для своего кластера — этот чек-лист сэкономит время.
Как вообще устроена Audit Policy
Если коротко: это список правил (rules), каждое из которых описывает, каким пользователям/группам/verb'ам/ресурсам соответствует, и какой уровень логирования (level) применить — от None (не логировать вообще) до RequestResponse (логировать запрос и ответ целиком, включая тело).
Ключевая деталь, которую легко упустить: правила проверяются сверху вниз, и применяется первое совпадение. Это не набор независимых фильтров, которые как-то комбинируются — это if / else if / else if. Если у вас широкое правило-исключение стоит выше узкого security-critical правила, второе никогда не сработает для тех же запросов. Большая часть проблем, которые реально стоит искать при ревью, — это именно ошибки порядка, а не ошибки в отдельном правиле.
Принцип 1: разделяй шум и сигнал
В любом мало-мальски живом кластере 90% API-трафика — это get/list/watch от системных компонентов: kubelet опрашивает статус нод, Prometheus скрейпит метрики, контроллеры следят за своими ресурсами. Если логировать всё это на приличном уровне, лог утонет в шуме, и искать в нём реальный инцидент станет невозможно.
Правильный паттерн — точечно гасить эти потоки:
- level: None users: ["system:serviceaccount:kube-system:coredns"] verbs: ["get", "list", "watch"]
Важно: гасить нужно максимально узко — по конкретному пользователю, конкретным verb'ам и, желательно, конкретным ресурсам. Вот тут кроется первая типовая ошибка.
Ловушка №1: исключение без ограничения verb/resource
Встречается регулярно:
- level: None users: ["system:serviceaccount:some-ns:some-controller"]
Без verbs и resources это правило гасит абсолютно любое действие данного пользователя или SA (ServiceAccount — сервисной учётной записи, от имени которой поды обращаются к API-серверу) — не только штатный read-трафик, ради которого его писали, но и любые мутации, которые этому SA когда-либо дадут (случайно через RBAC-ошибку или намеренно при расширении функционала). Если токен такого SA скомпрометируют, атака пройдёт полностью мимо аудита — не потому что кто-то планировал дыру, а потому что exclusion-правило изначально писалось «на глазок», под текущее поведение компонента, без явной фиксации границ.
Практическое правило: при добавлении нового exclusion-правила всегда явно перечисляйте verbs, даже если сейчас компонент делает только get/list/watch. Это фиксация границ: «этому компоненту разрешено молчать только на чтение, всё остальное обязано попасть в лог» — а не просто привычка, от которой можно отмахнуться.
Ловушка №2: comments врут
# Any actions with secrets and configs (except list/watch) - critical to monitor # for potential data leaks or unauthorized configuration changes - level: None verbs: ["get", "create", "update", "patch", "delete"] resources: - group: "" resources: ["secrets", "configmaps"] users: [...]
Комментарий говорит «critical to monitor», а правило реально исключает логирование для перечисленных SA. Похоже на копипасту из другого места файла, которую забыли поправить под новый смысл правила.
Это не влияет на работу политики, но такие несостыковки — прямой путь к тому, что через год кто-то прочитает комментарий, поверит ему на слово и потратит день на дебаг «почему события не логируются, хотя написано, что должны». Ревью комментариев — такая же часть код-ревью, как и ревью логики.
Ловушка №3: мёртвые правила от прошлых миграций
CNI мигрировали с kube-router на Cilium полгода назад, а правило-исключение для system:kube-router так и осталось в файле. Само по себе оно безобидно — просто никогда не сработает, потому что такого SA больше нет. Но:
оно засоряет файл и сбивает с толку при ревью («а, у нас ещё и kube-router где-то есть?»);
если однажды кто-то снова заведёт SA с таким же именем под другой компонент — унаследует чужие, неактуальные права на молчание.
Простой чек: периодически (раз в квартал, например) сверять список пользователей и SA в exclusion-правилах audit policy со списком реально существующих ServiceAccount в кластере — kubectl get sa -A — и вычищать несовпадения.
Принцип 2: чувствительные данные — максимум Metadata
Отдельный красивый паттерн из разобранного конфига:
# Secrets, ConfigMaps, and TokenReviews can contain sensitive & binary data, # so only log at the Metadata level. - level: Metadata verbs: ["get", "create", "update", "patch", "delete"] resources: - group: "" resources: ["secrets", "configmaps"] - group: "authentication.k8s.io" resources: ["tokenreviews"]
Логика простая: сам факт обращения к секрету нужно фиксировать (кто, когда, какой секрет), а вот содержимое секрета в аудит-лог попадать не должно ни при каких условиях — иначе аудит-лог сам становится хранилищем секретов, только менее защищённым. Поэтому для таких ресурсов Request/RequestResponse — это всегда осознанное решение, а не default.
Принцип 3: не экономь на RequestResponse там, где это оправдано
На другом полюсе — события, где важно видеть не просто факт, а содержимое запроса целиком:
- level: RequestResponse resources: - group: "" resources: ["pods/exec", "pods/attach", "pods/portforward"] - level: RequestResponse verbs: ["create", "update", "patch", "delete"] resources: - group: "rbac.authorization.k8s.io" resources: ["clusterrolebindings", "clusterroles", "rolebindings", "roles"]
exec/attach/portforward — это прямой доступ внутрь работающего контейнера, классический вектор при разборе инцидентов. Изменения RBAC — это потенциальная эскалация привилегий. Для обоих случаев экономить на уровне логирования не стоит: разница в объёме лога небольшая, а ценность при расследовании — огромная.
Принцип 4: используй порядок правил осознанно, а не случайно
Хороший пример осмысленного использования first-match-wins — обработка events:
# Defense evasion tactic - detect attempts to delete k8s events - level: Metadata verbs: ["delete"] resources: - group: "" resources: ["events"] - group: "events.k8s.io" resources: ["events"] # Don't log events requests. - level: None resources: - group: "" resources: ["events"] - group: "events.k8s.io" resources: ["events"]
Сами события — жутко шумный ресурс, логировать все обращения к ним бессмысленно. Но удаление events — классическая техника defense evasion (атакующий чистит следы своей активности). Поставив узкое правило для delete выше общего «глушим всё», получаем ровно нужное поведение: обычный трафик событий не засоряет лог, а попытка их удалить — фиксируется.
Это стоит держать в голове как общий паттерн: если для одного и того же ресурса нужна разная чувствительность в зависимости от verb'а — специфичное правило всегда должно стоять выше общего.
Мини-чек-лист для ревью своей Audit Policy
Есть ли правило для
system:unauthenticated, и стоит ли оно выше всех exclusion-правил?Есть ли хоть одно exclusion-правило (
level: None) без явныхverbsи/илиresources? Если да — можно ли сузить?Секреты/конфигмапы/токены нигде не поднимаются выше
Metadata?RBAC-изменения,
exec/attach/portforward, удаление сетевых политик — на достаточном уровне (Request/RequestResponse)?Нет ли в exclusion-правилах пользователей/SA, которых уже не существует в кластере (наследие миграций)?
Комментарии соответствуют тому, что реально делает правило?
Есть ли catch-all правило в самом конце файла (обычно
level: Metadata), чтобы ничего не проваливалось мимо лога по умолчанию?Нет ли явно временных правил («temporary», «TODO») без даты/тикета на их удаление?
Вывод
Audit Policy — это не место для «настроил и забыл». Она растёт вместе с кластером, и каждое новое exclusion-правило, добавленное на скорую руку, чтобы заглушить очередной шумный компонент, — это потенциальная слепая зона, если её не ограничить явно. Разница между «удобно» и «безопасно» здесь чаще всего решается одной строчкой — явным списком verbs.
Если у вас есть свои находки или паттерны при работе с Audit Policy — делитесь в комментариях, интересно сравнить подходы.
P.S.
В комментариях могу выложить готовый шаблон audit-policy.yaml - образец, по которому можно собрать свою политику.