• Твой профиль заполнен на 0%. Заполни за 1 минуту, чтобы тебя нашли единомышленники и работодатели. Заполнить →

Статья Zero Trust Segmentation (ZTS): Микросегментация сетей в K8s и облаках

1776441753841.webp

Компрометация одного pod в кластере редко начинается с длинной APT-истории. Чаще всё проще: уязвимый frontend, утекший токен, кривой debug endpoint, SSRF до внутреннего сервиса. Дальше начинается то, что в реальных кластерах ломает всю модель защиты - межсервисный трафик живёт по правилу “раз трафик уже внутри, значит можно”. Пока пространство имён не изолировано, pod по умолчанию остаётся почти без ограничений по входящим и исходящим соединениям, а lateral movement упирается не в защиту, а в список DNS-имён и открытых портов.

Проблема периметровых фаерволов в облаке и Kubernetes не в том, что они “устарели”. Проблема в том, что они теряют контекст рабочих нагрузок. Pod живёт минуты, IP меняется, Service переписывает адреса, а политика на уровне CIDR не знает, кто перед ней - backend с доступом к платёжному API или случайный job-контейнер, которому вообще не собирались давать выход наружу. ZTS закрывает именно этот разрыв: политика привязывается не к подсети, а к идентичности рабочей нагрузки - меткам, пространству имён, service account, иногда к прикладному протоколу.

Почему периметр в Kubernetes даёт ложное чувство контроля​

У native NetworkPolicy довольно жёсткая и полезная семантика, но понимают её часто неправильно. Политика действует в пределах одного пространства имён, применяется к pod через label selector и управляет только сетевым доступом на уровнях L3/L4 между pod, пространствами имён и ipBlock. Сам Kubernetes эти правила не исполняет - это обязанность сетевого плагина. Отсюда первая ловушка: YAML в кластере появился, а фактической фильтрации может не быть, если CNI не поддерживает нужную механику так, как вы ожидаете.

Вторая ловушка - порядок правил. В native NetworkPolicy его нет. Политики складываются: итоговый набор разрешений - это объединение всех правил allow, которые применились к pod в данном направлении. Чтобы соединение между двумя pod прошло, его должны разрешить и исходящие правила на источнике, и входящие правила на получателе. Явного приоритета правил в духе “сначала глобальный запрет, потом точечные исключения” здесь нет. Поэтому в production-кластерах постоянно всплывает один и тот же эффект: кажется, что политика стала строже, а на деле где-то уже лежит другое разрешающее правило, и трафик всё ещё проходит.

Третья ловушка - NAT и сервисная абстракция. В Kubernetes легко написать правило, которое выглядит логично на схеме, но работает не так после трансляции адресов. Особенно неприятно это бьёт по исходящему трафику, когда вы пытаетесь описать внешние зависимости через ipBlock, а путь трафика на практике проходит через цепочку переписываний адресов, балансировку и сервисные правила. На бумаге доступ ограничен, а в реальной сети границы оказываются другими.

Что считать ZTS в Kubernetes, а что - просто набором YAML​

В кластере ZTS начинается не с CRD и не с eBPF. Базовый каркас всегда один:
  • запрет по умолчанию для входящих и исходящих соединений;
  • явные разрешения между рабочими нагрузками по идентичности, а не по случайным IP;
  • отдельный контроль исходящего трафика наружу;
  • наблюдаемость межсервисного обмена, иначе сегментация превращается в гадание;
  • защитные ограничения на весь кластер, которые нельзя обойти локальной политикой внутри одного пространства имён.
Native NetworkPolicy решает только часть задачи. Она хороша как базовый список разрешений на уровне пространства имён и меток pod, но у неё нет явного запрета, порядка правил, привязки к именам сервисов, TLS-семантики и полноценного контроля на уровне узлов. Это хороший нижний слой, но не полноценная модель микросегментации для большого production-кластера.

