По умолчанию сеть в Kubernetes полностью открытая: любой Pod может обращаться к любому другому Pod в кластере, независимо от namespace. Это flat network model — удобно для разработки, но неприемлемо для продакшена с несколькими командами или микросервисами, где сервисы не должны видеть друг друга без явного разрешения.
NetworkPolicy — механизм для ограничения этого трафика на уровне L3/L4 (IP-адреса, порты, протоколы). Работает на уровне Pod, а не сервисов: правила применяются к конкретным Pod через label-селекторы.
[source: kubernetes.io/docs/concepts/services-networking/network-policies/]
Зависимость от CNI
NetworkPolicy — объект Kubernetes API, но его реализацию берёт на себя CNI-плагин. Это важно: если CNI не поддерживает NetworkPolicy — объекты создаются без ошибок, правила синтаксически корректны, но в реальности трафик не блокируется. Кластер молча игнорирует политики.
Перед применением NetworkPolicy убедись, что CNI её поддерживает. В кластерах на чистом Flannel NetworkPolicy создаётся без ошибки, но ничего не блокирует — самый опасный вариант, потому что создаётся иллюзия безопасности.
Проверить поддержку:
# Посмотреть CNI-плагин
kubectl get pods -n kube-system | grep -E 'calico|cilium|flannel|weave'
# Для Cilium — проверить статус
kubectl -n kube-system exec ds/cilium -- cilium status
Логика применения
Три ключевых правила, которые нужно понять один раз:
Правило 1: Если ни одна NetworkPolicy не выбирает Pod — весь трафик к нему и от него разрешён (default allow). Это поведение "по умолчанию" в Kubernetes.
Правило 2: Как только хотя бы одна NetworkPolicy выбирает Pod — для каждого указанного policyTypes (Ingress/Egress) всё запрещено, кроме явно разрешённого в правилах этой и других политик.
Правило 3: Политики аддитивны. Если несколько NetworkPolicy выбирают один Pod — результирующий набор правил равен объединению всех разрешений. Нельзя одной политикой "отменить" разрешение из другой.
Практический вывод: нельзя запретить что-то явно. Можно только не разрешать. Для полной изоляции нужно сначала применить deny-all, а потом явно разрешать нужные пути.
Структура NetworkPolicy
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: example-policy
namespace: app
spec:
podSelector: # к каким Pod применяется эта политика
matchLabels:
role: backend
policyTypes: # какие направления регулируем (всегда указывай явно)
- Ingress
- Egress
ingress: # правила входящего трафика
- from:
- podSelector:
matchLabels:
role: frontend
ports:
- protocol: TCP
port: 8080
egress: # правила исходящего трафика
- to:
- podSelector:
matchLabels:
role: database
ports:
- protocol: TCP
port: 5432
Три способа указать источник или назначение трафика:
Поле policyTypes лучше всегда указывать явно. Без него поведение определяется наличием секций ingress/egress: если есть только ingress — только Ingress регулируется, Egress остаётся открытым. Явное указание устраняет неоднозначность.
Default Deny All Ingress
Первый шаг при создании изолированного namespace — запретить весь входящий трафик:
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: default-deny-ingress
namespace: app
spec:
podSelector: {} # пустой selector = все Pod в namespace
policyTypes:
- Ingress
# секция ingress отсутствует = ничего не разрешено
После применения все Pod в namespace app недоступны по сети со стороны других Pod и внешних источников. Egress при этом остаётся открытым — Pod могут инициировать исходящие соединения.
Default Deny All Egress
Аналогично для исходящего трафика:
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: default-deny-egress
namespace: app
spec:
podSelector: {}
policyTypes:
- Egress
Критичный момент: после deny egress Pod не сможет обращаться к DNS (CoreDNS работает на порту 53 UDP/TCP). Это ломает name resolution — curl http://my-service перестаёт работать, хотя IP-подключение разрешено. Всегда добавляй явное разрешение для DNS сразу:
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: allow-dns-egress
namespace: app
spec:
podSelector: {}
policyTypes:
- Egress
egress:
- ports:
- protocol: UDP
port: 53
- protocol: TCP
port: 53
Обрати внимание: здесь нет секции to — разрешён egress на порт 53 к любому адресу. Это нужно для резолвинга как внутренних сервисов, так и внешних DNS.
[source: kubernetes.io/docs/concepts/services-networking/network-policies/#default-deny-all-egress-traffic]
Разрешить трафик между конкретными Pod
Frontend к backend на порт 8080:
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: allow-frontend-to-backend
namespace: app
spec:
podSelector:
matchLabels:
role: backend
policyTypes:
- Ingress
ingress:
- from:
- podSelector:
matchLabels:
role: frontend
ports:
- protocol: TCP
port: 8080
Политика применяется к Pod с label role: backend и разрешает входящий трафик только от Pod с label role: frontend на порт 8080. Всё остальное запрещено для этих Pod (потому что политика теперь их выбирает).
Разрешить трафик из другого namespace
Сценарий: monitoring-агент в namespace monitoring должен собирать метрики с Pod в namespace app.
Начиная с Kubernetes 1.22, все namespace автоматически получают label kubernetes.io/metadata.name: <name>. Используй его — не нужно вручную навешивать label на namespace:
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: allow-from-monitoring
namespace: app
spec:
podSelector:
matchLabels:
role: backend
policyTypes:
- Ingress
ingress:
- from:
- namespaceSelector:
matchLabels:
kubernetes.io/metadata.name: monitoring
ports:
- protocol: TCP
port: 9090
Если используешь namespaceSelector без kubernetes.io/metadata.name, нужно вручную поставить label на namespace:
kubectl label namespace monitoring env=monitoring
AND vs OR в правилах from/to
Это самая частая ошибка при написании NetworkPolicy. Разница определяется положением селекторов в YAML — в одном элементе массива или в разных.
AND — оба условия должны совпасть одновременно. Pod с label role: frontend и в namespace с label env: prod. Оба селектора в одном элементе (один дефис):
ingress:
- from:
- podSelector: # один элемент массива
matchLabels:
role: frontend
namespaceSelector: # в том же элементе (нет дефиса перед namespaceSelector)
matchLabels:
env: prod
OR — любое из условий подходит. Любой Pod с label role: frontend или любой Pod из namespace с label env: prod. Каждый селектор — отдельный элемент (свой дефис):
ingress:
- from:
- podSelector: # первый элемент
matchLabels:
role: frontend
- namespaceSelector: # второй элемент (свой дефис)
matchLabels:
env: prod
Разница в одном символе — дефис перед namespaceSelector. Смысл противоположный. Ошибку сложно заметить визуально, но последствия критичны — можно случайно разрешить трафик от всего namespace вместо конкретных Pod.
[source: kubernetes.io/docs/concepts/services-networking/network-policies/#behavior-of-to-and-from-selectors]
Комбинированная политика: полная изоляция backend
Backend принимает только от frontend, отправляет только к database и DNS:
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: backend-policy
namespace: app
spec:
podSelector:
matchLabels:
role: backend
policyTypes:
- Ingress
- Egress
ingress:
- from:
- podSelector:
matchLabels:
role: frontend
ports:
- protocol: TCP
port: 8080
egress:
- to:
- podSelector:
matchLabels:
role: database
ports:
- protocol: TCP
port: 5432
- ports: # DNS — без ограничения по to
- protocol: UDP
port: 53
- protocol: TCP
port: 53
Внешний трафик через ipBlock
Разрешить egress к конкретному внешнему API:
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: allow-external-api
namespace: app
spec:
podSelector:
matchLabels:
role: backend
policyTypes:
- Egress
egress:
- to:
- ipBlock:
cidr: 203.0.113.0/24
except:
- 203.0.113.10/32 # исключить конкретный IP из диапазона
ports:
- protocol: TCP
port: 443
ipBlock работает с IP-адресами вне кластера. Для Pod-IP внутри кластера используй podSelector/namespaceSelector — ipBlock для внутреннего трафика ненадёжен, потому что IP Pod могут меняться.
Отладка NetworkPolicy
Инструментов для отладки NetworkPolicy в стандартном kubectl немного. Практические подходы:
# Запустить временный Pod-клиент и проверить соединение вручную
kubectl run nettest --image=busybox:1.36 -n app --rm -it \
--labels="role=frontend" -- sh
# Внутри пода:
# wget -qO- http://backend-service:8080/health
# nc -zv backend-service 8080
# Посмотреть применённые NetworkPolicy в namespace
kubectl get networkpolicies -n app
# Детально посмотреть конкретную политику
kubectl describe networkpolicy backend-policy -n app
Для Cilium доступна более богатая диагностика:
# Показать, почему трафик заблокирован/разрешён
kubectl -n kube-system exec ds/cilium -- \
cilium policy trace --src-k8s-pod app:frontend-pod --dst-k8s-pod app:backend-pod
Когда использовать
Изоляция namespace по умолчанию — стандартная практика для prod-кластеров с несколькими командами. Применяй default-deny-ingress и default-deny-egress в каждом namespace, затем явно открывай нужные пути. Это требует дисциплины, но даёт контроль.
Микросегментация — явные политики между микросервисами. Frontend → backend → database, но не frontend → database напрямую. Снижает радиус поражения при компрометации одного компонента.
Изоляция между командами — разные команды в разных namespace не должны видеть друг друга по сети. NetworkPolicy + RBAC дают полную изоляцию.
Не нужна, если кластер имеет один namespace и одно приложение, или это dev/staging кластер с минимальными требованиями к безопасности, или CNI не поддерживает NetworkPolicy.
Типичные ошибки
Flannel без Calico — правила молча не работают. Самая опасная ошибка: NetworkPolicy создана, команда думает, что трафик ограничен, а на деле ничего не изменилось. Проверяй CNI перед применением в новом кластере.
Забыть разрешить DNS после deny egress. После default-deny-egress сервисы не могут резолвить имена. Всегда добавляй allow для порта 53 UDP/TCP. Симптом: curl http://service-name зависает, curl http://10.96.0.1:8080 (прямой IP) работает.
Перепутать AND/OR (разница в дефисе YAML). Проверяй написанное правило вручную: запусти Pod с нужными label и без них, убедись, что трафик блокируется/разрешается корректно.
Не указать policyTypes явно. Без policyTypes поведение неочевидно. Если есть только ingress-секция — Egress остаётся полностью открытым. Указывай policyTypes всегда.
Забыть, что podSelector в from/to выбирает Pod в namespace самой NetworkPolicy. Если хочешь разрешить Pod из другого namespace — нужен namespaceSelector, иначе podSelector игнорирует чужие namespace.
ipBlock для внутрикластерного трафика. IP Pod нестабильны и меняются при перезапуске. Для трафика между Pod внутри кластера используй только podSelector/namespaceSelector.
Альтернативы
CiliumNetworkPolicy — расширение от Cilium, добавляет L7-правила на уровне HTTP, gRPC, Kafka. Позволяет написать "разрешить только GET /api/v1/health от frontend" вместо "разрешить TCP 8080". Требует Cilium как CNI. Работает рядом со стандартным NetworkPolicy.
Service Mesh (Istio, Linkerd) — mTLS между всеми сервисами и политики на уровне L7 через AuthorizationPolicy (Istio). Работает через sidecar-прокси, независимо от CNI. Более мощно: можно ограничивать по HTTP-методу, пути, JWT-claims. Значительно сложнее в эксплуатации и добавляет latency.
Egress Gateway — паттерн для контроля исходящего трафика кластера. Весь egress проходит через единую точку (Istio Egress Gateway или отдельный proxy). Упрощает аудит, позволяет применять единую политику для внешних зависимостей.
PodSecurity Standards — не прямая альтернатива, но дополняет сетевую изоляцию: запрещает hostNetwork: true (Pod с hostNetwork обходит NetworkPolicy), hostPort, привилегированные контейнеры.
[source: kubernetes.io/docs/concepts/services-networking/network-policies/#behavior-of-to-and-from-selectors]