Статья Network Policies в Kubernetes: изоляция подов на практике

diagram_1_preview.webp
Вы получаете алерт: один из подов в prod-кластере ведёт себя странно. Исходящие соединения, сканирование портов изнутри кластера, запросы к сервисам в соседних неймспейсах. К тому моменту, когда вы открываете логи - под уже опросил всё, что живёт рядом. Потому что Network Policies не было ни одной, и плоская сеть дала атакующему полный обзор на весь кластер.

Именно такая картина встречается примерно на каждом втором аудите. Команда разворачивает Kubernetes, пишет Helm-чарты, настраивает ingress, подключает мониторинг - и где-то в процессе решает, что неймспейсы сами по себе изолируют трафик. Нет. Неймспейсы в K8s - административные границы, не сетевые. Без NetworkPolicy любой под в кластере может говорить с любым другим по умолчанию.

Разберём, как Network Policies устроены под капотом, почему одного K8s API для серьёзной изоляции недостаточно, как Calico и Cilium расширяют стандартный функционал - и как выстроить изоляцию, которая работает в production, а не только в demo-кластере.

Как работают Network Policies​

Синтаксис и компоненты​

NetworkPolicy - ресурс в группе networking.k8s.io/v1. Определяет правила сетевого доступа к подам и от подов через selectors и списки разрешений.

YAML:
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
  name: allow-frontend-to-backend
  namespace: production
spec:
  podSelector:
    matchLabels:
      app: backend
  policyTypes:
    - Ingress
  ingress:
    - from:
        - podSelector:
            matchLabels:
              app: frontend
      ports:
        - protocol: TCP
          port: 8080

Ключевое поведение, которое путает новичков: NetworkPolicy работает как allowlist. Если к поду применена хоть одна политика - всё, что не указано явно как разрешённое, автоматически запрещено. Если политик нет вообще - всё разрешено. Это создаёт неприятный эффект: добавили одну политику для защиты одного порта, случайно заблокировали всё остальное.

podSelector отбирает поды, к которым применяется политика, через label matching. Пустой podSelector: {} захватывает все поды в неймспейсе - именно так строится default deny.

namespaceSelector - отбирает неймспейсы по лейблам. Важный нюанс: kubernetes.io/metadata.name стал автоматически проставляться на неймспейсы только с Kubernetes 1.21. В старых кластерах проверяйте лейблы на неймспейсах перед написанием политик.

ipBlock предназначен для внешних IP-диапазонов. Для pod-to-pod трафика внутри кластера не используйте ipBlock - IP подов меняются при перезапуске, и правила устаревают.

ports без указания конкретных портов применяет правило ко всем портам. Указывайте явно везде - это убирает неожиданности при расширении политики.

Ingress и Egress правила​

policyTypes говорит K8s, что именно контролирует политика. Если указать только Ingress - egress пода остаётся неограниченным. Если указать Egress без правил в секции egress - под теряет весь исходящий трафик, включая DNS-запросы.

Последнее - одна из самых частых причин, почему "политика поломала сервис": забыли разрешить UDP 53 к CoreDNS, поды перестали резолвить имена, сервис начал падать со странными ошибками connection timeout.

YAML:
# Явное разрешение DNS для пода с egress-политикой
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
  name: allow-dns-egress
  namespace: production
spec:
  podSelector:
    matchLabels:
      app: backend
  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

Комбинация namespaceSelector + podSelector в одном элементе from/to - это AND-логика: под должен быть И в указанном неймспейсе, И с указанными лейблами. Если написать их как два отдельных элемента списка - это OR. Путаница здесь стоит часов отладки.

CNI и поддержка Network Policies​

Почему стандартного K8s API недостаточно​

NetworkPolicy - это API-объект в Kubernetes, но сам K8s не занимается его enforcement. Это задача CNI-плагина. Если у вас Flannel - Network Policies будут создаваться в etcd и тихо игнорироваться. Flannel не реализует NetworkPolicy вообще.

diagram_2_cni.webp

Weave поддерживает NetworkPolicy L3/L4, но разработка фактически заморожена. В production его сейчас выбирают редко - слишком много вопросов к поддержке и производительности в крупных кластерах.