Calico закрывает следующий уровень зрелости. Он поддерживает native NetworkPolicy, свои политики внутри пространства имён и кластерные GlobalNetworkPolicy, умеет порядок правил, действия Deny, Log, Pass, селекторы по service account и работу не только с pod, но и с виртуальными машинами и сетевыми интерфейсами узлов. Отдельно полезны tiers - уровни политик. Они позволяют разделить правила платформенной команды, команды безопасности и прикладных команд так, чтобы одни не обходили других случайным разрешением в своём пространстве имён.

Cilium решает задачу с другой стороны. Он строит контроль доступа вокруг идентичности рабочей нагрузки, которую выводит из меток, а не из IP. CiliumNetworkPolicy расширяет возможности standard NetworkPolicy там, где встроенная модель уже тесна: фильтрация на уровне L7, правила для DNS/FQDN, более богатая наблюдаемость, кластерные политики и отдельные правила запрета. За это приходится платить сложностью: если одновременно использовать несколько типов политик без жёсткой дисциплины, итоговый набор разрешений быстро становится трудно предсказуемым.

Где eBPF действительно меняет игру​

У Cilium разница с iptables не сводится к лозунгу “быстрее и современнее”. Cilium загружает BPF-программы в сетевой стек Linux и применяет политику на более низком и прямом пути прохождения пакета. Это уменьшает зависимость от длинных netfilter/iptables-цепочек, которые плохо масштабируются, когда в кластере много pod, сервисов и сетевых правил.

Но интереснее другое. eBPF меняет не только производительность, но и сам способ наблюдения за трафиком. Когда политика, телеметрия и обработка пакетов живут ближе к datapath, становится проще увидеть, кто с кем говорил, по какому протоколу, с каким результатом и на каком уровне соединение было отрезано. На этом фоне iptables чаще оставляет команду один на один с разбором счётчиков, цепочек и побочных эффектов NAT.

При этом нельзя скатываться в упрощение “Calico - это iptables, Cilium - это eBPF”. У Calico тоже есть eBPF dataplane. Поэтому выбор между ними не сводится к вопросу “нужен ли нам eBPF”. Вопрос другой: где для вас важнее централизованное управление политиками, глобальные правила и уровни приоритета, а где - контроль по идентичности, видимость L7 и более тесная связка политики с телеметрией.

Практика: базовый профиль ZTS для production-кластера​

Ниже не демонстрационный стенд ради примера, а минимальный каркас, который обычно переживает первый контакт с production. Предположим обычную цепочку frontend -> backend -> database.

Изоляция пространства имён начинается с запрета по умолчанию​

Первый шаг скучный, но без него всё остальное бессмысленно:
YAML:
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
  name: default-deny
  namespace: payments
spec:
  podSelector: {}
  policyTypes:
    - Ingress
    - Egress
Пустой podSelector выбирает все pod’ы пространства имён. После применения такой политики рабочие нагрузки переходят в режим, где и входящий, и исходящий трафик закрыт до тех пор, пока вы явно не разрешите нужные потоки.

Сразу после этого кластер обычно “ломается” не из-за приложения, а из-за DNS. Если не открыть резолвинг имён, сервисы начинают зависать на обращениях к именам, и команда очень быстро пытается откатить всю сегментацию, хотя на деле не хватает одного обязательного правила:
YAML:
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
  name: allow-dns
  namespace: payments
spec:
  podSelector: {}
  policyTypes:
    - Egress
  egress:
    - to:
        - namespaceSelector:
            matchLabels:
              kubernetes.io/metadata.name: kube-system
          podSelector:
            matchLabels:
              k8s-app: kube-dns
      ports:
        - protocol: UDP
          port: 53
        - protocol: TCP
          port: 53
У native NetworkPolicy всё по-прежнему работает на уровнях L3/L4, без понимания доменных имён как сущностей прикладного уровня. Этого хватает, чтобы закрыть дыру “любой pod говорит куда угодно”, но не хватает, чтобы удобно и прозрачно описывать исходящий доступ к внешним сервисам в терминах имён, а не сетевых диапазонов.

Разрешения между сервисами должны быть двусторонними​

Backend должен принимать трафик только от frontend:
YAML:
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
  name: backend-ingress-from-frontend
  namespace: payments
