Аудит-политика 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

  1. Есть ли правило для system:unauthenticated, и стоит ли оно выше всех exclusion-правил?

  2. Есть ли хоть одно exclusion-правило (level: None) без явных verbs и/или resources? Если да — можно ли сузить?

  3. Секреты/конфигмапы/токены нигде не поднимаются выше Metadata?

  4. RBAC-изменения, exec/attach/portforward, удаление сетевых политик — на достаточном уровне (Request/RequestResponse)?

  5. Нет ли в exclusion-правилах пользователей/SA, которых уже не существует в кластере (наследие миграций)?

  6. Комментарии соответствуют тому, что реально делает правило?

  7. Есть ли catch-all правило в самом конце файла (обычно level: Metadata), чтобы ничего не проваливалось мимо лога по умолчанию?

  8. Нет ли явно временных правил («temporary», «TODO») без даты/тикета на их удаление?

Вывод

Audit Policy — это не место для «настроил и забыл». Она растёт вместе с кластером, и каждое новое exclusion-правило, добавленное на скорую руку, чтобы заглушить очередной шумный компонент, — это потенциальная слепая зона, если её не ограничить явно. Разница между «удобно» и «безопасно» здесь чаще всего решается одной строчкой — явным списком verbs.

Если у вас есть свои находки или паттерны при работе с Audit Policy — делитесь в комментариях, интересно сравнить подходы.

P.S.

В комментариях могу выложить готовый шаблон audit-policy.yaml - образец, по которому можно собрать свою политику.

Комментарии (0)