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

Cilium Tetragon: runtime-защита на eBPF, которая не просто логирует, а блокирует

9 мин чтения

Falco, Tracee и большинство eBPF-инструментов security-мониторинга умеют одно: заметить, что что-то произошло, и отправить алерт — уже после того, как syscall выполнился и данные, возможно, успели уйти из хоста. Tetragon, eBPF-движок runtime-безопасности из проекта Cilium, умеет больше: остановить процесс синхронно в ядре в тот момент, когда сработал kprobe, — до того, как syscall вообще успеет вернуть управление вызвавшему коду. Разбираем, из чего собирается TracingPolicy, чем блокировка в ядре отличается от постфактум-алерта и почему включать Sigkill в первый же день — это способ положить продакшен, а не защитить его.

Содержание

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

От алерта к Sigkill: зачем ещё один eBPF-инструмент безопасности

Detection-only — это не недостаток конкретного инструмента, а архитектурное ограничение целого класса решений. Falco цепляет kprobes и tracepoints, собирает события в userspace, прогоняет их через правила и в случае совпадения шлёт уведомление — в Slack, в SIEM, куда угодно. К моменту, когда это уведомление кто-то прочитает (или даже когда его обработает автоматика), исходный syscall давно завершился: файл прочитан, шелл запущен, соединение установлено. Detection здесь работает как камера видеонаблюдения — отличный источник для расследования инцидента постфактум, но не физическая преграда в момент, когда происходит нарушение.

Tetragon устроен иначе не потому, что использует другой eBPF-хук, а потому, что действие можно привязать прямо к точке перехвата: TracingPolicy с действием Sigkill завершает процесс синхронно, из ядра, прежде чем управление вернётся из перехваченной функции обратно в вызывающий код. Разница не в скорости реакции — счёт не идёт на миллисекунды алертинга против миллисекунд блокировки, — а в том, что до записи в лог событие для Tetragon уже необратимо предотвращено, а не просто зафиксировано.

Detection постфактум vs Sigkill в ядре до завершения syscall

Почему это стоит разбирать именно сейчас, а не когда Tetragon только анонсировали: за 2026 год проект прошёл путь от версии 1.4 (февраль), которая заметно упростила написание и отладку TracingPolicy — до неё авторам политик приходилось куда больше гадать, почему селектор не матчится, — до 1.7 (апрель) с более зрелым инструментарием вокруг enforcement. Это не «фича неделю как в бете» — это инструмент, который за пару релизов дошёл до состояния, когда включать блокировку в проде уже не авантюра, если делать это по правильному паттерну раскатки (о нём — ниже).

Анатомия TracingPolicy: из чего собирается правило

TracingPolicy — кастомный ресурс (apiVersion: cilium.io/v1alpha1, kind: TracingPolicy), который описывает три вещи: где перехватывать, что сравнивать и что делать при совпадении.

Анатомия TracingPolicy: hook → selectors → matchActions

Точка перехвата — один из нескольких типов хуков: kprobes (функции ядра и системные вызовы), tracepoints (заранее определённые точки в ядре, более стабильные между версиями, чем произвольные символы), uprobes (функции в userspace-бинарниках) и более редкие fentries/lsmhooks/usdts. Для практики важнее всего kprobes — именно на них строится почти любая политика runtime-безопасности: перехват execve для контроля запуска процессов, перехват операций с файловыми дескрипторами для контроля доступа к файлам.

Селекторы — фильтрация внутри хука, без которой пришлось бы ловить вообще все вызовы функции по всему кластеру:

matchActions — то, что происходит при совпадении всех условий селектора. Post — просто сгенерировать событие (это и есть режим наблюдения); Sigkill — синхронно убить процесс из ядра; Override — подменить возвращаемое значение вызова (требует поддержки CONFIG_BPF_KPROBE_OVERRIDE в ядре) и передать управление дальше так, будто функция вернула, например, -EPERM; Signal — отправить произвольный сигнал, но асинхронно — операция может успеть завершиться до того, как процесс получит и обработает сигнал, поэтому для жёсткой блокировки полагаться на Signal не стоит, это скорее мягкое уведомление процессу, чем гарантия остановки.

Runtime vs deploy-time: где Tetragon стоит рядом с admission-контролем

Легко перепутать Tetragon с admission-контролем вроде Kyverno или ValidatingAdmissionPolicy — оба в итоге «блокируют что-то нехорошее в Kubernetes». Разница — в моменте, а не в строгости.

Deploy-time admission-контроль vs runtime-enforcement на ноде

Admission-контроль — это deploy-time: kube-apiserver проверяет объект в момент kubectl apply, до того как под вообще запланирован на ноду. ValidatingAdmissionPolicy или Kyverno отлично отловят под, который просит hostPath, работает от root или тянет образ с тегом :latest, — но они ничего не знают про то, что происходит внутри уже разрешённого и запущенного контейнера через десять минут после старта. Если в зависимость легитимного образа кто-то протащил supply-chain-компрометацию, которая на старте выглядит безобидно, а посреди рабочего дня пытается развернуть /bin/sh и утащить переменные окружения наружу, — admission-контроль это событие просто не увидит: под уже прошёл проверку и живёт своей жизнью.

