NetworkPolicy: изоляция трафика

Как из «плоской» сети, где каждый под достучится до каждого, сделать сеть с явными границами доверия.

NetworkPolicy — это правило-файрвол уровня пода: оно описывает, какому трафику (ingress/egress) разрешено идти к выбранным подам или от них.

По умолчанию сеть Kubernetes плоская и полностью открытая: любой под может обратиться к любому другому поду в любом namespace по его IP. Это удобно на старте, но опасно в проде. Если злоумышленник захватил один скромный под (скажем, обработчик картинок), из него по умолчанию виден весь кластер — база данных, платёжный сервис, внутренние API. NetworkPolicy позволяет это запретить и оставить только нужные связи.

Главное про «по умолчанию всё разрешено»

Ключевое правило, переворачивающее интуицию: пока на под не нацелена ни одна NetworkPolicy, ему разрешён весь трафик. Но как только хотя бы одна политика выбирает под (через podSelector) по направлению ingress, для этого направления включается режим «запрещено всё, кроме явно разрешённого». То есть политики работают как белый список: добавление первой же ingress-политики на под закрывает все входящие соединения, кроме тех, что вы перечислили.

Отсюда важное следствие: NetworkPolicy не «вычитает» доступ, а «начинает с нуля и добавляет». Это основа оборонительного мышления — мы не латаем дыры по одной, а закрываем всё и приоткрываем по необходимости.

Правила ingress и egress

Политика разделяет два направления: ingress — кто может обращаться к подам, egress — куда поды могут обращаться сами. Разрешим обращаться к подам app: db только подам app: api и только на порт 5432:

apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
  name: db-allow-api
  namespace: shop
spec:
  podSelector:
    matchLabels:
      app: db
  policyTypes:
    - Ingress
  ingress:
    - from:
        - podSelector:
            matchLabels:
              app: api
      ports:
        - protocol: TCP
          port: 5432

Теперь к базе достучится только API; любой другой под (даже в том же namespace) получит отказ на соединение. Источники в from можно задавать тремя способами: podSelector (по меткам подов), namespaceSelector (из определённых namespace) и ipBlock (по CIDR — для внешних адресов).

Изоляция namespace и default-deny

Частый приём — «закрыть namespace по умолчанию», запретив весь входящий трафик, а потом точечно открывать. Пустой podSelector: {} выбирает все поды namespace, а пустой блок ingress не разрешает ничего:

apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
  name: default-deny-ingress
  namespace: shop
spec:
  podSelector: {}
  policyTypes:
    - Ingress

После этого внутри shop ничего не принимает входящих, пока вы не добавите разрешающие политики поверх. Аналогично делают default-deny для egress, чтобы поды не ходили наружу без явного разрешения (включая выход в интернет). Чтобы разрешить трафик только внутри своего namespace, используют namespaceSelector с метками namespace.

Важно понимать, как несколько политик складываются: они аддитивны и работают по «или». Если на один под нацелены две политики, разрешённым считается трафик, подходящий хотя бы под одну из них — нельзя одной политикой «отнять» то, что разрешает другая, ведь deny-правил в NetworkPolicy просто нет. Поэтому типичная прод-схема такая: один default-deny закрывает namespace, а дальше каждый сервис добавляет узкую разрешающую политику ровно под свои связи. Например, после default-deny на egress практически всегда добавляют отдельную политику, разрешающую обращения к kube-dns на порт 53 (UDP и TCP) — иначе поды потеряют способность резолвить имена.

Zero-trust подход

Связка «default-deny на ingress и egress + точечные разрешения» и есть zero-trust в кластере: ни одно соединение не считается доверенным только потому, что оно «внутри». Каждая связь между сервисами должна быть явно описана. Да, это больше YAML и дисциплины, зато захват одного пода больше не открывает дорогу ко всему кластеру — поверхность атаки сжимается до перечисленных связей. Для прод-окружений с чувствительными данными (платежи, персональные данные) это фактически стандарт.

Как это работает под капотом

Важная тонкость: сам Kubernetes не применяет NetworkPolicy — он лишь хранит объекты в API. Реальное соблюдение правил — задача CNI-плагина. Если установлен плагин с поддержкой политик (Calico, Cilium, Weave), его агент на каждом узле читает объекты NetworkPolicy и программирует правила фильтрации в ядре — через iptables/ipset или eBPF (у Cilium). Если же стоит плагин без такой поддержки (например, голый flannel), объекты NetworkPolicy создаются, но молча игнорируются — трафик идёт как раньше. Поэтому защита есть ровно тогда, когда её обеспечивает CNI.

Частые ошибки

Опаснейшая ошибка — написать политики, убедиться, что «правила есть», и решить, что всё закрыто, не проверив, что CNI вообще умеет их применять: на flannel они не работают и создают ложное чувство безопасности. Вторая — забыть про egress и DNS: если вы сделали default-deny на egress, поды перестают резолвить имена (DNS-запрос к CoreDNS — это тоже egress), и всё «ломается без причины»; нужно явно разрешить трафик к kube-dns. Третья — перепутать «политика разрешает» с «политика запрещает»: в NetworkPolicy нет явных deny-правил, только allow; запрет получается из самого факта, что под кем-то выбран. Четвёртая — навесить ingress-политику на под и забыть, что egress при этом остаётся полностью открытым (направления независимы). Пятая — селектор по неверным меткам: политика просто никого не выберет и не подействует.

Итоги

  • По умолчанию сеть плоская: любой под доходит до любого; NetworkPolicy задаёт явные границы.
  • Политики — белый список: как только под выбран политикой по направлению, всё остальное по этому направлению запрещается.
  • ingress — кто может обращаться к подам, egress — куда поды ходят сами; направления независимы.
  • default-deny (пустой podSelector) + точечные allow = zero-trust: ни одно соединение не доверено по умолчанию.
  • Соблюдает правила CNI-плагин (Calico/Cilium/Weave); без поддержки в CNI (flannel) политики игнорируются.
Проверьте себя
1. Под app: db не выбран ни одной NetworkPolicy. Какой входящий трафик к нему разрешён?
AНикакой — поды по умолчанию изолированы
BВесь — пока под не выбран ни одной политикой, ему разрешён любой трафик
CТолько трафик из того же namespace
DТолько трафик от подов с меткой app: api
2. Вы применили корректные NetworkPolicy, но трафик по-прежнему идёт без ограничений. Что проверить в первую очередь?
AПерезапустить API-сервер, чтобы политики применились
BПоддерживает ли установленный CNI-плагин NetworkPolicy (например, flannel их игнорирует)
CДобавить в политику явное deny-правило
DВключить аддон ingress в minikube