Kubernetes NetworkPolicy: как изолировать сервисы и не дать одному поду достучаться до чужой базы

Kubernetes NetworkPolicy: как изолировать сервисы и не дать одному поду достучаться до чужой базы

Коротко:

  • По умолчанию все поды в кластере могут общаться друг с другом без каких-либо ограничений - это нужно исправлять явно.
  • NetworkPolicy работает только если в кластере установлен CNI-плагин с поддержкой политик: Calico, Cilium, Weave. Flannel без дополнений политики не применяет.
  • Политика без явного podSelector применяется ко всем подам в namespace - легко случайно заблокировать лишнее.
  • Ingress-правило разрешает входящий трафик к поду, egress - исходящий от него. Оба направления нужно описывать отдельно.
  • Чтобы убедиться, что политика работает, недостаточно посмотреть на YAML - нужно проверять соединение реальными запросами изнутри кластера.
  • Deny-all как базовое правило для namespace - самый надежный старт, после которого добавляют только нужные разрешения.

Почему открытая сеть внутри кластера - это риск

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

Это поведение заложено в Kubernetes намеренно: на старте оно упрощает разработку. Но в production такая открытость становится проблемой. Концепция least privilege - минимально необходимых прав - должна распространяться и на сетевой уровень. Именно для этого существует Kubernetes NetworkPolicy.

Сетевая политика описывает, каким подам разрешено отправлять трафик к данному поду (ingress) и куда этот под может обращаться сам (egress). Всё остальное по умолчанию блокируется - но только если хотя бы одна политика применена к поду. Без единой политики под остается полностью открытым.

Как устроен объект NetworkPolicy

NetworkPolicy - это ресурс Kubernetes, который живет в конкретном namespace. Он выбирает целевые поды через podSelector и описывает разрешенный трафик через секции ingress и egress.

Минимальная структура выглядит так:

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

Это правило говорит: «К подам с меткой app: api разрешен входящий трафик на порт 8080 только от подов с меткой app: frontend». Всё остальное - заблокировано для направления ingress.

Важная деталь: policyTypes определяет, какие направления вообще контролирует данная политика. Если указать только Ingress, egress-трафик остается неограниченным. Если указать оба направления, то для каждого нужно описать явные разрешения - иначе соответствующий трафик будет заблокирован.

Полная изоляция namespace: deny-all как отправная точка

Рекомендуемый подход для production: начинать с запрета всего трафика в namespace, а затем добавлять разрешения точечно. Это называется default-deny.

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

Пустой podSelector: {} означает «применить ко всем подам в namespace». После применения этого манифеста весь трафик между подами в namespace production заблокирован - включая исходящий DNS, что ломает резолвинг имен.

Поэтому сразу после deny-all добавьте разрешение на DNS:

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
      ports:
        - protocol: UDP
          port: 53
        - protocol: TCP
          port: 53

Без этого правила сервисы не смогут резолвить имена других сервисов через CoreDNS - и упадут с ошибками вроде dial tcp: lookup db-service: no such host, которые выглядят как проблемы с приложением, а не с сетевой политикой.

Разрешаем только нужные соединения: реальные примеры

Сценарий 1: API-сервис должен ходить только в свою базу

Допустим, у вас есть под app: api, который должен обращаться к PostgreSQL с меткой app: postgres, но не должен иметь доступа к Redis или другим сервисам.

apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
  name: api-egress-to-postgres
  namespace: production
spec:
  podSelector:
    matchLabels:
      app: api
  policyTypes:
    - Egress
  egress:
    - to:
        - podSelector:
            matchLabels:
              app: postgres
      ports:
        - protocol: TCP
          port: 5432

Теперь api-под может инициировать соединения только на порт 5432 к postgres. К Redis, к внешним API и к другим сервисам - нет. Если завтра разработчик случайно укажет в коде адрес чужой базы, соединение просто не пройдет.

Сценарий 2: разные команды в разных namespace не должны пересекаться