Tetragon работает на другом слое — runtime, на уровне ноды, весь жизненный цикл контейнера, а не только момент его создания. Он не знает и не обязан знать, соответствует ли манифест пода политике безопасности организации, — зато видит каждый execve, каждую операцию с файловым дескриптором, каждый перехваченный syscall внутри уже работающего контейнера. Это ровно то место, куда admission-контроль структурно не дотягивается: не потому, что Kyverno или VAP написаны слабо, а потому, что они не запускаются повторно на каждый syscall внутри уже живого пода — и не должны, иначе admission стал бы узким местом для всего рантайма кластера. Правильная модель — не «который из двух выбрать», а два независимых слоя защиты: один держит периметр входа в кластер, второй — то, что происходит внутри после входа.

Таблица сравнения

ПараметрFalco / detection-onlyTetragon Sigkill/OverrideAdmission-контроль (Kyverno/VAP)
Момент действияпосле завершения syscallдо возврата из перехваченной функциидо создания объекта в API
Что видитпроцессы, файлы, сеть на нодето же, но с правом действияманифест ресурса
Реакция на совпадениеалерт в userspaceблокировка в ядреreject запроса к API
Риск ложного срабатыванияшумный алертубитый легитимный процессотклонённый деплой
Защищает отизвестной активности постфактумэксплуатации уже внутри контейнеранебезопасного манифеста на входе
Требует ядра с eBPFдаданет

Что нужно, чтобы раскатить политику по правилам

Задача: заблокировать запуск /bin/sh и /bin/bash внутри контейнеров с меткой security.hogin.pro/immutable: "true" — типичный случай для сервисов, которым интерактивный шелл вообще не нужен ни при каких обстоятельствах.

Сначала — политика без enforcement, только наблюдение:

apiVersion: cilium.io/v1alpha1
kind: TracingPolicy
metadata:
  name: "block-shell-in-immutable"
spec:
  podSelector:
    matchLabels:
      security.hogin.pro/immutable: "true"
  kprobes:
  - call: "sys_execve"
    syscall: true
    args:
    - index: 0
      type: "string"
    selectors:
    - matchArgs:
      - index: 0
        operator: "Prefix"
        values:
        - "/bin/sh"
        - "/bin/bash"
      matchActions:
      - action: "Post"

matchActions: [Post] здесь — это и есть audit-режим: селектор уже полностью настроен и матчится ровно на те же события, что впоследствии будет блокировать, но пока только генерирует событие, ничего не останавливая. Применяем и смотрим поток совпадений через tetra:

kubectl apply -f block-shell-in-immutable.yaml

tetra getevents -o compact --namespace prod \
  --pod-labels security.hogin.pro/immutable=true
# 🚀 process   prod/api-6f9. /bin/sh -c "healthcheck.sh"
# 💥 exit      prod/api-6f9. /bin/sh     0

Именно на этом шаге чаще всего всплывает то, ради чего вообще существует audit-режим: healthcheck-скрипт, sidecar или CI-раннер, который дергает /bin/sh легитимно и о котором никто не вспомнил на этапе проектирования политики. Здесь есть два honest-варианта: сузить podSelector/containerSelector, исключив такие контейнеры из-под действия политики, либо признать, что метка immutable на этом workload’е была навешена преждевременно. Оба варианта лучше, чем узнать про этот /bin/sh в момент, когда Sigkill уже убил healthcheck и под пошёл в перезапуск по CrashLoopBackOff.

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

      matchActions:
      - action: "Sigkill"
kubectl apply -f block-shell-in-immutable.yaml

Структура политики, селекторы, podSelector — всё осталось прежним. Изменилось одно действие в одном месте. Это и есть весь смысл паттерна: audit и enforcement — это не два разных ресурса и не два режима работы Tetragon, а одна и та же политика с разным matchActions, что делает переключение обратимым и локальным изменением, а не миграцией на другую систему.

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

kubectl exec -n prod api-6f9xy -- /bin/sh -c "echo test"
# command terminated with exit code 137

tetra getevents -o compact --namespace prod
# 🚀 process   prod/api-6f9. /bin/sh -c "echo test"
# 💀 kill      prod/api-6f9. /bin/sh                    SIGKILL

Exit-код 137 (128 + сигнал 9) и событие kill с SIGKILL в потоке Tetragon подтверждают, что блокировка сработала синхронно из ядра, а не через отложенный внешний контроллер. Легитимный трафик того же пода — HTTP-запросы, здоровый execve разрешённых бинарников — идёт как обычно: политика матчится только на sys_execve с аргументом, начинающимся на /bin/sh или /bin/bash, и не трогает ничего за пределами этого условия.

Итог

eBPF runtime-security перестаёт быть красивым источником логов ровно в тот момент, когда matchActions в TracingPolicy переключают с Post на Sigkill — но это переключение нужно заслужить периодом наблюдения на реальном трафике, а не включить в первый день из энтузиазма. Tetragon не заменяет admission-контроль вроде Kyverno или VAP и не заменяется им — они защищают разные моменты жизни workload’а, вход в кластер и то, что происходит после него, и по-хорошему должны работать вместе. Цена enforcement на уровне ядра реальна: неправильно откалиброванный селектор убивает не атакующего, а ваш собственный healthcheck. Плата за это — возможность остановить эксплуатацию до того, как она успеет что-то сделать, а не написать про неё пост-мортем.


Поделиться:

Следующая статья
containerd 1.x доживает: что сломается при переходе на containerd 2.0 перед Kubernetes 1.36