Для серьёзной изоляции выбор на практике - между Calico и Cilium, с принципиально разными архитектурными подходами.

Calico​

Calico в стандартном режиме строится на iptables. Производительность хорошая, совместимость широкая, почти все managed K8s-дистрибутивы поддерживают его из коробки. В режиме eBPF (конфигурируется при установке) производительность сравнима с Cilium, но L7-политики недоступны.

Стандартные networking.k8s.io/v1 NetworkPolicy в Calico работают корректно. Поверх них - собственный CRD-стек в группе projectcalico.org/v3 с двумя ключевыми типами: NetworkPolicy (namespace-scoped, с расширенным синтаксисом) и GlobalNetworkPolicy (cluster-wide).

Главное добавление Calico поверх стандарта: действие Deny (стандартные K8s NetworkPolicy умеют только allow), поле order для приоритетов, и работа с именованными сетевыми наборами (NetworkSets).

Cilium​

Cilium строится на eBPF и обходится без iptables. Policy enforcement происходит в eBPF-программах, которые загружаются прямо в ядро. Это даёт L7-фильтрацию без sidecar-прокси - политики применяются в datapath.

Архитектурное отличие от Calico: Cilium оперирует identities, а не IP. Каждому поду присваивается числовой идентификатор на основе его лейблов. Правила пишутся на идентификаторах - это устойчиво к ситуации, когда IP пода меняется при перезапуске.

Hubble - встроенный инструмент наблюдаемости для Cilium. Flow logs с причиной разрешения или дропа, визуализация трафика между сервисами. Без него отлаживать Cilium-политики в production - отдельное приключение, как подробно разобрано в разборе KSPM и runtime-мониторинга живого кластера.

Расширенные возможности Calico​

GlobalNetworkPolicy​

Стандартные K8s NetworkPolicy namespace-scoped - их нельзя применить к нескольким неймспейсам одновременно. Если вы хотите запретить всем подам в кластере обращаться к IMDS-эндпойнту облачного провайдера (169.254.169.254) - без Calico придётся писать одинаковые политики в каждый неймспейс вручную.

YAML:
apiVersion: projectcalico.org/v3
kind: GlobalNetworkPolicy
metadata:
  name: block-cloud-metadata
spec:
  order: 10
  selector: all()
  types:
    - Egress
  egress:
    - action: Deny
      destination:
        nets:
          - 169.254.169.254/32

selector: all() применяет политику ко всем подам во всех неймспейсах. Поле order определяет приоритет - меньшее число обрабатывается первым. Политика с order: 10 применится раньше политики с order: 100. Если у вас есть GlobalNetworkPolicy с Deny и order 10, плюс namespace-level политика с Allow - deny победит.

Другой типичный сценарий для GlobalNetworkPolicy: базовый egress-baseline на уровне кластера. Разрешить DNS и нужные внутренние сервисы, запретить всё остальное. В отдельных неймспейсах добавлять специфичные разрешения.

Политики на основе DNS​

Calico умеет разрешать egress-трафик по DNS-именам через spec.egress.destination.domains. Полезно, когда IP внешнего сервиса неизвестен заранее или меняется (CDN, облачные API).

YAML:
apiVersion: projectcalico.org/v3
kind: NetworkPolicy
metadata:
  name: allow-external-api
  namespace: production
spec:
  selector: app == 'payment-service'
  types:
    - Egress
  egress:
    - action: Allow
      destination:
        domains:
          - "api.payment-provider.com"
      ports:
        - protocol: TCP
          port: 443

Работает через DNS sniffer: Calico перехватывает DNS-ответы, извлекает IP и обновляет правила. Задержка между первым DNS-ответом и применением правила - от нескольких секунд. Для высоконагруженных latency-sensitive сервисов это нужно проверять отдельно. Ещё ограничение: только egress, только при использовании DNS в поде - прямые обращения по IP через это правило не пройдут.

Cilium: фильтрация прикладного уровня​

HTTP-aware фильтрация​

Cilium умеет смотреть внутрь HTTP-трафика через eBPF + Envoy и писать правила на уровне методов, путей и заголовков.