Предположим, в кластере есть два namespace: team-payments и team-catalog. По умолчанию поды из одного namespace могут обращаться к подам другого. Чтобы это запретить, добавьте default-deny для каждого namespace и разрешите трафик только внутри него:

apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
  name: allow-same-namespace
  namespace: team-payments
spec:
  podSelector: {}
  policyTypes:
    - Ingress
  ingress:
    - from:
        - podSelector: {}

Пустой podSelector в секции from означает «любой под из того же namespace». Трафик из team-catalog при этом заблокирован.

Сценарий 3: разрешить доступ из другого namespace

Если нужно, чтобы под из monitoring мог собирать метрики из team-payments, используйте namespaceSelector:

apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
  name: allow-monitoring-ingress
  namespace: team-payments
spec:
  podSelector:
    matchLabels:
      app: api
  policyTypes:
    - Ingress
  ingress:
    - from:
        - namespaceSelector:
            matchLabels:
              kubernetes.io/metadata.name: monitoring
          podSelector:
            matchLabels:
              app: prometheus
      ports:
        - protocol: TCP
          port: 9090

Обратите внимание: когда namespaceSelector и podSelector стоят рядом в одном элементе списка from (без разделяющего дефиса), это означает «И то, И другое» - только поды prometheus из namespace monitoring. Если поставить дефис перед podSelector, это будет «ИЛИ» - любой под из namespace monitoring ИЛИ любой под с меткой prometheus из любого namespace. Это одна из самых частых точек ошибки в YAML.

Egress-политики: контроль исходящего трафика

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

Пример: запретить всем подам в namespace ходить в интернет, кроме одного - payment-сервиса, которому нужен внешний платежный шлюз:

apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
  name: allow-payment-external
  namespace: production
spec:
  podSelector:
    matchLabels:
      app: payment
  policyTypes:
    - Egress
  egress:
    - to:
        - ipBlock:
            cidr: 185.10.20.0/24
      ports:
        - protocol: TCP
          port: 443
    - to:
        - namespaceSelector:
            matchLabels:
              kubernetes.io/metadata.name: kube-system
      ports:
        - protocol: UDP
          port: 53

Поле ipBlock позволяет указывать конкретные CIDR-диапазоны. С помощью поля except внутри ipBlock можно исключить подсети - например, разрешить весь внешний интернет, но запретить диапазон внутренней сети.

Calico и другие CNI: кто реально применяет правила

NetworkPolicy - это API-объект Kubernetes, но сам по себе кластер его не исполняет. Применением занимается CNI-плагин (Container Network Interface). Не все плагины одинаково поддерживают политики.

CNI-плагинПоддержка NetworkPolicyРасширенные возможности
CalicoПолнаяСобственные CRD: GlobalNetworkPolicy, HostEndpoint, layer 7
CiliumПолнаяeBPF, политики L7 по HTTP-методам и путям
Weave NetПолнаяБазовый набор без расширений
FlannelНет (без Canal)Требует Canal для применения политик
kube-routerПолнаяBGP, поддержка стандартного API

Calico - самый распространенный выбор для production. Помимо стандартного API, он предоставляет собственные ресурсы GlobalNetworkPolicy и NetworkPolicy (в apiVersion projectcalico.org/v3), которые позволяют писать политики на уровне всего кластера, а не отдельного namespace. Это удобно для глобальных запретов - например, заблокировать доступ к метаданным облачного провайдера (169.254.169.254) для всех подов сразу.

Если вы не уверены, какой CNI установлен в вашем кластере, проверьте поды в namespace kube-system:

kubectl get pods -n kube-system | grep -E 'calico|cilium|weave|flannel'

Типичные ошибки конфигурации

Большинство проблем с сетевыми политиками возникают не из-за непонимания концепции, а из-за нюансов YAML и поведения по умолчанию.

Ошибка 1: AND vs OR в секции from

Это самая частая точка путаницы. Посмотрите на разницу:

# Вариант А: AND - только prometheus из namespace monitoring
from:
  - namespaceSelector:
      matchLabels:
        app: monitoring
    podSelector:
      matchLabels:
        app: prometheus

# Вариант Б: OR - любой под из monitoring ИЛИ любой prometheus из любого namespace
from:
  - namespaceSelector:
      matchLabels:
        app: monitoring
  - podSelector:
      matchLabels:
        app: prometheus

Один дефис - принципиальная разница в безопасности.

Ошибка 2: забытый DNS после deny-all

После применения default-deny-all сервисы начинают падать с ошибками резолвинга. Команда тратит время на диагностику приложения, хотя проблема - в отсутствии разрешения для UDP/TCP 53 на kube-system. Всегда добавляйте DNS-правило сразу после запрещающей политики.

Ошибка 3: политика применена, но CNI её игнорирует

Если кластер использует Flannel без Canal, манифест NetworkPolicy будет принят API-сервером (ресурс создастся без ошибок), но никакой изоляции не произойдет. Проверяйте поддержку CNI до написания политик.

Ошибка 4: метки не совпадают

Политика ссылается на метку app: postgres, а реальный под имеет метку app: postgresql или component: database. Соединение заблокировано, под не получает трафик. Всегда проверяйте метки через kubectl get pods --show-labels.

Ошибка 5: не указан policyTypes

Если policyTypes не указан, Kubernetes сам определяет, какие направления контролирует политика - исходя из наличия секций ingress и egress. Это поведение неочевидно и может привести к тому, что egress останется открытым, когда вы ожидали его заблокировать. Указывайте policyTypes явно всегда.

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

Применить YAML и увидеть сообщение «configured» - не значит, что изоляция работает. Нужна проверка реальных соединений.

Самый простой способ - запустить временный под с утилитами и проверить доступность:

kubectl run test-pod --image=nicolaka/netshoot -it --rm -- /bin/bash

# Внутри пода:
curl -v http://postgres-service:5432
nc -zv redis-service 6379
curl -v http://api-service:8080/health

Если политика работает правильно, соединения, которые должны быть заблокированы, будут зависать или получать Connection refused / timeout. Соединения, которые разрешены, должны отвечать нормально.

Для Calico есть дополнительный инструмент - calicoctl. Он позволяет посмотреть активные политики и их статус:

calicoctl get networkpolicy --all-namespaces -o wide

Cilium предоставляет собственную CLI для диагностики:

cilium policy get
cilium endpoint list

Также полезно включить логирование заблокированных соединений. В Calico это делается через аннотацию log в правиле политики - тогда отброшенные пакеты попадают в системный лог ноды, и вы видите, что именно блокируется.

Важно: проверяйте политики после каждого изменения меток подов или namespace. Ротация подов при деплое не влияет на политики, но переименование меток или изменение namespace сразу меняет то, на какие поды распространяется правило.

Чеклист: безопасная настройка сетевых политик

  1. Убедитесь, что CNI-плагин поддерживает NetworkPolicy (Calico, Cilium, Weave).
  2. Добавьте метки ко всем namespace: без меток namespaceSelector не работает.
  3. Проверьте метки всех целевых подов через kubectl get pods --show-labels.
  4. Начните с default-deny-all для namespace и сразу добавьте разрешение для DNS.
  5. Всегда указывайте policyTypes явно, не полагайтесь на автоопределение.
  6. Проверяйте AND/OR логику в секциях from и to - один дефис меняет смысл.
  7. После применения политики проверяйте соединения реальными запросами из тестового пода.
  8. Для глобальных запретов (например, блокировка cloud metadata endpoint) используйте GlobalNetworkPolicy в Calico.
  9. Включите логирование отброшенных пакетов на период первичной настройки.
  10. Задокументируйте, какой сервис к чему обращается, до написания политик - иначе легко заблокировать нужное.

Когда NetworkPolicy не решает задачу