spec:
  podSelector:
    matchLabels:
      app: backend
  policyTypes:
    - Ingress
  ingress:
    - from:
        - podSelector:
            matchLabels:
              app: frontend
      ports:
        - protocol: TCP
          port: 8080
Database должна принимать трафик только от backend:
YAML:
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
  name: db-ingress-from-backend
  namespace: payments
spec:
  podSelector:
    matchLabels:
      app: database
  policyTypes:
    - Ingress
  ingress:
    - from:
        - podSelector:
            matchLabels:
              app: backend
      ports:
        - protocol: TCP
          port: 5432
Но этого мало. Соединение backend -> database заработает только в том случае, если у backend отдельно открыт исходящий трафик к database. Именно здесь команды часто начинают искать проблему в DNS, kube-proxy или сетевом плагине, хотя причина банальна: модель требует, чтобы разрешение было согласовано с обеих сторон.
YAML:
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
  name: backend-egress-to-db
  namespace: payments
spec:
  podSelector:
    matchLabels:
      app: backend
  policyTypes:
    - Egress
  egress:
    - to:
        - podSelector:
            matchLabels:
              app: database
      ports:
        - protocol: TCP
          port: 5432

Исходящий трафик наружу: CIDR подходит не всегда​

Если нужно открыть доступ к фиксированному внешнему диапазону, native NetworkPolicy подойдёт:
YAML:
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
  name: backend-egress-to-partner-cidr
  namespace: payments
spec:
  podSelector:
    matchLabels:
      app: backend
  policyTypes:
    - Egress
  egress:
    - to:
        - ipBlock:
            cidr: 203.0.113.0/24
      ports:
        - protocol: TCP
          port: 443
Такой вариант нормален для статичных партнёрских сетей, но быстро начинает сыпаться на управляемых сервисах, SaaS API и любых внешних системах, где адреса плавают или часть маршрута идёт через сервисную трансляцию.

Для Calico тот же случай описывается ближе к реальной эксплуатации:
YAML:
apiVersion: projectcalico.org/v3
kind: GlobalNetworkPolicy
metadata:
  name: allow-approved-egress-domains
spec:
  order: 100
  namespaceSelector: projectcalico.org/name == "payments"
  selector: app == "backend"
  types:
    - Egress
  egress:
    - action: Allow
      protocol: UDP
      destination:
        ports:
          - 53
    - action: Allow
      destination:
        domains:
          - api.stripe.com
          - "*.sentry.io"
Для Cilium задача выглядит так:
YAML:
apiVersion: cilium.io/v2
kind: CiliumNetworkPolicy
metadata:
  name: backend-fqdn-egress
  namespace: payments
spec:
  endpointSelector:
    matchLabels:
      app: backend
  egress:
    - toFQDNs:
        - matchName: "api.stripe.com"
        - matchPattern: "*.sentry.io"
      toPorts:
        - ports:
            - port: "443"
              protocol: TCP
    - toEndpoints:
        - matchLabels:
            "k8s:io.kubernetes.pod.namespace": kube-system
            "k8s:k8s-app": kube-dns
      toPorts:
        - ports:
            - port: "53"
              protocol: ANY
          rules:
            dns:
              - matchPattern: "*"
Но у таких правил есть эксплуатационные ограничения. Доступ по FQDN в Cilium завязан на DNS proxy и кэширование IP из DNS-ответов. Wildcard здесь не волшебство, а вполне конкретная механика с набором ограничений. Если команда этого не понимает, очень быстро появляется неприятный разрыв между тем, как политика выглядит в YAML, и тем, как она ведёт себя на реальном трафике.

Когда разговор доходит до выбора между Calico и Cilium, обычно всё быстро скатывается в упрощение уровня “iptables против eBPF”. Если хочется посмотреть на этот выбор шире - с точки зрения enforcement, наблюдаемости, L7-фильтрации и эксплуатационных компромиссов, загляните в статью Cilium vs Calico: Сравнение CNI-плагинов с точки зрения безопасности и eBPF.

Наблюдаемость: если межсервисный трафик не виден, сегментации у вас нет​

