Именно такая картина встречается примерно на каждом втором аудите. Команда разворачивает 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 вообще.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"
Практический смысл: у вас есть
/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
Изоляцию нужно начинать с запрета всего - потом точечно открывать. Иначе вы всегда в ситуации «политика есть, но список разрешений неполный, что-то проскакивает».
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 первым делом - иначе отладка начнётся со звонков о том, что «всё упало и непонятно почему».