Перейти к содержимому
HoginHogin
Назад

Wazuh и Kubernetes: собираем аудит-логи кластера и видим, кто и что делает (часть 5/6)

8 мин чтения

В четвёртой части Wazuh научился не только видеть атаку, но и сам закрывать атакующему IP. Но всё это время взгляд SIEM ограничивался отдельными хостами: агент, логи, файловая система. Пятая часть меняет масштаб — подключаем Kubernetes. У кластера есть свойство, за которое его любят и атакующие, и те, кто защищается: любое действие в кластере — это запрос к API-серверу. kubectl exec, чтение секрета, создание privileged-пода, смена RBAC — всё это HTTP-запросы, которые API-сервер умеет записывать в audit-лог. Умеет — но по умолчанию либо не пишет вообще, либо пишет в файл на самом API-сервере, где логи лежат мёртвым грузом, и первым их читает как раз тот, кто скомпрометировал кластер.

Содержание

Открыть содержание

Что логирует API-сервер

Kubernetes audit logging — встроенный механизм kube-apiserver: флаг --audit-policy-file включает политику, которая решает, какие запросы писать и с какой детализацией. Каждое событие — одна JSON-строка формата audit.k8s.io/v1:

{"kind":"Event","apiVersion":"audit.k8s.io/v1","level":"Request",
 "auditID":"8f6b...","stage":"ResponseComplete","verb":"create",
 "user":{"username":"dev@company","groups":["system:authenticated"]},
 "objectRef":{"resource":"pods","subresource":"exec","namespace":"prod","name":"api-7d9f"},
 "responseStatus":{"code":101}}

Из этого куска уже видно, почему аудит — золотая жила для детектов: в одном событии есть кто (user.username), что (verb + objectRef), где (namespace, имя объекта) и чем закончилось (responseStatus.code — 101 у exec это переключение протокола, то есть сессия открылась). Рядом в полной строке лежат sourceIPs — цепочка адресов от клиента до прокси — и userAgent, по которому видно, зашёл человек через kubectl или что-то стучится скриптом. Именно эти техники — exec в под, чтение secrets, создание privileged-подов — прямо описаны в матрице MITRE ATT&CK for Containers, и именно их возьмём под детекты.

Проблема не в том, что данных нет, а в том, что они умирают на диске API-сервера: на трёх-пяти мастер-нодах по файлу, без центрального хранения, без правил, без алертов. Задача этой части — построить конвейер: API-сервер → Fluent Bit → Wazuh → алерт.

Путь аудит-события в Wazuh

Уровни аудита: Metadata, Request, RequestResponse

Главное решение при написании audit policy — уровень детализации на каждый тип ресурсов. Их три, и отличаются они тем, сколько тела запроса и ответа попадает в лог:

Сведём выбор в таблицу — она же ответ на вопрос «какой уровень на что вешать»:

УровеньТела запроса и ответаОбъём относительно MetadataКогда выбирать
Metadataнет1×всё по умолчанию; secrets и configmaps — всегда
RequestrequestObject~2–3×exec (нужна команда), создание подов (нужна спецификация)
RequestResponse+ responseObject~5–10×, зависит от ресурсовточечно: RBAC, отдельные ресурсы под конкретные детекты

Уровни аудита и их цена

Два решения, которые определяют объём и безопасность самого лога:

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

apiVersion: audit.k8s.io/v1
kind: Policy
omitStages:
  - "RequestReceived"
rules:
  # 1. секреты — только Metadata: тела секретов не должны
  #    утекать в логи и SIEM
  - level: Metadata
    resources:
      - group: ""
        resources: ["secrets", "configmaps"]

  # 2. exec/attach/portforward — Request: команда в requestObject,
  #    в ответе бинарный поток, которого всё равно нет
  - level: Request
    resources:
      - group: ""
        resources: ["pods/exec", "pods/attach", "pods/portforward"]

  # 3. поды — RequestResponse: спецификация нужна детекту
  #    privileged-подов, ответ подтверждает, что под создан
  - level: RequestResponse
    resources:
      - group: ""
        resources: ["pods"]

  # 4. RBAC — всегда максимум: кто раздал себе права,
  #    интереснее всего с телами запросов
  - level: RequestResponse
    resources:
      - group: "rbac.authorization.k8s.io"
        resources: ["roles", "rolebindings", "clusterroles", "clusterrolebindings"]

  # 5. всё остальное — Metadata
  - level: Metadata

В self-hosted кластере (kubeadm) политика включается двумя флагами в /etc/kubernetes/manifests/kube-apiserver.yaml, файл политики и каталог логов пробрасываются в под через hostPath:

- --audit-policy-file=/etc/kubernetes/audit-policy.yaml
- --audit-log-path=/var/log/kubernetes/audit.log
- --audit-log-maxage=7

В managed-кластерах аудит пишется в облачный лог (CloudWatch в EKS, Cloud Logging в GKE) — конвейер тот же, меняется только вход Fluent Bit.

Декодер: учим Wazuh читать audit-события

Дальше — знакомая по третьей части механика: prematch выбирает наши строки, JSON-движок разворачивает вложенности в пути через точку. В /var/ossec/etc/decoders/local_decoder.xml:

<decoder name="k8s-audit">
  <prematch>^.+"kind":"Event".+"apiVersion":"audit.k8s.io</prematch>
  <plugin_decoder>JSON_Decoder</plugin_decoder>