YAML:
apiVersion: cilium.io/v2
kind: CiliumNetworkPolicy
metadata:
  name: api-l7-policy
  namespace: production
spec:
  endpointSelector:
    matchLabels:
      app: api-service
  ingress:
    - fromEndpoints:
        - matchLabels:
            app: frontend
      toPorts:
        - ports:
            - port: "8080"
              protocol: TCP
          rules:
            http:
              - method: GET
                path: "/api/v1/.*"
              - method: POST
                path: "/api/v1/orders"

diagram_4_cilium_l7.webp

Практический смысл: у вас есть /admin endpoint, который не должен вызываться из соседних сервисов - только из управляющего пода. Стандартный NetworkPolicy разрешит трафик на 8080/tcp в целом, путь он не фильтрует. CiliumNetworkPolicy даст точечное разрешение на конкретные методы и пути.

L7-политики в Cilium применяются через Envoy-прокси, которая автоматически вставляется в datapath. Это добавляет небольшой latency overhead по сравнению с L3/L4. Для большинства сервисов разница незаметна, но в нагрузочных тестах проверяйте.

Безопасность на основе идентичности​

В Cilium вы не пишете from: ipBlock: 10.0.0.0/8. Вы пишете from: endpoint with labels app=frontend. Cilium сам следит за соответствием лейблов и IP, перекомпилирует eBPF-программы при изменении.

Это решает классическую проблему: при пересоздании пода IP меняется, IP-based правила в iptables устаревают. В Cilium identity привязана к лейблам - IP является деталью реализации.

Важный момент для смешанных кластеров: CiliumNetworkPolicy и стандартные K8s NetworkPolicy применяются аддитивно - трафик должен быть разрешён обеими политиками. Если применена стандартная NetworkPolicy с deny и CiliumNetworkPolicy с allow - трафик будет заблокирован.

Паттерны изоляции​

Default deny all​

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

diagram_3_default_deny.webp

YAML:
# Default deny all - запрет всего входящего и исходящего
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
  name: default-deny-all
  namespace: production
spec:
  podSelector: {}
  policyTypes:
    - Ingress
    - Egress

После применения - все поды в неймспейсе теряют связность. Сразу добавляете разрешение DNS, иначе поды не смогут резолвить имена:

YAML:
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
  name: allow-dns
  namespace: production
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

Применять default deny нужно по одному неймспейсу - с тестированием после каждого. Попытка накатить это на весь кластер за один раз заканчивается инцидентом.

Изоляция на уровне неймспейсов​

Production и staging не должны видеть друг друга, даже если живут в одном кластере. Запрещаете входящие соединения из всех неймспейсов кроме своего:

YAML:
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
  name: deny-from-other-namespaces
  namespace: production
spec:
  podSelector: {}
  policyTypes:
    - Ingress
  ingress:
    - from:
        - podSelector: {}
        # без namespaceSelector - только поды из этого же ns

Проверяйте лейблы на неймспейсах: kubectl get namespace production --show-labels. Без kubernetes.io/metadata.name или кастомного лейбла namespaceSelector работать не будет - политика применится, но правила не смогут корректно идентифицировать неймспейс как источник.

Микросегментация​

Микросегментация - это когда NetworkPolicy контролирует трафик на уровне каждой пары сервисов. Frontend ходит только в backend, backend - только в database, мониторинг - только читает /metrics.

YAML:
# Backend: разрешает входящий только от frontend
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
  name: backend-ingress
  namespace: production
spec:
  podSelector:
    matchLabels:
      app: backend
  policyTypes:
    - Ingress
  ingress:
    - from:
        - podSelector:
            matchLabels:
              app: frontend
      ports:
        - protocol: TCP
          port: 8080
---
# Database: разрешает входящий только от backend
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
  name: database-ingress
  namespace: production
spec:
  podSelector:
    matchLabels:
      app: database
  policyTypes:
    - Ingress
  ingress:
    - from:
        - podSelector:
            matchLabels:
              app: backend
      ports:
        - protocol: TCP
          port: 5432

