Микросегментация в Kubernetes через Cilium: модель угроз, правила и проверка через Hubble
Внедрили Cilium Network Policy в K8s-кластере после рекомендаций НКЦКИ по lateral movement: описываем модель угроз, правила Ingress/Egress и как Hubble помогает проверить изоляцию.
Рост атак через lateral movement - НКЦКИ и BSS рекомендуют микросегментацию как ключевую меру 2024
НКЦКИ в этом году прямым текстом включил микросегментацию в список приоритетных мер против атак через lateral movement. BSS в своём отчёте за первое полугодие нарисовал примерно ту же картину: как только атакующий получает точку входа в периметр, дальше он движется по внутренней сети практически без сопротивления. В большинстве Kubernetes-кластеров, с которыми мы работаем, это ровно так и устроено - поды общаются со всеми подряд, и никакого принципа least privilege внутри кластера нет.
Решили закрыть этот пробел на одном из проектов. Инструмент выбрали Cilium: он работает на уровне eBPF, не требует sidecar-проксей, и в нём есть Hubble - встроенный инструмент наблюдаемости, без которого проверить, что политики реально работают, было бы заметно сложнее.
Модель угроз: что мы защищали
Прежде чем писать правила, разобрались - от чего именно защищаемся. Три сценария, которые нас беспокоили.
Первый - компрометация одного пода и горизонтальное движение. Атакующий получает RCE в поде, например через уязвимость в web-приложении. Без сегментации у него сразу доступ ко всем остальным подам в кластере: базам данных, сервисам внутреннего API, очередям сообщений. Задача - чтобы скомпрометированный под мог общаться только с тем, с чем ему положено по логике приложения.
Второй - утечка данных через неожиданный egress. Под отправляет данные на внешний адрес. В стандартной конфигурации Kubernetes это никак не ограничено. Мы хотим, чтобы поды с доступом к чувствительным данным не могли выходить в интернет напрямую - только через явно разрешённые маршруты.
Третий - атака на управляющую плоскость кластера. Скомпрометированный под не должен иметь возможности обращаться к API-серверу Kubernetes или etcd. Это базовая изоляция, которую нередко забывают сделать.
Как устроены политики
Cilium Network Policy работает поверх стандартных Kubernetes NetworkPolicy, но с более богатой семантикой: можно фильтровать не только по namespace и label, но и по DNS-именам, HTTP-путям, протоколам L7. Для большинства наших случаев хватило L3/L4.
Базовый принцип - default deny для всего namespace, потом явные разрешения. Выглядит это примерно так: сначала вешается CiliumNetworkPolicy с policyTypes Ingress и Egress без единого правила - всё запрещено. Потом для каждого деплоймента прописываются точные разрешения.
Для backend-сервиса, который общается только с базой данных и очередью:
- Ingress - разрешить трафик только от frontend-namespace по label
app: frontend. - Egress - разрешить только до подов с label
app: postgresна порт 5432 и до подов с labelapp: rabbitmqна 5672. Плюс CoreDNS на 53/UDP - без этого DNS не работает, и поды не могут ресолвить имена вообще.
Для web-frontend отдельная история: ему нужен egress наружу для запросов к внешним API. Здесь мы использовали Cilium-specific возможность - FQDN-based policy. Вместо разрешения по IP прописываем разрешённые DNS-имена. Это чище, чем таскать за собой список IP, которые имеют привычку меняться.
Политику для доступа к API-серверу решили отдельно: явный блок egress на CIDR control-plane нод для всех namespace, кроме системных. Это немного грубо, но надёжно.
DNS - самый неочевидный момент
Первые тесты показали: поды теряют DNS сразу после включения default deny, даже если CoreDNS разрешён. Причина - CoreDNS сам ходит наружу за ответами на рекурсивные запросы. Нужно разрешить Egress из namespace kube-system к upstream DNS-серверам, иначе резолюция ломается для всего кластера.
Второй момент с DNS: FQDN-политики Cilium работают через механизм перехвата DNS-ответов. Cilium видит, какой IP вернул DNS для разрешённого имени, и временно добавляет его в разрешённый список. Если TTL у записи короткий и IP меняется быстро - бывают небольшие окна, когда трафик блокируется. В нашем случае это не было проблемой, но знать стоит.
Hubble: как проверяли, что работает
Писать политики вслепую - это боль. Hubble решает именно эту задачу: он показывает реальные потоки трафика между подами с пометкой forwarded / dropped и ссылкой на конкретную политику, которая заблокировала или пропустила трафик.
Мы использовали два инструмента в связке. Hubble UI - для первичного осмотра: видно граф зависимостей, можно быстро найти неожиданные соединения, которые мы не учли в политиках. Hubble CLI - для точечной диагностики: hubble observe --namespace production --verdict DROP выдаёт в реальном времени всё заблокированное. Это позволило поймать несколько соединений, которые мы честно не знали, что они существуют - оказалось, что один сервис периодически обращается к соседнему namespace за конфигом, и это нигде не было задокументировано.
После включения политик прогнали smoke-тесты приложения и параллельно смотрели в Hubble на DROP-события. Если что-то падало - сразу видели, какое именно соединение заблокировалось, и либо добавляли правило, либо шли разбираться, зачем вообще это соединение существует.
Что получилось
Политики работают, приложение работает, Hubble показывает ожидаемую картину. Несколько соединений, которые мы обнаружили в процессе, оказались техническим долгом: сервисы общались напрямую там, где должны были ходить через API-gateway. Часть исправили сразу, часть зафиксировали как задачу.
Полный аудит изоляции - это не разовая история. Политики нужно актуализировать при каждом добавлении нового сервиса, иначе default deny начнёт ломать деплойменты в самый неподходящий момент. Поэтому договорились с командой разработки: каждый новый сервис приходит с явно задокументированными зависимостями - кто с ним общается и куда он ходит сам. Это можно автоматически транслировать в заготовку политики.
Главный вывод из этого проекта: микросегментация в Kubernetes технически не сложна. Сложная часть - инвентаризация реальных зависимостей перед тем, как писать правила. Если этот шаг пропустить и сразу включать default deny - получите шквал дропов и сломанное приложение. Если сначала дать Hubble понаблюдать за трафиком в режиме без блокировок, а потом переходить к политикам - всё становится значительно управляемее.