</decoder>

После декодера поля события доступны правилам как динамические: verb, user.username, objectRef.resource, objectRef.namespace, requestObject.*. Вложенные массивы JSON-движок разворачивает с числовыми индексами — requestObject.spec.containers.0.securityContext.privileged — это ещё пригодится для privileged-подов.

Правила: exec, secrets, privileged-поды

Три детекта из заготовки, в /var/ossec/etc/rules/local_rules.xml. Базовое правило распознаёт все аудит-события, дети навешивают условия — ровно та же иерархия if_sid, что мы строили для SSH и биллинга:

<group name="kubernetes,audit,">

  <rule id="100210" level="3">
    <decoded_as>k8s-audit</decoded_as>
    <description>kubernetes: audit event $(objectRef.resource) $(verb)</description>
  </rule>

  <rule id="100211" level="12">
    <if_sid>100210</if_sid>
    <field name="verb">^create$</field>
    <field name="objectRef.resource">^pods/exec$</field>
    <field name="user.username">!^system:serviceaccount:kube-system:</field>
    <description>kubernetes: exec into pod by $(user.username) — outside allowlist</description>
    <mitre><id>T1609</id></mitre>
  </rule>

  <rule id="100212" level="9">
    <if_sid>100210</if_sid>
    <field name="verb">^get|list|watch$</field>
    <field name="objectRef.resource">^secrets$</field>
    <field name="user.username">!^system:serviceaccount:</field>
    <description>kubernetes: secrets read via API by human user $(user.username)</description>
    <mitre><id>T1552.007</id></mitre>
  </rule>

  <rule id="100213" level="12">
    <if_sid>100210</if_sid>
    <field name="verb">^create$</field>
    <field name="objectRef.resource">^pods$</field>
    <field name="requestObject.spec.containers.0.securityContext.privileged">^true$</field>
    <description>kubernetes: privileged pod created by $(user.username)</description>
    <mitre><id>T1610</id></mitre>
  </rule>

</group>

Что здесь важно:

Доставка: Fluent Bit до менеджера

Осталось довести лог до Wazuh. Канонический путь из доков — Fluent Bit DaemonSet: под на каждой ноде читает audit-лог с hostPath и отправляет его менеджеру по syslog over TCP. ConfigMap:

[SERVICE]
    Flush             5
    Daemon            Off
    Log_Level         warning

[INPUT]
    Name              tail
    Path              /var/log/kubernetes/audit.log
    Refresh_Interval  10
    Skip_Long_Lines   On

[OUTPUT]
    Name              syslog
    Host              WAZUH_MANAGER_IP
    Port              514
    Mode              tcp
    Syslog_Hostname   k8s-audit
    Syslog_Appname    kube-apiserver
    Syslog_Tag        audit

DaemonSet монтирует /var/log/kubernetes с нод мастеров (подробный манифест есть в доках Wazuh — там обычные tolerations для control-plane). На менеджере в ossec.conf открываем syslog-приёмник только для pod-сети кластера:

<remote>
  <connection>syslog</connection>
  <port>514</port>
  <protocol>tcp</protocol>
  <allowed-ips>10.42.0.0/16</allowed-ips>
</remote>

allowed-ips здесь — не косметика: открытый syslog-порт на 514 — это приглашение всему интернета кормить ваш SIEM мусором, поэтому приёмник смотрит только в сеть подов.

Как проверить, что всё работает

В отличие от Active Response из четвёртой части, декодеры и правила спокойно проверяются без живого кластера — /var/ossec/bin/wazuh-logtest прогоняет строку через все фазы. Возьмите реальную строку из /var/log/kubernetes/audit.log на мастере, вставьте в logtest и убедитесь, что Phase 2 нашла k8s-audit, а Phase 3 — правило 100211.

Живая проверка end-to-end — exec от человеческой учётки в любой под прод-неймспейса:

kubectl -n prod exec -it deploy/api -- sh

На менеджере:

tail -f /var/ossec/logs/alerts/alerts.json | grep -E '100211|100212|100213'

Алерт с T1609, вашим user.username и именем пода должен появиться в течение секунд (Flush 5). Если logtest срабатывает, а живой алерт нет — разрыв в транспорте: смотрите логи Fluent Bit (kubectl logs -n logging -l app=fluent-bit) и /var/ossec/logs/ossec.log на ошибки syslog-приёмника.

Итог

Аудит-лог Kubernetes — редкий случай, когда лучший источник сигналов о компрометации кластера уже пишется бесплатно, его нужно только довести до SIEM: политика на сто строк, ConfigMap Fluent Bit, декодер на три строки и три правила — и в Wazuh видно всё, что происходит на control-plane API: кто залогинился, кто сделал exec, кто выдал себе RBAC. Но у этой картины есть граница: API-сервер видит запросы, а не процессы. Что бинарник запустил внутри контейнера, какие файлы он открыл, куда установил соединение — на уровне API этого нет, и аудит это не покажет никогда. Этот пробел закрывает runtime-детект на syscalls — Falco, герой следующей, заключительной части цикла.

Что видит Wazuh и где граница


Поделиться:

Следующая статья
Wazuh Active Response: автоматически банить, изолировать и реагировать на инциденты (часть 4/6)