«Слепые зоны находятся, когда действительно всматриваешься»
«Слепые зоны находятся, когда действительно всматриваешься»

Всем привет! Решил поделиться с вами опытом написания и проведения ревью аудит‑политики Kubernetes.

Аудит‑политика Kubernetes — один из тех пунктов, который пишется один раз при настройке кластера, а потом годами живёт «как есть», обрастая исключениями для новых компонентов и почти никогда не пересматривается целиком. В какой‑то момент это уже не политика безопасности, а слой из комментариев трёхлетней давности.

Недавно я сел пересмотреть собственный конфиг на 500+ строк — собирал его из нескольких источников, в первую очередь опираясь на Kubernetes Threat Matrix. Хочу поделиться не столько конкретными находками (так как они специфичны для конкретного кластера), сколько принципами и типовыми ловушками, которые всплыли в процессе ревью. Если вы пишете или проводите ревью политики аудита для своего кластера — этот чек‑лист вполне сэкономит время.

Как вообще устроена Audit Policy

Если коротко: это список правил, каждое из которых описывает, каким пользователям/группам/verb'ам/ресурсам соответствует, и какой уровень логирования (level) применить — от None (не логировать вообще) до RequestResponse (логировать запрос и ответ целиком, включая тело).

Ключевая деталь, которую легко упустить: правила проверяются сверху вниз, и применяется первое совпадение. Если у вас широкое правило‑исключение стоит выше узкого критичного с точки зрения безопасности правила, второе никогда не сработает для тех же запросов. Большая часть проблем, которые реально стоит искать при ревью, — это именно ошибки порядка, а не ошибки в отдельном правиле.

Принцип 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 это правило гасит абсолютно любое действие данного пользователя или сервис аккаунта — не только штатный трафик операций чтения, ради которого его писали, но и любые мутации, которые этому сервис аккаунту когда‑либо дадут (случайно через RBAC‑ошибку или намеренно при расширении функционала). Если токен такого сервис аккаунта скомпрометируют, атака пройдёт полностью мимо аудита — не потому что кто‑то планировал дыру, а потому что правило-исключение изначально писалось «на глаз», под текущее поведение компонента, без явной фиксации границ.

Практическое правило: при добавлении нового правила-исключения всегда явно перечисляйте verbs, даже если сейчас компонент делает только get/list/watch. Это фиксация границ, определение, что мы видим и что нам особенно важно.

Ловушка № 2: неактуальные комментарии

# 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», а правило реально исключает логирование для перечисленных в нем сервис аккаунтов.

Это не влияет на работу политики, но такие несостыковки — прямой путь к тому, что через год кто‑то прочитает комментарий, поверит ему на слово и потратит день на дебаг «почему события не логируются, хотя написано, что должны». Ревью комментариев — такая же часть код‑ревью, как и ревью логики.

Ловушка № 3: неактуальные правила от прошлых миграций

CNI мигрировали с kube‑router на Cilium полгода назад, а правило‑исключение для system:kube-router так и осталось в файле. Само по себе оно безобидно — просто никогда не сработает, потому что такого сервис аккаунта уже больше нет. Но:

  • оно засоряет файл и сбивает с толку при ревью;

  • если однажды кто‑то снова заведёт сервис аккаунт с таким же именем под другой компонент — унаследует чужие, неактуальные права.

Простой чек: периодически (раз в квартал, например) сверять список пользователей и сервис аккаунтов в правилах исключения политики аудита со списком реально существующих сервис аккаунтов в кластере — 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"]

Логика простая: сам факт обращения к секрету нужно фиксировать (кто, когда, какой секрет), а вот содержимое секрета в аудит-лог попадать не должно ни при каких условиях — иначе аудит-лог сам становится хранилищем секретов, только менее защищённым.

Принцип 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: используй порядок правил осознанно, а не случайно

Хороший пример осмысленного использования правильного порядка — обработка 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'а — специфичное правило всегда должно стоять выше общего.

Мини‑чек‑лист для ревью своей политики аудита

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

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

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

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

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

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

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

  8. Нет ли явно временных правил без даты/тикета на их удаление?

Вывод

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

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

P. S.

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