Сегментация без наблюдаемости быстро превращается в череду аварийных разрешений “лишь бы заработало”. У Cilium для этого есть Hubble. Его ценность не в красивой картинке, а в том, что он показывает реальную карту взаимодействий: кто с кем говорил, какой был протокол, чем закончилось соединение, где сработал запрет. Если включена фильтрация на уровне L7, видно уже не только соединение как факт, но и часть прикладного контекста.

Для быстрой диагностики обычно хватает двух команд. Первая показывает поток событий уровня L7:
Bash:
hubble observe -f -t l7 -o compact
Вторая вытаскивает только отброшенный и аварийный трафик:
Bash:
hubble observe --verdict DROPPED --verdict ERROR
Но и здесь есть подводный камень. Видимость уровня L7 легко превращается в сбор чувствительных данных, если бездумно тащить эти события в журналирование и SIEM. В запросах могут оказаться параметры, заголовки, идентификаторы и прочие данные, которые потом внезапно начинают жить в индексах, резервных копиях и правилах поиска.

У Calico наблюдаемость строится иначе. Там важны Flow Logs, а в свежих версиях ещё и Whisker как инструмент просмотра потоков. Для команды безопасности это часто даже удобнее: вместо попытки угадать, кто с кем общается, вы видите реальный межсервисный обмен, точки отказа, отклонённые соединения и последствия новых политик.

Shadow traffic чаще всего всплывает в тот момент, когда вы переводите кластер на запрет по умолчанию и внезапно находите десятки неописанных зависимостей. Это тот самый трафик, о котором никто не думал, пока он проходил бесконтрольно. Для Calico здесь полезны Log-правила и staged-политики, которые позволяют сначала посмотреть на последствия, а уже потом включать жёсткое применение. Это куда ближе к нормальной эксплуатации, чем писать запреты вслепую и надеяться, что ничего важного не отвалится.

ZTS вместе с Service Mesh: два уровня, разные задачи​

NetworkPolicy и service mesh часто пытаются сравнивать лоб в лоб, хотя они отвечают на разные вопросы. Сетевая политика отвечает на вопрос: может ли этот источник вообще установить соединение с этим получателем. Service mesh отвечает на другой вопрос: кто именно пришёл, в каком контексте, с каким удостоверением и что именно ему разрешено делать на уровне запроса.
1776441289454.webp

Такая связка полезна именно как многослойная защита. Если между сервисами не должно быть сетевого пути, это режется на уровне NetworkPolicy, GlobalNetworkPolicy или CiliumNetworkPolicy. Если путь разрешён, mesh всё равно не обязан пускать трафик дальше автоматически: он может потребовать mTLS, проверить источник по service account или identity-представлению и отдельно ограничить методы, пути и прикладной контекст. За счёт этого NetworkPolicy и mesh не дублируют друг друга, а закрывают разные участки одной и той же проблемы.

Минимальная связка для пространства имён payments может выглядеть так:
YAML:
apiVersion: security.istio.io/v1
kind: PeerAuthentication
metadata:
  name: default
  namespace: payments
spec:
  mtls:
    mode: STRICT
---
apiVersion: security.istio.io/v1
kind: AuthorizationPolicy
metadata:
  name: payments-api
  namespace: payments
spec:
  selector:
    matchLabels:
      app: backend
  action: ALLOW
  rules:
    - from:
        - source:
            principals:
              - cluster.local/ns/payments/sa/frontend
      to:
        - operation:
            methods: ["POST"]
            paths: ["/pay"]
И рядом с этим остаётся обычное сетевое разрешение на уровне L4:
YAML:
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
  name: backend-ingress-from-frontend
  namespace: payments
spec:
  podSelector:
    matchLabels:
      app: backend
  policyTypes:
    - Ingress
  ingress:
    - from:
        - podSelector:
            matchLabels:
              app: frontend
      ports:
        - protocol: TCP
          port: 8443
У такой схемы понятный смысл. Если кто-то пытается обойти mesh, L3/L4-политика всё ещё режет саму достижимость сервиса. Если сетевой доступ открыт, mesh всё ещё требует mTLS и может зарубить запрос по идентичности, пути или методу. Это нормальная многослойная защита, а не дублирование одного и того же механизма.