Стандартный API покрывает большинство задач L3/L4-изоляции, но у него есть ограничения. Он не умеет фильтровать трафик по HTTP-методам, URL-путям или заголовкам - для этого нужны либо расширения CNI (Cilium умеет политики L7), либо service mesh (Istio, Linkerd с mTLS и authorization policy).

Также стандартный NetworkPolicy не применяется к трафику между нодами на уровне хоста - только к трафику между подами. Если вам нужна изоляция на уровне ноды, Calico предоставляет ресурс HostEndpoint.

Наконец, NetworkPolicy не заменяет аутентификацию между сервисами. Если злоумышленник скомпрометировал под, который легитимно имеет доступ к базе, сетевая изоляция не поможет. Для этого нужны mTLS и application-level authN.

Пример из практики: команда настроила жесткий deny-all и описала все нужные политики. Всё работало. Потом добавили новый сервис для сбора метрик и забыли добавить egress-правило, разрешающее ему обращаться к Prometheus. Сервис падал с timeout, команда несколько часов смотрела в логи приложения. Решение: всегда проверяйте, не стал ли новый сервис жертвой существующего deny-all.

FAQ

Что такое NetworkPolicy простыми словами?

Это правило, которое описывает, какие поды могут отправлять трафик к данному поду и куда этот под может обращаться сам. Без таких правил все поды в кластере общаются между собой свободно - как компьютеры в одной локальной сети без firewall.

Если я применил NetworkPolicy, нужно ли что-то еще настраивать в CNI?

Если CNI поддерживает NetworkPolicy (Calico, Cilium), он автоматически подхватывает новые объекты и применяет правила. Дополнительной настройки не требуется. Главное - убедиться, что плагин с такой поддержкой вообще установлен до создания первой политики.

Чем Calico NetworkPolicy отличается от стандартного API?

Стандартный ресурс networking.k8s.io/v1/NetworkPolicy работает в рамках одного namespace и поддерживает только L3/L4-фильтрацию. Calico добавляет собственные CRD: GlobalNetworkPolicy для правил на уровне всего кластера, поддержку приоритетов между политиками и более гибкие селекторы. Оба типа можно использовать одновременно.

Можно ли написать одну политику, которая разрешает и ingress, и egress?

Да. В одном манифесте можно указать оба направления в policyTypes и описать правила для каждого в соответствующих секциях. Но часто удобнее разделять их на отдельные объекты - так проще читать и отлаживать.

Как проверить, что политика применилась и работает?

Запустите тестовый под (например, nicolaka/netshoot) и попробуйте установить соединение к целевому сервису через curl, nc или telnet. Если соединение зависает или сбрасывается там, где должно быть заблокировано, - политика работает. Также можно использовать calicoctl или Cilium CLI для просмотра активных правил.

Политика применена, но трафик всё равно проходит. Почему?

Чаще всего причина одна из трёх: CNI не поддерживает NetworkPolicy (например, чистый Flannel), метки на подах или namespace не совпадают с теми, что указаны в политике, или в секции from/to использована OR-логика вместо AND из-за лишнего дефиса в YAML.

Нужны ли сетевые политики в dev-окружении?

В dev они не обязательны, но если вы хотите воспроизводить production-поведение или тестировать сами политики, имеет смысл их добавить. Главное - не забыть об этом при переносе в production: кластер без политик в dev не даст никакой уверенности, что в prod всё будет изолировано.

Итог

Сетевые политики - это не сложная концепция, но дьявол в деталях: пустой selector, один лишний дефис в YAML или забытый DNS могут сделать настройку либо бесполезной, либо сломавшей весь namespace. Самый надежный подход - начинать с deny-all, сразу разрешать DNS, а дальше добавлять точечные правила для каждого направления трафика.

Проверяйте политики реальными запросами, а не только смотрите на созданные объекты в кластере. И убедитесь заранее, что ваш CNI вообще умеет применять эти правила - иначе все YAML-манифесты просто лежат мертвым грузом, создавая ложное ощущение безопасности.