Как CNI реализует NetworkPolicy
Знать NetworkPolicy API недостаточно — на собеседованиях, в production и иногда даже на CKAD задают вопросы вида «policy применилась, но Pod-Pod трафик не блокируется, что не так». Чтобы понимать такие баги, нужно знать как CNI plugins реально применяют policy на уровне Linux. Они все реализуют один и тот же K8s API, но через разные dataplanes.
eBPF: безопасная инспекция kernel в runtime без модулей
Архитектура enforcement: общая схема
Любой CNI с поддержкой NetworkPolicy состоит из двух уровней:
Control plane — Go-агент, который через informer от API server получает все NetworkPolicies и labels Pods, считает, какие Pods должны быть isolated и какие источники разрешены. Dataplane — программа на уровне ядра Linux (или близко к нему), которая реально режет пакеты.
Calico: iptables и filter table
Calico — самый распространённый CNI с поддержкой NetworkPolicy. Дефолтный mode — iptables. Каждый node имеет calico-node DaemonSet, который:
- Через informer подписан на NetworkPolicy.
- Для каждого Pod на своём node генерирует набор iptables правил.
- Размещает их в filter table, в специальных chains (
cali-*), которые подключены к стандартным chainsFORWARD,INPUT,OUTPUT.
Когда пакет идёт от Pod A к Pod B через bridge/veth, он проходит через FORWARD chain, оттуда переходит в cali-fw-* chains для каждого Pod и проверяется на match. Если ни одного match — DROP.
# На node с Calico:
sudo iptables -L -n -t filter | grep cali | head -20
# Видны chains вида cali-fw-<endpoint-id>
Calico также поддерживает eBPF dataplane (опционально включается). В eBPF mode он размещает программы на TC ingress hook каждого veth и фильтрует там — быстрее, чем iptables, особенно при большом количестве правил.
iptables работает линейно: чем больше правил, тем дольше каждый пакет идёт через chain. В большом кластере с сотнями NetworkPolicies это становится заметным накладным расходом. eBPF и OVS обходят это через программирование, а не линейные lookup-ы.
Cilium: eBPF и L7 policy
Cilium с самого начала строился на eBPF — Berkeley Packet Filter, виртуальной машине в ядре Linux, через которую можно прикрепить программы к различным hook-ам: TC, XDP, socket, kprobe.
В Cilium на каждый Pod (точнее, его veth) прикреплены eBPF программы, которые:
- При входящем пакете смотрят source IP, lookup-ят его в eBPF map «identity → labels», проверяют по правилам policy, ACCEPT или DROP.
- Identity Pod-а вычисляется из labels (а не из IP) — это важно, потому что Pod IP эфемерные, но labels стабильные. Cilium хранит mapping «identity → Pods» отдельно.
Помимо стандартного NetworkPolicy, Cilium поддерживает свой CiliumNetworkPolicy CRD, который расширяется на L7:
apiVersion: cilium.io/v2
kind: CiliumNetworkPolicy
metadata:
name: l7-http-policy
spec:
endpointSelector:
matchLabels:
app: api
ingress:
- fromEndpoints:
- matchLabels:
app: web
toPorts:
- ports:
- port: "8080"
protocol: TCP
rules:
http:
- method: GET
path: "/api/v1/users"
«Разрешить web → api на 8080 ТОЛЬКО для GET /api/v1/users, остальные HTTP-методы и paths — DROP». Это L7 policy — стандартный K8s NetworkPolicy так не умеет.
L7 реализуется через Envoy proxy, который Cilium встраивает в network path трафика, плюс eBPF redirect-ы.
Antrea: Open vSwitch flows
Antrea — CNI, разработанный VMware на основе Open vSwitch (OVS). На каждом node работает antrea-agent, который:
- Создаёт OVS bridge.
- При создании Pod добавляет его veth как OVS port.
- Для NetworkPolicy генерирует OVS flows в специальных таблицах bridge.
OVS — это более «программируемый» switch, чем Linux bridge. Flows организованы в pipeline (несколько таблиц последовательно), и можно довольно эффективно фильтровать трафик.
Antrea также поддерживает AntreaNetworkPolicy CRD с L7 и приоритетами правил (deny over allow).
Weave Net: iptables, но с подводными камнями
Weave Net использует iptables аналогично Calico, но исторически:
- Поддержка namespaceSelector была неполной до версии 2.4.
- Не все edge cases сочетания selectors поддерживались.
- Производительность iptables как у Calico (линейная).
На CKAD vanilla Weave обычно не встречается, но в legacy кластерах — да.
Killer момент: bypass policy между Pods на одном node
Subtle bug, который встречается в нестандартных setups: трафик между Pods на одном node может идти через Linux bridge без прохождения через FORWARD chain, если bridge настроен без hairpin mode и без br_netfilter модуля.
Что происходит:
- Pod A и Pod B на одном node.
- A → B: пакет идёт через veth-A → bridge → veth-B.
- Если bridge не отправляет пакет через iptables FORWARD chain — Calico iptables правила его не видят. Policy не применяется.
Это решается включением br_netfilter:
sudo modprobe br_netfilter
sudo sysctl -w net.bridge.bridge-nf-call-iptables=1
Стандартные kubelet setups (kubeadm, managed K8s) уже включают это. Но если кто-то сделал ручную инсталляцию и забыл — policy «по необъяснимой причине» не работает между Pods на одной машине. Calico проверяет это при старте и warning-ит в логах.
Cilium с eBPF dataplane обходит эту проблему — он не зависит от br_netfilter, программы прикреплены прямо к TC hook veth каждого Pod. Это одна из причин, почему Cilium считается «надёжнее» в edge cases.
Limitations: L3/L4 only
Стандартный Kubernetes NetworkPolicy работает только на L3/L4:
Это значит, что задача «разрешить GET /healthz но запретить /admin» не решается стандартным NetworkPolicy. Нужны:
- CiliumNetworkPolicy с http rules (если установлен Cilium);
- Istio AuthorizationPolicy с request.* атрибутами (если установлен service mesh);
- Open Policy Agent (OPA) как Admission Controller (но это про создание API объектов, а не runtime трафик).
На CKAD вопросы про L7 редкие, но осознавать ограничение — обязательно.
Egress через NAT в Internet
Когда Pod выходит наружу (в Internet), пакет проходит через node IP — обычно с MASQUERADE (SNAT) на исходящем интерфейсе. То есть source IP пакета меняется с PodIP на node IP. Это делает kube-proxy или CNI plugin.
Важные следствия для NetworkPolicy:
- Внутренний egress ipBlock matches на PodIP target Pod, не на ClusterIP (мы это упоминали в прошлом уроке). Это потому что DNAT происходит до policy enforcement.
- Egress в Internet — нужен ipBlock с
0.0.0.0/0(или конкретные CIDR). namespaceSelector/podSelector бесполезны, потому что внешние сервисы — не Pods. - Внешние сервисы видят node IP, не Pod IP. Поэтому если ваш ingress firewall в облаке хочет allowlist Pod IP — это не сработает, нужно whitelist node IPs или фиксированный SNAT (Egress Gateway в Cilium, EgressNetworkPolicy в OpenShift).
Testing NetworkPolicy: инструменты
Самый простой способ протестировать policy — поднять test Pod и сделать nc или wget:
# Запускаем test Pod
kubectl run test --rm -it --image=busybox --restart=Never -- sh
# Внутри Pod
nc -zv 10.0.5.10 5432 # TCP check на IP
nc -zv postgres 5432 # через DNS
wget -O- --timeout=3 http://api:8080/healthz
Если policy блокирует — nc -zv зависнет до timeout. Если разрешает — nc: open ... succeeded.
Полезные инструменты:
- calicoctl —
calicoctl get networkpolicy, debugging Calico-specific. - cilium connectivity test — комплексный тест matrix Pod↔Pod в кластере с Cilium.
- np-viewer / kubectl-np-viewer plugin — визуализатор effective policy для каждого Pod.
- netshoot image (
nicolaka/netshoot) — Pod с pre-installed tools (nc, dig, tcpdump, wireshark).
kubectl run netshoot --rm -it --image=nicolaka/netshoot --restart=Never -- bash
# Внутри: dig postgres, tcpdump -i eth0, mtr 10.0.5.10
Killer момент: kubectl exec НЕ через policy
kubectl exec -it pod -- nc target 80 — это не «pod connects to target». Это:
- ваш CLI отправляет HTTP request на API server;
- API server открывает поток к kubelet на узле, где Pod;
- kubelet через CRI открывает exec-stream в контейнере;
- в этом stream выполняется
ncот лица процессов Pod.
Когда nc делает connect(), уже изнутри Pod-а, сетевой трафик идёт от Pod к target. Этот трафик проходит через NetworkPolicy enforcement, как и любой другой Pod-Pod трафик. Так что тестирование через kubectl exec валидно.
Но: сам по себе exec-канал (ваш SSH-подобный stream) идёт через API server, не через Pod-сеть. Если бы NetworkPolicy «запретила доступ к Pod», это не помешало бы kubectl exec, потому что API server подключается к kubelet, а kubelet — к контейнеру через CRI socket, а не сеть. Это часто путает: «policy блокирует ingress, но я могу зайти exec-ом» — да, потому что это разные слои.
Это объясняет, почему kubectl exec всегда работает даже на «полностью изолированный» Pod. Policy не контролирует control-plane operations. Если хотите ограничить exec — это RBAC и authorization, не NetworkPolicy.