Но здесь тоже нельзя игнорировать ограничения реализации. Когда в одном кластере встречаются Cilium, aggressive eBPF-оптимизации и Istio, надо смотреть не на рекламные схемы, а на реальный путь прохождения пакета, совместимость режимов и побочные эффекты для сетевого стека узла.

Если хочется докрутить этот слой дальше и посмотреть, как такие схемы ломаются не в теории, а в конфигурации, стоит открыть материал Service Mesh: ошибки конфигурации. Там мы подробно разобрали неприятный, но очень жизненный момент: наличие mTLS в дашборде ещё не гарантирует, что трафик реально идёт так, как вы ожидаете, особенно рядом с кривыми NetworkPolicy.

Как выглядит production-ready ZTS для multi-tenant кластера​

Рабочая схема обычно складывается в несколько слоёв.

Сверху - общий защитный слой на весь кластер. В Calico он естественно ложится на tiers и GlobalNetworkPolicy: платформенная команда и команда безопасности держат глобальный запрет по умолчанию, системные исключения, правила для DNS, журналирования, ingress/egress-шлюзов и служебных пространств имён. В Cilium ту же роль берут на себя кластерные политики и централизованный контроль над их изменением.

Ниже - базовый профиль на уровне каждого арендатора или пространства имён: запрет по умолчанию и только явные разрешения между сервисами. Ещё ниже - дисциплина исходящего трафика: где возможно, фиксированные диапазоны; где сеть живёт внешними API и SaaS-сервисами, правила по доменным именам. Поверх этого - нормальная наблюдаемость межсервисного трафика, пробный ввод политик, логирование отказов и аккуратная интеграция с SIEM без превращения телеметрии в свалку.

Если в кластере работает несколько команд, без централизованного слоя управления ZTS почти всегда расползается в хаос локальных разрешений внутри пространств имён. Calico в этом месте силён именно моделью управления политиками: tiers, глобальные правила, порядок применения, отдельные действия log, deny, pass. Cilium сильнее там, где политика и наблюдаемость должны жить как один механизм: идентичности рабочих нагрузок, Hubble, фильтрация на уровне L7, правила по DNS/FQDN и более тесная связка с eBPF datapath.

Подведем итоги​

Пока в кластере нет сегментации, внутренняя сеть живёт слишком расслабленно. Один пробитый pod - и дальше вопрос уже не в том, был ли вход, а в том, сколько сервисов окажутся достижимыми по умолчанию. В этот момент очень быстро выясняется, что внутренний трафик у многих живёт вообще без внятных границ.

ZTS наводит здесь порядок. Каждый разрешённый поток приходится назвать, обосновать и потом ещё наблюдать вживую, а не держать в голове как архитектурную легенду двухлетней давности. Работа не самая романтичная: политики ломают скрытые зависимости, egress вытаскивает наружу забытые интеграции, а east-west трафик показывает про кластер больше, чем хотелось бы. Зато после этого сеть перестаёт быть серой зоной, где всё как-то само договорилось.

Если в результате компрометации одного сервиса атакующий упирается не в широкий внутренний доступ, а в цепочку явных ограничений, телеметрию и понятные правила, значит сегментация в кластере действительно появилась. Всё остальное обычно заканчивается либо красивой схемой, либо allow-all где-нибудь в углу репозитория.
 

Вложения

  • 1776441217479.webp
    1776441217479.webp
    33,8 КБ · Просмотры: 4
Мы в соцсетях:

Взломай свой первый сервер и прокачай скилл — Начни игру на HackerLab

🚀 Первый раз на Codeby?
Гайд для новичков: что делать в первые 15 минут, ключевые разделы, правила
Начать здесь →
🔴 Свежие CVE, 0-day и инциденты
То, о чём ChatGPT ещё не знает — обсуждаем в реальном времени
Threat Intel →
💼 Вакансии и заказы в ИБ
Pentest, SOC, DevSecOps, bug bounty — работа и проекты от проверенных компаний
Карьера в ИБ →

HackerLab