Операционный вызов микросегментации - поддержание политик в актуальном состоянии. Добавили новый сервис - политики нужно обновить. Решается через GitOps: политики лежат в репозитории, меняются через PR, применяются автоматически. Без этого через полгода политики расходятся с реальной топологией сервисов.

Тестирование и отладка​

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

Самый надёжный способ - запустить тест-под и проверить связность вручную:

Bash:
# Запуск временного пода с нужными лейблами
kubectl run test-frontend \
  --image=nicolaka/netshoot \
  --labels="app=frontend" \
  -n production \
  --rm -it \
  -- sh

# Внутри - проверяем доступность backend
nc -zv backend-service 8080
# Expected: Connection to backend-service 8080 port [tcp/*] succeeded!

# Проверяем, что database недоступна напрямую из frontend
nc -zv database-service 5432 -w 3
# Expected: nc: connect to database-service port 5432 (tcp) timed out

Тестируйте оба направления: разрешённые соединения должны проходить, запрещённые - нет. Пропустите одно - либо сломаете production, либо изоляция не работает там, где должна.

Hubble для Cilium​

Если кластер на Cilium - Hubble даёт flow logs с причиной каждого дропа. Это несравнимо удобнее tcpdump при отладке политик:

Bash:
# Наблюдение за трафиком в неймспейсе
hubble observe --namespace production --follow

# Только дропы
hubble observe --namespace production --verdict DROPPED

# Трафик конкретного пода
hubble observe --pod production/backend-pod-xyz --follow

# JSON с деталями дропа
hubble observe --namespace production --verdict DROPPED -o json | \
  jq '.flow | {src: .source.pod_name, dst: .destination.pod_name, reason: .drop_reason_desc}'

DROPPED с причиной Policy denied - политика сработала, вы видите попытку и блокировку. Если видите дропы там, где ожидали разрешение - смотрите на identity endpoint (лейблы пода) и проверяйте соответствие selector в политике.

Отладка Calico​

Bash:
# Посмотреть все политики в кластере
calicoctl get networkpolicy --all-namespaces -o wide
calicoctl get globalnetworkpolicy -o wide

# Статус ноды
calicoctl node status

# Лог felix - компонент, применяющий политики
kubectl logs -n kube-system \
  $(kubectl get pods -n kube-system -l k8s-app=calico-node -o name | head -1) \
  -c calico-node | grep -i "policy\|drop\|deny"

Типичные ошибки: политика применена, но CNI не тот - проверьте kubectl get pods -n kube-system | grep calico. Конфликт между K8s NetworkPolicy и Calico CRD с разными order - deny с меньшим order победит allow с большим. После применения egress-политики пропал DNS - забыли разрешить UDP/TCP 53 к kube-dns.

Audit-режим в Cilium​

Перед включением enforcement Cilium позволяет запустить политики в режиме аудита: трафик не блокируется, только логируется в Hubble.

Bash:
# Включить audit mode для неймспейса
kubectl annotate namespace production \
  "policy.cilium.io/verdict-log"="audit"

Разворачивать изоляцию в большом кластере без предварительного audit-прогона - гарантированный инцидент в первую же ночь. Неделя в audit-режиме, сбор трафика в Hubble, анализ что реально ходит между сервисами - и только потом переключение в deny.

Что держать в голове​

Network Policies закрывают lateral movement по сети, но не являются единственным рубежом обороны в кластере. Они не защищают от компрометации через shared etcd, уязвимостей в рантайме или escape из контейнера - про то, что происходит когда атакующий всё-таки оказывается внутри пода и начинает двигаться дальше, есть подробный разбор техник пентеста контейнеров и компрометации K8s-кластера.

CNI выбирайте под задачу: Calico - если нужны стабильные L3/L4 политики с cluster-wide контролем и DNS-based rules, Cilium - если важны L7 и identity-based filtering плюс встроенная наблюдаемость через Hubble.

Начинайте с default deny в отдельном неймспейсе, не со всего кластера. Тестируйте оба направления - разрешённое и заблокированное. Политики держите в Git, меняйте через PR. И обязательно разрешайте DNS первым делом - иначе отладка начнётся со звонков о том, что «всё упало и непонятно почему».
 
Мы в соцсетях:

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

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

HackerLab