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

Falco в Kubernetes: runtime-защита на уровне syscalls и единый SIEM с Wazuh (часть 6/6)

9 мин чтения

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

Содержание

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

Что такое Falco и где он стоит

Falco — runtime-детектор от CNCF: первый проект класса runtime security, дошедший до статуса graduated (февраль 2024), с живыми релизами — v0.44.x на момент написания — и продакшн-историями уровня Shopify и GitLab. Разворачивается он как DaemonSet: по одному поду на каждую ноду, и этого достаточно, чтобы видеть все контейнеры ноды — свои, чужие и запущенные мимо вашего CI.

Ключ к пониманию Falco — точка наблюдения. Контейнер, как бы он ни изолировался, живёт через ядро: execve запускает процесс, open открывает файл, connect устанавливает соединение — и всё это syscalls. Драйвер Falco, встроенный в ядро ноды, получает этот поток целиком, движок прогоняет каждое событие через правила, а параллельный обогатитель ходит в API-сервер и добавляет контекст: рядом с proc.name=sh в алерте оказываются container.image.repository и k8s.pod.name. Не «какой-то процесс что-то сделал», а «вот этот образ, в этом поде, в этом неймспейсе».

Путь события Falco: от syscall до SIEM

Драйвер: eBPF или kernel-модуль

Чтобы перехватывать syscalls, Falco нужен кусок кода в ядре — драйвер. Варианта три, и выбор сводится к тому, какие ядра у вас и кто ими управляет:

ДрайверЧто требуетКогда выбирать
kernel-модульмодуль под версию ядра, собирается и грузится на каждой нодестарые ядра без eBPF; среды, где BPF запрещён политикой
eBPF-пробаядро с включённым BPF; проба скачивается как артефактпереходный вариант для ядер 4.x
modern_ebpfядро ≥ 5.8 с BTF; ничего не скачивает и не собираетmanaged-кластеры и свежие узлы — разумный дефолт

Совет практичный: если узлы на ядре 5.8+ — любой managed-кластер моложе пары лет, — ставьте modern_ebpf: ни загрузки артефактов, ни пересборки после обновления ядра. Kernel-модуль остаётся рабочим вариантом для старых железных нод, но требует дисциплины: обновили ядро — пересобрали модуль.

Правила: списки, макросы, условие

Из коробки Falco приходит с ruleset в несколько сотен правил: шелл в контейнере, запись в чувствительные пути, запуск сетевых утилит, поведение криптомайнеров, обращение к API-серверу из пода. Формат — YAML, и он собирается из повторно используемых блоков, как конструктор:

Правило Falco собирается из блоков

Свои правила кладут отдельным файлом — дефолтный ruleset не трогают, чтобы его можно было обновлять чартом. Два детекта из заготовки: шелл в контейнере, где его не должно быть, и mount хост-директории изнутри контейнера:

# app-runtime-rules.yaml
- macro: prod_app
  condition: container and k8s.ns.name in (prod)

- rule: Interactive Shell in Application Container
  desc: Shell spawned inside a prod app container where no shell should ever run
  condition: >
    spawned_process and shell_procs and prod_app
    and not k8s.pod.name startswith debug
  output: >
    shell in app container (user=%user.name proc=%proc.name
    parent=%proc.pname image=%container.image.repository
    pod=%k8s.pod.name ns=%k8s.ns.name)
  priority: WARNING
  tags: [container, shell, mitre_execution, T1059]

- rule: Container Mounting Host Filesystem
  desc: A mount syscall from inside a container — mounting is the runtime's job, not the workload's
  condition: >
    evt.type = mount and container
    and not proc.name in (runc, crun, dockerd, containerd-shim, kubelet)
  output: >
    mount inside container (user=%user.name proc=%proc.name
    args=%evt.args image=%container.image.repository
    pod=%k8s.pod.name ns=%k8s.ns.name)
  priority: CRITICAL
  tags: [container, filesystem, mitre_privilege_escalation, T1611]

Почему правила написаны именно так:

falcosidekick: шина для алертов

Сам Falco по части вывода аскетичен: файл, stdout, HTTP, gRPC. Расширяет его falcosidekick — лёгкий сервис, который принимает события Falco по HTTP и раздаёт их в несколько десятков выходов: Slack и Teams для людей, Loki и Elasticsearch для хранения, webhook для всего остального. В конвейере он занимает место шины: движок Falco занят фильтрацией и не должен знать, куда летят алерты и что делать при недоступности получателя. Для нашей задачи нужен один выход — syslog на Wazuh; webhook выбирайте, если алерты принимает Wazuh API или любой другой HTTP-коллектор.

Чем Falco дополняет аудит из части 5

Соберём оба механизма рядом — видно, что они не конкурируют, а стыкуются:

Аудит API (часть 5)Falco
Точка наблюденияHTTP-запросы к kube-apiserversyscalls всех контейнеров ноды
Отвечает на вопрос«кто и что попросил у кластера»«что процесс реально сделал»
Типовые детектыexec, чтение secrets, RBAC-дрейфшелл в поде, запись в /etc, криптомайнер
Слепая зонапроцессы после старта подаRBAC-контекст: кто создал под
Где живётфлаги apiserver + Fluent BitDaemonSet в кластере
Ценаобъём audit-логанемного CPU и RAM на ноду

Та же атака глазами связки: аудит фиксирует «dev@company сделал exec в api-7d9f», Falco — «в api-7d9f запустился sh, прочитал /etc/passwd и открыл соединение наружу». Первый говорит, кто вошёл, второй — что сделал вошедший; таймлайн склеивается в Wazuh по имени пода.

Что нужно, чтобы завести Falco и отправить алерты в Wazuh

Конфигурация чарта — один values-файл, который включает драйвер, JSON-вывод, наши правила и sidekick:

# values-falco.yaml
driver:
  kind: modern_ebpf

falco:
  json_output: true
  json_include_output_property: true
  time_format_iso_8601: true
  priority: warning        # порог отсечения: notice-шум не отправляем

customRules:
  app-runtime-rules.yaml: |-
    # оба правила из предыдущего раздела — целиком, как есть

falcosidekick:
  enabled: true
  config:
    syslog:
      host: "WAZUH_MANAGER_IP"
      port: "514"
      protocol: "tcp"

Установка:

helm repo add falcosecurity https://falcosecurity.github.io/charts
helm repo update
helm install falco falcosecurity/falco -n falco --create-namespace -f values-falco.yaml

Обвязку для falcosidekick.enabled чарт делает сам: Falco начинает отправлять JSON-события на сервис sidekick, а тот — на ваш syslog. На менеджер событие прилетает в знакомом по части 5 виде — JSON-строкой:

{"hostname":"node-1","output":"shell in app container (user=root proc=sh ...)","priority":"Warning","rule":"Interactive Shell in Application Container","time":"2026-10-12T07:12:44.123456789Z","output_fields":{"container.image.repository":"shop/api","k8s.ns.name":"prod","k8s.pod.name":"api-7d9f-xk2lp","proc.name":"sh","user.name":"root"}}

Дальше — знакомая пара декодер и правила, /var/ossec/etc/decoders/local_decoder.xml:

<decoder name="falco">
  <prematch>^.+"priority":"</prematch>
  <plugin_decoder>JSON_Decoder</plugin_decoder>
</decoder>

Prematch цепляется за поле, которого нет ни в аудит-событиях, ни в логах агентов; JSON-движок разворачивает событие, и правилам доступны rule, priority, output и hostname. Правила — в /var/ossec/etc/rules/local_rules.xml, ID продолжают нумерацию части 5:

<group name="falco,runtime,">

  <rule id="100220" level="3">
    <decoded_as>falco</decoded_as>
    <description>falco: $(rule) [$(priority)]</description>
  </rule>

  <rule id="100221" level="12">
    <if_sid>100220</if_sid>
    <field name="rule">^Interactive Shell in Application Container</field>
    <description>falco: shell in application container — $(output)</description>
    <mitre><id>T1059</id></mitre>
  </rule>

  <rule id="100222" level="12">
    <if_sid>100220</if_sid>
    <field name="priority">^(Critical|Alert|Emergency)$</field>
    <description>falco: high-priority runtime event — $(rule)</description>
  </rule>

</group>

После правок перезапустите менеджер: systemctl restart wazuh-manager.

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

Сначала убедитесь, что датчики живы — на каждой ноде под Falco должен отчитаться о загрузке драйвера:

kubectl -n falco get pods -o wide
kubectl -n falco logs -l app.kubernetes.io/name=falco --tail=5

Живая проверка end-to-end — та же, что в части 5, только теперь смотрим runtime: exec шелла в продовый под:

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

В логах пода Falco должна появиться строка Interactive Shell in Application Container с вашим подом, а на менеджере — алерт:

tail -f /var/ossec/logs/alerts/alerts.json | grep -E '100221|100222'

Если в логах Falco событие есть, а в Wazuh нет — разрыв в транспорте: смотрите под falcosidekick (kubectl -n falco logs deploy/falco-falcosidekick) и /var/ossec/logs/ossec.log на ошибки syslog-приёмника. Декодер и правила, как всегда, проверяются офлайн через /var/ossec/bin/wazuh-logtest: вставьте JSON-строку события и убедитесь, что Phase 2 нашла декодер falco, а Phase 3 — правило 100221.

Итог

Falco закрывает последнюю дыру цикла — то, что Wazuh органически не видел: процессы внутри контейнеров. Вместе с агентами на хостах (части 1–4), аудитом control-plane (часть 5) и runtime-детектами получается SIEM, у которого закрыт весь путь атакующего: вход на хост, запрос к API, syscall в ядре. Цена вопроса — DaemonSet на нодах и десяток строк YAML. Ни одной enterprise-лицензии, один дашборд, одни правила, один Active Response на всё.

Три слоя защиты в одном SIEM


Поделиться:

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