Kubernetes для самых маленьких. Часть 20: Как работает NetworkPolicy?

Все части серии «Kubernetes для самых маленьких»

В части 14 мы видели главное правило сетевой модели Kubernetes: любой под может обратиться к любому другому поду по его адресу, без NAT. Потом к этому добавились Service, DNS, внешний адрес от MetalLB, шлюз на Traefik и сертификаты из прошлой части — трафик снаружи пришёл в порядок. Но внутри кластера по-прежнему нет ни одной стены: под из чужого неймспейса может открыть соединение с базой данных, взломанный фронтенд — пойти куда угодно в локальную сеть. Сегодня разберём NetworkPolicy — стандартный объект Kubernetes для ограничения трафика между подами, — затем политики самого Calico, которые умеют больше, и посмотрим, во что всё это превращается на узле.


По умолчанию разрешено всё

Пока в неймспейсе нет ни одной политики, его поды не изолированы: принимают соединения от кого угодно и сами идут куда угодно — к другим подам, в сеть узлов, в интернет через NAT узла. Так задумано: кластер должен работать сразу после установки, без настройки межсетевого экрана.

Изоляция включается политикой, которая выбирает под. Как только под попал под селектор хотя бы одной NetworkPolicy с типом Ingress, он принимает только то, что явно разрешено одной из выбирающих его политик. То же отдельно для Egress. Направления независимы: политика только на вход не трогает исходящий трафик пода.

Отсюда три свойства модели, которые надо запомнить:

  • Политика выбирает поды, к которым применяется, а не трафик. Поле spec.podSelector — «к кому», правила внутри — «что разрешено».
  • Политики аддитивны. Разрешения всех политик, выбирающих под, складываются. Порядка нет, приоритетов нет, и запрещающих правил в стандартном API нет вообще: запрет — это отсутствие разрешения у изолированного пода. Нельзя написать «всем можно, кроме этого адреса».
  • Политики учитывают состояние соединений. Если разрешено соединение от A к B, ответные пакеты B к A пропускаются без отдельного правила — сетевой плагин опирается на conntrack, таблицу отслеживания соединений ядра.

И три исключения, прописанные в документации Kubernetes:

  • трафик между подом и его узлом разрешён всегда: иначе kubelet не смог бы выполнять пробы liveness и readiness;
  • под не может закрыть доступ к самому себе;
  • поведение для подов с hostNetwork: true не определено, и большинство плагинов, Calico в том числе, их просто не видят: такой под живёт в сетевом пространстве имён узла, и его трафик выглядит как трафик узла.

NetworkPolicy реализует сетевой плагин (CNI), а не сам Kubernetes: API-сервер примет объект в любом кластере, но если плагин политик не умеет (как, например, flannel — простой CNI-плагин, который даёт только связность между подами), ничего не произойдёт и никто об этом не предупредит. Calico, который стоит у нас с части 4, умеет.


Объект NetworkPolicy

apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
  name: hello-from-traefik
  namespace: demo
spec:
  podSelector:
    matchLabels:
      app.kubernetes.io/name: hello
  policyTypes:
    - Ingress
  ingress:
    - from:
        - namespaceSelector:
            matchLabels:
              kubernetes.io/metadata.name: traefik
          podSelector:
            matchLabels:
              app.kubernetes.io/name: traefik
      ports:
        - protocol: TCP
          port: 8080

podSelector — к каким подам своего неймспейса применяется политика. Пустой селектор {} — ко всем подам неймспейса. Политика никогда не выбирает поды другого неймспейса.

policyTypes — Ingress, Egress или оба. Если поле опустить, Ingress подразумевается всегда, а Egress — только если в политике есть секция egress. Лучше писать явно: политика «запретить весь исходящий» как раз выглядит как policyTypes: [Egress] без секции egress, и без явного поля её смысл потеряется.

ingress[].from и egress[].to — список участников (peers), каждый одного из видов:

  • podSelector — поды своего неймспейса с такими метками;
  • namespaceSelector — все поды неймспейсов с такими метками. По имени неймспейс выбирают меткой kubernetes.io/metadata.name, которую control plane ставит на каждый неймспейс автоматически и менять не даёт;
  • namespaceSelector и podSelector в одном элементе — поды с такими метками в неймспейсах с такими метками;
  • ipBlock — диапазон адресов (cidr) с исключениями (except).

Самая частая ошибка — лишний дефис. Сравните:

# один участник: поды traefik В неймспейсе traefik (И)
- from:
    - namespaceSelector: {matchLabels: {kubernetes.io/metadata.name: traefik}}
      podSelector: {matchLabels: {app.kubernetes.io/name: traefik}}

# два участника: ВСЕ поды неймспейса traefik ИЛИ поды traefik своего неймспейса
- from:
    - namespaceSelector: {matchLabels: {kubernetes.io/metadata.name: traefik}}
    - podSelector: {matchLabels: {app.kubernetes.io/name: traefik}}

Разница в один символ, и вторая запись открывает больше, чем кажется.

ports — протокол (TCP, UDP или SCTP — транспортный протокол с несколькими потоками в одном соединении, который встречается в основном в телеком-оборудовании), порт числом или именем порта контейнера, диапазон через endPort (только числами). Список участников и список портов внутри одного правила связаны через И: «от этих участников на эти порты». Правило без from разрешает от всех, правило без ports — на все порты.

Два тонких места:

  • Порты и адреса — после DNAT. Документация Kubernetes оставляет на усмотрение плагина, видит ли политика адрес до или после подмены, а Calico проверяет пакет уже после того, как kube-proxy заменил ClusterIP сервиса на адрес пода. Поэтому писать надо порт контейнера (8080), а не порт сервиса (80), и выбирать поды метками, а не адресом ClusterIP: ipBlock с адресом из сети сервисов 10.96.0.0/12 не сработает. Ниже, в разделе «Смотрим руками», это видно на сервисе kubernetes.
  • ipBlock — для адресов вне кластера. Адреса подов меняются при каждом перезапуске, для них есть селекторы. И помните про NAT: под, выходящий наружу, у нас получает адрес узла (natOutgoing: true в Calico), поэтому внешний сервер видит не под, а узел, а политика на стороне кластера, наоборот, видит настоящий адрес пода до NAT.

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


Default deny и что после него ломается

Правильная отправная точка для неймспейса с приложениями — запретить всё и открыть нужное. Запрет — это политика, которая выбирает все поды и ничего не разрешает:

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

Сразу после неё в demo перестаёт работать всё, и первое — DNS. Под ищет postgres.demo.svc.cluster.local, отправляет запрос на 10.96.0.10:53, kube-proxy переписывает адрес на под CoreDNS в kube-system, а исходящий трафик пода запрещён. Приложение видит не «соединение запрещено», а «имя не найдено» или таймаут, и ищут проблему обычно не там. Поэтому вторая политика — всегда разрешение DNS:

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

TCP нужен тоже: DNS переходит на TCP, когда ответ не помещается в UDP-датаграмму. Метка k8s-app: kube-dns стоит на подах CoreDNS в кластере kubeadm по историческим причинам.

Что ещё ломается после default deny и о чём стоит подумать заранее:

Что Почему Чем открыть
входящий трафик от Traefik поды стали изолированы на вход политика from неймспейса traefik
поды обращаются друг к другу внутри demo изоляция действует и внутри неймспейса правила для конкретных пар
приложение ходит к API Kubernetes 10.96.0.1:443 после DNAT — адреса control plane, 192.168.10.17:6443 ipBlock с адресом API-сервера и портом 6443
приложение ходит во внешний мир (обновления, внешние API) исходящий запрещён ipBlock с нужными адресами или отказ от этого вовсе
сбор метрик Prometheus входящий на порт метрик политика from неймспейса мониторинга
пробы kubelet не ломаются: трафик узла к своему поду разрешён всегда —

Набор политик для demo

В demo живут hello (принимает HTTP от Traefik на порту 8080) и postgres — учебная база, к которой ходит только hello:

apiVersion: apps/v1
kind: Deployment
metadata:
  name: postgres
  namespace: demo
  labels:
    app.kubernetes.io/name: postgres
spec:
  replicas: 1
  selector:
    matchLabels:
      app.kubernetes.io/name: postgres
  template:
    metadata:
      labels:
        app.kubernetes.io/name: postgres
    spec:
      containers:
      - name: postgres
        image: postgres:18.6-alpine
        env:
        - name: POSTGRES_HOST_AUTH_METHOD   # только для опыта: вход без пароля
          value: trust
        ports:
        - name: postgres
          containerPort: 5432
        resources:
          requests:
            cpu: 100m
            memory: 128Mi
          limits:
            memory: 256Mi
---
apiVersion: v1
kind: Service
metadata:
  name: postgres
  namespace: demo
spec:
  selector:
    app.kubernetes.io/name: postgres
  ports:
  - name: postgres
    port: 5432
    targetPort: postgres

Настоящую базу без пароля не запускают: пароль кладут в Secret (часть 12), а данные — на том, о котором следующая часть. Здесь база нужна только как сервер на порту 5432.

Политики default-deny и allow-dns уже есть. Добавим ещё три.

Вход в hello от Traefik — политика hello-from-traefik из начала. Traefik, как мы видели в части 18, ходит прямо на адреса подов, а не через ClusterIP, поэтому источник — под Traefik, и селектор по его неймспейсу и меткам работает. Если бы Traefik работал с hostNetwork: true, источником был бы адрес узла, и пришлось бы разрешать адреса узлов через ipBlock.

Выход из hello к базе:

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

И вход в базу от hello:

apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
  name: postgres-from-hello
  namespace: demo
spec:
  podSelector:
    matchLabels:
      app.kubernetes.io/name: postgres
  policyTypes:
    - Ingress
  ingress:
    - from:
        - podSelector:
            matchLabels:
              app.kubernetes.io/name: hello
      ports:
        - protocol: TCP
          port: 5432

При default deny на оба направления соединение должно быть разрешено дважды: на выход у клиента и на вход у сервера. Забыть одну из двух половин — вторая по частоте ошибка после DNS.

Если база не в кластере, а на отдельном сервере, вместо селектора в hello-to-postgres пишется ipBlock с его адресом /32, а вход на сервере ограничивается его собственным файрволом — по адресам узлов, потому что снаружи поды видны с адресами узлов.

Итог по неймспейсу:

                 ┌──────────────── demo (default deny) ────────────────┐
 Traefik ───────►│ hello :8080 ──────────────────► postgres :5432      │
 (ns traefik)    │   │                                                 │
                 │   └──► CoreDNS :53 (kube-system)                    │
                 │                                                     │
                 │ всё остальное: вход и выход запрещены               │
                 └─────────────────────────────────────────────────────┘

Политики неймспейса — ответственность команды приложения: у кого есть права на demo, тот может их менять и удалять. Ограничения, которые должны действовать для всех и всегда, независимо от того, что напишут команды, стандартный API выразить не может: он неймспейсный и только разрешающий. Для этого нужны политики Calico.


Как Calico применяет политики

Calico на каждом узле работает DaemonSet'ом calico-node, и внутри него главный компонент — Felix, агент, который программирует датаплейн узла: маршруты к подам и правила фильтрации. Политики попадают к нему так:

 kubectl apply  ──►  kube-apiserver  ──►  etcd
                          │ watch
                          ▼
                     Typha (calico-system)          раздаёт изменения многим узлам,
                          │                         чтобы не нагружать API-сервер
                          ▼
                     Felix в calico-node на каждом узле
                          │ пересчитывает: какие политики выбирают
                          │ какие поды ЭТОГО узла
                          ▼
         цепочки и наборы адресов в таблице nftables ip calico
         для интерфейса caliXXXXXXXXXXX каждого пода

Typha — промежуточный кэш между API-сервером и агентами Felix: на больших кластерах сотни Felix не держат каждый свой watch к API-серверу, а получают изменения от нескольких реплик Typha.

Felix видит каждый под как WorkloadEndpoint — внутренний объект Calico с адресом пода, его метками, метками неймспейса и именем veth-интерфейса на узле (cali плюс первые 11 символов SHA-1 от строки <неймспейс>.<под>, как мы считали в части 14). Через kubectl эти объекты не видны. Kubernetes NetworkPolicy Felix переводит в свою внутреннюю модель, где она хранится рядом с политиками Calico и отличается видом, и всё вместе превращает в правила nftables. На стенде Calico работает в режиме linuxDataplane: Nftables (часть 4), и все правила лежат в его таблице ip calico — той же, где живёт маскарадинг natOutgoing из части 14. Имена цепочек в ней — с префиксом бывшей таблицы iptables, filter-:

  • две карты вердиктов (verdict map — хеш-таблица nftables, которая по ключу сразу выдаёт действие, мы видели такие у kube-proxy в части 15): filter-cali-to-wl-dispatch и filter-cali-from-wl-dispatch, где ключ — имя интерфейса cali…, а значение — переход в цепочку этого пода; одноимённые цепочки применяют их правилом вида oifname vmap @filter-cali-to-wl-dispatch (для выхода из пода — по входному интерфейсу), а пакет для неизвестного интерфейса отбрасывают;
  • на каждый интерфейс пода две цепочки: filter-cali-tw-<интерфейс> (to workload — трафик в под, то есть ingress) и filter-cali-fw-<интерфейс> (from workload — трафик из пода, egress);
  • на каждую политику — цепочки filter-cali-pi-<вид>/<неймспейс>/<имя> (policy ingress) и filter-cali-po-… (policy egress), где вид — knp для Kubernetes NetworkPolicy, np для NetworkPolicy Calico и gnp для GlobalNetworkPolicy; на них прыгают цепочки интерфейсов выбранных подов. Если у нескольких политик одного tier'а одинаковый селектор, Felix собирает их в цепочку-группу filter-cali-gi-…/filter-cali-go-… и прыгает уже на неё;
  • после политик в цепочке пода стоят профили filter-cali-pri-kns.<неймспейс> и filter-cali-pri-ksa.<неймспейс>.<ServiceAccount> (pro- для egress) — их Calico заводит на каждый неймспейс и ServiceAccount, и они разрешают всё. Именно так устроено «по умолчанию разрешено всё»: если ни одна политика под не выбрала, пакет доходит до профиля и принимается;
  • селекторы в правилах («поды с меткой app.kubernetes.io/name=traefik в неймспейсе traefik») превращаются в наборы адресов (sets) nftables с именами cali40s-<хеш селектора>. Когда появляется новый под с такой меткой, Felix добавляет его адрес в набор, а цепочки не трогает.

Другие датаплейны Calico устроены так же по смыслу: в режиме Iptables те же цепочки (cali-tw-…, cali-pi-…) лежат в таблице filter iptables, а наборы — в ipset; в режиме eBPF (eBPF — механизм ядра Linux для загрузки в ядро проверенных программ обработки пакетов) правила исполняются такими программами на интерфейсах, без netfilter; этот режим разобран в части 5 серии про Kubernetes на BGP.

Благодаря двум цепочкам на интерфейс политика проверяется на узле отправителя (egress) и на узле получателя (ingress) — каждая на своём конце, даже если поды на разных узлах.


Политики Calico

Calico добавляет свои ресурсы в группе projectcalico.org/v3. Оператор Calico 3.32 по умолчанию ставит Calico API server — расширение API Kubernetes, через которое эти ресурсы доступны обычному kubectl (без него понадобилась бы утилита calicoctl). Проверить: kubectl get tigerastatus apiserver. С этим связана ловушка имён: networkpolicies есть и в networking.k8s.io, и в projectcalico.org, а короткое имя cnp занято сразу двумя ресурсами. kubectl get networkpolicies показывает только политики Kubernetes, поэтому политики Calico запрашивают полным именем ресурса:

$ kubectl get networkpolicies -A
No resources found
$ kubectl get networkpolicies.projectcalico.org -A
NAMESPACE       NAME                                   CREATED AT
calico-system   calico-system.apiserver-access         2026-10-24T10:37:10Z
calico-system   calico-system.default-deny             2026-10-24T10:37:10Z
calico-system   calico-system.kube-controller-access   2026-10-24T10:37:10Z
kube-system     calico-system.cluster-dns              2026-10-24T10:36:50Z

Эти четыре политики создал оператор Calico для своих компонентов — о них ниже.

Kubernetes NetworkPolicy Calico NetworkPolicy Calico GlobalNetworkPolicy
Область неймспейс неймспейс весь кластер, в том числе узлы (HostEndpoint)
Действия только разрешить Allow, Deny, Pass, Log Allow, Deny, Pass, Log
Порядок нет, всё складывается поле order поле order
Селекторы matchLabels, matchExpressions выражения: app.kubernetes.io/name == 'hello' && tier != 'test' то же, плюс namespaceSelector
Прочее порты, ipBlock ServiceAccount, ICMP, сети, наборы сетей то же

HostEndpoint — объект Calico для интерфейса самого узла: с ним политики начинают фильтровать трафик узла, а не только подов. Сразу после создания HostEndpoint без политик Calico запрещает интерфейсу всё, кроме служебных исключений (failsafe), поэтому на рабочем кластере их заводят осторожно; сегодня мы их не трогаем.

Действия:

  • Allow — пропустить, дальше не проверять;
  • Deny — отбросить, дальше не проверять;
  • Log — записать пакет в журнал ядра (с префиксом calico-packet, он задаётся полем logPrefix в FelixConfiguration) и продолжить со следующего правила;
  • Pass — прекратить проверку политик текущего уровня (tier) и перейти к следующему.

order — число; политики с меньшим значением проверяются раньше, правила внутри политики — сверху вниз. Первое сработавшее Allow или Deny решает судьбу пакета.

Tiers — уровни политик. Политики группируются в tier'ы, tier'ы проверяются по своему order от меньшего к большему, и внутри каждого — свои политики по их order. Так администраторы безопасности могут держать свои правила на уровне, который проверяется раньше командных и который команды не могут менять (права на tier выдаются через RBAC отдельно). Встроенные уровни в Calico 3.32:

Tier order defaultAction Что в нём
kube-admin 1 000 Pass ClusterNetworkPolicy с tier: Admin (о них ниже)
default 1 000 000 Deny все Kubernetes NetworkPolicy и политики Calico без явного tier
kube-baseline 10 000 000 Pass ClusterNetworkPolicy с tier: Baseline

По документации Calico порядок уровня default изменить нельзя, а у kube-admin и kube-baseline зафиксировано действие Pass. Кроме них, оператор Calico создаёт уровень calico-system с order: 100 для собственных политик — тех четырёх, что мы видели выше. calico-system.default-deny закрывает поды в calico-system, кроме calico-apiserver, две другие политики в calico-system открывают компонентам нужные им пути, а calico-system.cluster-dns в kube-system выбирает поды CoreDNS: разрешает им весь исходящий трафик и входящий от компонентов Calico, а остальной входящий передаёт дальше правилом Pass. Трогать этот уровень не нужно.

Неявный запрет в конце tier'а. Это главное, что надо понимать про уровни. Если хотя бы одна политика tier'а выбрала под, а ни одно правило в этом tier'е не приняло решения, пакет отбрасывается — как у изолированного пода в стандартном API. Tier, в котором ни одна политика под не выбрала, просто пропускается. Чтобы пакет, не подпавший под правила, ушёл проверяться дальше, нужно либо поставить tier'у defaultAction: Pass, либо закончить политику явным правилом action: Pass.


Закрыть подам путь к служебному серверу

Теперь общий приём, который понадобится в части 22. В сети узлов есть серверы, к которым должны ходить узлы, но не поды: NFS-сервер nfs.example.com (192.168.10.30), который мы настроим в части 22, файловый сервер files.example.com (192.168.10.31) с общими папками по SMB (протокол доступа к файлам Windows, порт 445) для части 24, сервер резервных копий, интерфейсы управления оборудованием. На таких серверах доступ обычно ограничивают адресами узлов 192.168.10.17, .20 и .21. Но под, который идёт во внешнюю сеть, у нас выходит через NAT узла — с адресом узла. Для сервера он неотличим от самого узла, и ограничение по адресам узлов перестаёт что-либо значить.

Закрывать это нужно со стороны кластера: запретить всем подам исходящий трафик к служебным адресам. Командные NetworkPolicy для этого не годятся — они неймспейсные, только разрешающие, и любую из них команда может удалить. Нужна GlobalNetworkPolicy в отдельном tier'е, который проверяется раньше командных политик.

Почему это не ломает сами тома: сетевые тома монтирует ядро узла (о том, кто и как отдаёт ему команду, — в следующей части), и драйверы CSI, которые этим управляют, работают с hostNetwork: true. Их трафик — это трафик узла, а не WorkloadEndpoint, и политики подов к нему не применяются.

Сначала tier:

apiVersion: projectcalico.org/v3
kind: Tier
metadata:
  name: infra
spec:
  order: 200
  defaultAction: Pass

order: 200 — после служебного calico-system (100), но раньше kube-admin (1 000) и default (1 000 000). defaultAction: Pass — пакеты, которые не попали ни под одно правило этого tier'а, идут дальше, к обычным политикам.

Политика:

apiVersion: projectcalico.org/v3
kind: GlobalNetworkPolicy
metadata:
  name: infra.deny-pods-to-service-hosts
spec:
  tier: infra
  order: 100
  namespaceSelector: all()       # только поды: у узлов неймспейса нет
  types:
    - Egress
  egress:
    - action: Log
      destination:
        nets:
          - 192.168.10.30/32     # nfs.example.com
          - 192.168.10.31/32     # files.example.com
    - action: Deny
      destination:
        nets:
          - 192.168.10.30/32
          - 192.168.10.31/32
    - action: Pass               # всё остальное — на проверку дальше

Разберём, почему именно так.

  • namespaceSelector: all() выбирает конечные точки во всех неймспейсах — то есть поды. Без него селектор all() у GlobalNetworkPolicy выбрал бы и ресурсы без неймспейса — конечные точки узлов (HostEndpoint), если они когда-нибудь появятся, — и тогда политика закрыла бы служебным серверам дорогу от самих узлов. Для них в Calico есть отдельный селектор global().
  • Log перед Deny — каждая попытка попадёт в журнал ядра узла, и будет видно, какой под пытался достучаться до сервера. Когда журнал перестанет быть нужен, правило убирают.
  • Последнее правило Pass — обязательно, даже при defaultAction: Pass. Эта политика выбирает все поды кластера, и без явного Pass судьбу «остального» трафика решало бы поведение tier'а по умолчанию. Если положить такую политику в tier default только с правилом Deny, неявный запрет в конце tier'а отбросит весь исходящий трафик всех подов, кроме разрешённого командными политиками, — и в неймспейсах без политик кластер разом потеряет связь. Явный Pass делает намерение очевидным и не зависит от настроек tier'а.
  • Если какому-то поду доступ всё же нужен (например, задаче резервного копирования), для него ставится отдельное правило Allow с селектором до Deny — и это решение администратора, видимое в одном месте.

Адресов обычно больше двух; их удобно собрать в GlobalNetworkSet — именованный набор сетей с метками — и ссылаться на него селектором (destination: selector: role == 'service-hosts'), а не повторять адреса в каждом правиле.

Тем же способом в Calico делают и запрет по умолчанию сразу для всего кластера, которого нет в стандартном API: GlobalNetworkPolicy с namespaceSelector, исключающим системные неймспейсы (kubernetes.io/metadata.name not in {"calico-system", "kube-public", "kube-system", "tigera-operator"}), и правилом, разрешающим DNS, — такой пример есть в документации Calico.


ClusterNetworkPolicy

То, что мы сделали Calico-политикой, — администраторские правила поверх командных — давно хотели получить в стандартном API. В SIG Network этим занимается проект Network Policy API. Сначала он выпустил пару ресурсов AdminNetworkPolicy и BaselineAdminNetworkPolicy (версия v1alpha1), а в апреле 2026 года в выпуске v0.2.0 объединил их в один ресурс ClusterNetworkPolicy (policy.networking.k8s.io/v1alpha2) с полем tier: Admin — проверяется раньше NetworkPolicy и может запрещать, Baseline — проверяется после и задаёт правила по умолчанию, которые команды могут переопределить. Внутри уровня политики упорядочены полем priority от 0 до 1000, действия — Accept, Deny и Pass (передать решение дальше).

Статус на момент написания: это альфа, в сам Kubernetes не входит и ставится отдельными CRD, и поддерживают его не все сетевые плагины. Calico 3.32 его понимает и ставит CRD сам — на нашем кластере clusternetworkpolicies.policy.networking.k8s.io есть с установки, — а политики раскладывает по tier'ам kube-admin и kube-baseline, переводя priority в order. Для учебного кластера это интересно попробовать, но опираться в эксплуатации на альфа-API, поля которого ещё меняются (между v1alpha1 и v1alpha2 поменялись и названия ресурсов, и действия: Allow стал Accept), рано — политики Calico здесь надёжнее.


Смотрим руками

Стенд — как в конце прошлой части: Traefik в неймспейсе traefik отдаёт hello.apps.example.com из demo, где работают три пода hello и postgres из манифеста выше. Подготовим пробный под в demo и второй — в неймспейсе sandbox, где политик нет:

$ kubectl -n demo run probe --image=busybox:1.37 --labels=app.kubernetes.io/name=probe -- sleep 1d
pod/probe created
$ kubectl create namespace sandbox
namespace/sandbox created
$ kubectl -n sandbox run probe --image=busybox:1.37 -- sleep 1d
pod/probe created
$ kubectl -n demo get pods -o wide
NAME                       READY   STATUS    RESTARTS   AGE   IP              NODE         NOMINATED NODE   READINESS GATES
hello-6d5c6db6b4-2wvbm     1/1     Running   0          54s   172.20.211.30   k8s-node-1   <none>           <none>
hello-6d5c6db6b4-nt56v     1/1     Running   0          54s   172.20.172.44   k8s-node-2   <none>           <none>
hello-6d5c6db6b4-snq5t     1/1     Running   0          54s   172.20.211.27   k8s-node-1   <none>           <none>
postgres-dbd894d99-cqrv5   1/1     Running   0          53s   172.20.211.34   k8s-node-1   <none>           <none>
probe                      1/1     Running   0          41s   172.20.172.50   k8s-node-2   <none>           <none>
$ kubectl -n traefik get pods -o wide
NAME                       READY   STATUS    RESTARTS   AGE   IP              NODE         NOMINATED NODE   READINESS GATES
traefik-6b9b5d5f95-j9rhw   1/1     Running   0          69s   172.20.172.47   k8s-node-2   <none>           <none>
traefik-6b9b5d5f95-vvfzh   1/1     Running   0          69s   172.20.211.35   k8s-node-1   <none>           <none>

До политик всё открыто:

$ kubectl -n sandbox exec probe -- nc -zv -w 2 postgres.demo 5432
postgres.demo (10.106.39.155:5432) open
$ kubectl -n demo exec probe -- wget -qO- -T 2 http://hello.demo/
hello from hello-6d5c6db6b4-nt56v
$ kubectl -n demo exec probe -- nc -zv -w 2 192.168.10.31 445
192.168.10.31 (192.168.10.31:445) open

Под из чужого неймспейса открыл соединение с базой, а под из demo — с файловым сервером. Применяем default-deny:

$ kubectl apply -f default-deny.yaml
networkpolicy.networking.k8s.io/default-deny created
$ kubectl -n demo exec probe -- nslookup hello.demo.svc.cluster.local
;; connection timed out; no servers could be reached

command terminated with exit code 1
$ kubectl -n demo exec probe -- wget -qO- -T 2 http://hello.demo/
wget: bad address 'hello.demo'
command terminated with exit code 1

DNS сломался первым, как и ожидалось, и wget жалуется не на соединение, а на имя. Добавляем allow-dns и остальные политики:

$ kubectl apply -f allow-dns.yaml
networkpolicy.networking.k8s.io/allow-dns created
$ kubectl apply -f hello-from-traefik.yaml -f hello-to-postgres.yaml -f postgres-from-hello.yaml
networkpolicy.networking.k8s.io/hello-from-traefik created
networkpolicy.networking.k8s.io/hello-to-postgres created
networkpolicy.networking.k8s.io/postgres-from-hello created
$ kubectl -n demo get networkpolicy
NAME                  POD-SELECTOR                      AGE
allow-dns             <none>                            40s
default-deny          <none>                            91s
hello-from-traefik    app.kubernetes.io/name=hello      20s
hello-to-postgres     app.kubernetes.io/name=hello      20s
postgres-from-hello   app.kubernetes.io/name=postgres   20s
$ kubectl -n demo exec probe -- nslookup hello.demo.svc.cluster.local
Server:		10.96.0.10
Address:	10.96.0.10:53


Name:	hello.demo.svc.cluster.local
Address: 10.103.28.1

$ kubectl -n demo exec probe -- wget -qO- -T 2 http://hello.demo/
wget: download timed out
command terminated with exit code 1
$ kubectl -n sandbox exec probe -- nc -zv -w 2 postgres.demo 5432
nc: postgres.demo (10.106.39.155:5432): Connection timed out
command terminated with exit code 1

Полное имя в nslookup — не случайность: с коротким hello.demo busybox печатает NXDOMAIN для каждого суффикса поиска и выходит с ошибкой, даже найдя адрес (часть 16). DNS работает; probe не может к hello (у него нет разрешения на выход, а у hello — на вход от него), из sandbox к базе тоже не пройти. Заметьте: отказ выглядит как таймаут, а не «connection refused» — пакет отбрасывается молча.

Теперь проверим, что hello ходит к базе. Образ hello собран на distroless, в нём нет ни оболочки, ни nc, поэтому добавим в под эфемерный контейнер командой kubectl debug. Он запускается внутри уже работающего пода и делит с ним сетевое пространство имён — у него тот же адрес и те же политики. Удалить эфемерный контейнер из пода нельзя, он исчезнет вместе с подом:

$ kubectl -n demo debug hello-6d5c6db6b4-nt56v --image=busybox:1.37 --profile=general -c nettest -- sleep 1d
$ kubectl -n demo exec hello-6d5c6db6b4-nt56v -c nettest -- nc -zv -w 2 postgres 5432
postgres (10.106.39.155:5432) open
$ kubectl -n demo exec hello-6d5c6db6b4-nt56v -c nettest -- nc -zv -w 2 192.168.10.32 443
nc: 192.168.10.32 (192.168.10.32:443): Connection timed out
command terminated with exit code 1

К базе можно, к серверу в локальной сети 192.168.10.32 — нет: исходящий трафик hello тоже под default-deny. А вот что бывает, если забыть одну половину соединения:

$ kubectl -n demo delete networkpolicy postgres-from-hello
networkpolicy.networking.k8s.io "postgres-from-hello" deleted from demo namespace
$ kubectl -n demo exec hello-6d5c6db6b4-nt56v -c nettest -- nc -zv -w 2 postgres 5432
nc: postgres (10.106.39.155:5432): Connection timed out
command terminated with exit code 1
$ kubectl apply -f postgres-from-hello.yaml
networkpolicy.networking.k8s.io/postgres-from-hello created
$ kubectl -n demo exec hello-6d5c6db6b4-nt56v -c nettest -- nc -zv -w 2 postgres 5432
postgres (10.106.39.155:5432) open

Снаружи сайт работает — Traefik входит в hello по разрешению hello-from-traefik. В этой проверке к Traefik обращаемся напрямую через NodePort его сервиса (32486 на k8s-node-1), с ноутбука и с заголовком Host; полный путь через адрес MetalLB и TLS — тот, что собран в части 17 и двух следующих:

$ curl -s -m 5 -H 'Host: hello.apps.example.com' http://192.168.10.20:32486/
hello from hello-6d5c6db6b4-nt56v

Через шлюз с сертификатом это была бы команда curl -s https://hello.apps.example.com/; её вывод здесь не снимался.

Политика видит адреса после DNAT. Разрешим probe выход к API Kubernetes — сначала по адресу сервиса kubernetes, как подсказывает интуиция:

apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
  name: probe-to-api
  namespace: demo
spec:
  podSelector:
    matchLabels:
      app.kubernetes.io/name: probe
  policyTypes:
    - Egress
  egress:
    - to:
        - ipBlock:
            cidr: 10.96.0.1/32
      ports:
        - protocol: TCP
          port: 443
$ kubectl apply -f probe-to-api.yaml
networkpolicy.networking.k8s.io/probe-to-api created
$ kubectl -n demo exec probe -- nc -zv -w 2 kubernetes.default.svc.cluster.local 443
nc: kubernetes.default.svc.cluster.local (10.96.0.1:443): Connection timed out
command terminated with exit code 1

Не пускает. Меняем в политике cidr на 192.168.10.17/32 и port на 6443 — адрес и порт API-сервера, в которые kube-proxy превращает 10.96.0.1:443:

$ kubectl apply -f probe-to-api.yaml
networkpolicy.networking.k8s.io/probe-to-api configured
$ kubectl -n demo exec probe -- nc -zv -w 2 kubernetes.default.svc.cluster.local 443
kubernetes.default.svc.cluster.local (10.96.0.1:443) open
$ kubectl -n demo delete -f probe-to-api.yaml
networkpolicy.networking.k8s.io "probe-to-api" deleted from demo namespace

Под по-прежнему подключается к 10.96.0.1:443, а политика сравнивает уже 192.168.10.17:6443.

Что на узле. Под hello-6d5c6db6b4-nt56v работает на k8s-node-2 с адресом 172.20.172.44. Его интерфейс найдём по маршруту и сверим с хешем имени:

user@k8s-node-2:~$ ip route | grep 172.20.172.44
172.20.172.44 dev calid81a2fd6ead scope link metric 1024
user@k8s-node-2:~$ printf demo.hello-6d5c6db6b4-nt56v | sha1sum
d81a2fd6ead57c74c790ee4269d803e58bb91e4f  -

Пакет, который ядро узла отправляет в интерфейс пода, проходит хук forward, базовую цепочку Calico filter-FORWARD, цепочку filter-cali-FORWARD и попадает в диспетчер — карту вердиктов, где по имени выходного интерфейса сразу находится цепочка пода. На узле четыре пода — hello, probe из demo, Traefik и служебный под Calico:

user@k8s-node-2:~$ sudo nft list map ip calico filter-cali-to-wl-dispatch
table ip calico {
	map filter-cali-to-wl-dispatch {
		type ifname : verdict
		elements = { "cali5ed29fc4313" : goto filter-cali-tw-cali5ed29fc4313,
			     "calia463c8b45d5" : goto filter-cali-tw-calia463c8b45d5,
			     "cali1f20149a837" : goto filter-cali-tw-cali1f20149a837,
			     "calid81a2fd6ead" : goto filter-cali-tw-calid81a2fd6ead }
	}
}

Сама цепочка пода (комментарии с хешами свои на каждом узле):

user@k8s-node-2:~$ sudo nft list chain ip calico filter-cali-tw-calid81a2fd6ead
table ip calico {
	chain filter-cali-tw-calid81a2fd6ead {
		ct state established,related counter packets 48 bytes 4780 accept comment "cali:J8USgVKtKmeIe9eC;"
		ct state invalid counter packets 0 bytes 0 drop comment "cali:tpzDHJWaPoRJ9c-x;"
		counter packets 5 bytes 300 meta mark set meta mark & 0xfffcffff comment "cali:mOiJXmMqCk_6D0uA;"
		counter packets 3 bytes 180 meta mark set meta mark & 0xfffdffff comment "cali:-BrknvkZT89OBg5i; Start of tier default"
		meta mark & 0x00020000 == 0x00000000 counter packets 3 bytes 180 jump filter-cali-pi-knp/demo/default-deny comment "cali:wS87dAYgY8Qv1scI;"
		meta mark & 0x00010000 == 0x00010000 counter packets 0 bytes 0 return comment "cali:rJjBzICTlwANm-rK; Return if policy accepted"
		meta mark & 0x00020000 == 0x00000000 counter packets 3 bytes 180 jump filter-cali-pi-knp/demo/hello-from-traefik comment "cali:Ob1dmafZJNlYN6IN;"
		meta mark & 0x00010000 == 0x00010000 counter packets 3 bytes 180 return comment "cali:XT75spmzaSoDqrIn; Return if policy accepted"
		meta mark & 0x00020000 == 0x00000000 counter packets 0 bytes 0 drop comment "cali:QhISMjA7w6n0c7jC; End of tier default. Drop if no policies passed packet"
		counter packets 0 bytes 0 jump filter-cali-pri-kns.demo comment "cali:t5R3VD0h1d7cyDqK;"
		meta mark & 0x00010000 == 0x00010000 counter packets 0 bytes 0 return comment "cali:xOh-51vkZftxinOv; Return if profile accepted"
		counter packets 0 bytes 0 jump filter-cali-pri-ksa.demo.default comment "cali:-WMoRlMBSgE6l-hC;"
		meta mark & 0x00010000 == 0x00010000 counter packets 0 bytes 0 return comment "cali:tYnNxJTkcWQ0zGdu; Return if profile accepted"
		counter packets 0 bytes 0 drop comment "cali:xPRB56C7OTlIahZq; Drop if no profiles matched"
	}
}

Здесь вся модель в полутора десятках строк. Сначала пропускаются пакеты уже установленных соединений (conntrack) — так работает «ответ разрешён» и так уживаются открытые соединения с новыми политиками — и отбрасываются пакеты, которые conntrack считает некорректными. Затем сбрасываются биты-метки результата: 0x10000 — «принято политикой», 0x20000 — «передано дальше» (Pass). По очереди вызываются цепочки политик, выбирающих этот под: default-deny и hello-from-traefik, по имени видно и вид (knp — Kubernetes NetworkPolicy), и неймспейс. Политика, разрешившая пакет, ставит бит 0x10000, и цепочка пода возвращает управление — дальше пакет принимает базовая цепочка filter-FORWARD (Policy explicitly accepted packet). Если ни одна не разрешила и никто не поставил Pass — End of tier default ... drop: тот самый неявный запрет в конце tier'а. Ниже — профили kns.demo и ksa.demo.default, которые приняли бы всё, если бы под не выбрала ни одна политика; у нашего пода до них не доходит ни один пакет. Счётчики counter показывают, сколько пакетов прошло через каждое правило: все новые соединения, дошедшие до пода, приняла hello-from-traefik. Попытки probe сюда не дошли вовсе — их отбросила его собственная цепочка filter-cali-fw-… на выходе.

Цепочка политики hello-from-traefik — одно правило: источник из набора адресов, порт 8080, поставить бит «принято»:

user@k8s-node-2:~$ sudo nft list chain ip calico filter-cali-pi-knp/demo/hello-from-traefik
table ip calico {
	chain filter-cali-pi-knp/demo/hello-from-traefik {
		ip saddr @cali40s-rob4WbEGIc5jlJUd4ap62IG tcp dport 8080 counter packets 1 bytes 60 meta mark set meta mark | 0x00010000 comment "cali:xmq1Y6-XZkIVNcn7; KubernetesNetworkPolicy demo/hello-from-traefik ingress"
	}
}
user@k8s-node-2:~$ sudo nft list set ip calico cali40s-rob4WbEGIc5jlJUd4ap62IG
table ip calico {
	set cali40s-rob4WbEGIc5jlJUd4ap62IG {
		type ipv4_addr
		flags interval
		elements = { 172.20.172.47, 172.20.211.35 }
	}
}

В наборе — адреса двух подов Traefik, по одному на каждом рабочем узле. Масштабируете Traefik до трёх — Felix добавит третий адрес в набор, цепочки останутся прежними. Цепочка default-deny для входа пуста по смыслу — в ней одно правило continue: политика выбирает под, но ничего не разрешает.

Теперь выход пода probe (интерфейс cali5ed29fc4313 на том же узле):

user@k8s-node-2:~$ sudo nft list chain ip calico filter-cali-fw-cali5ed29fc4313
table ip calico {
	chain filter-cali-fw-cali5ed29fc4313 {
		ct state established,related counter packets 41 bytes 2278 accept comment "cali:AeGE1uIZEs6IHBCc;"
		ct state invalid counter packets 0 bytes 0 drop comment "cali:ul9cvPWeiE6970Y4;"
		counter packets 84 bytes 6128 meta mark set meta mark & 0xfffcffff comment "cali:-epCTaGIhf4ACahG;"
		udp dport 4789 counter packets 0 bytes 0 drop comment "cali:2zjF5A7i-ubwEJFH; Drop VXLAN encapped packets originating in workloads"
		meta l4proto ipencap counter packets 0 bytes 0 drop comment "cali:o6BUzHCylnAha-3F; Drop IPinIP encapped packets originating in workloads"
		counter packets 49 bytes 3732 meta mark set meta mark & 0xfffdffff comment "cali:5ZJaeiYdH_1iS0vr; Start of tier default"
		meta mark & 0x00020000 == 0x00000000 counter packets 33 bytes 2672 jump filter-cali-go-iBOnLIGKDZQ62MbnzC50 comment "cali:QPSvYligqE5NnYoT;"
		meta mark & 0x00010000 == 0x00010000 counter packets 26 bytes 2252 return comment "cali:k937F-wsU9jm-T4y; Return if policy accepted"
		meta mark & 0x00020000 == 0x00000000 counter packets 0 bytes 0 drop comment "cali:Y9PXwujeD4Iy_Mqj; End of tier default. Drop if no policies passed packet"
		counter packets 0 bytes 0 jump filter-cali-pro-kns.demo comment "cali:KUve0gdJD1lTvy1H;"
		meta mark & 0x00010000 == 0x00010000 counter packets 0 bytes 0 return comment "cali:8XgaEsHkxyfJbRYq; Return if profile accepted"
		counter packets 0 bytes 0 jump filter-cali-pro-ksa.demo.default comment "cali:fzqYFm0Vr-1cSXG0;"
		meta mark & 0x00010000 == 0x00010000 counter packets 0 bytes 0 return comment "cali:nbx0mCNhhs4-y0pv; Return if profile accepted"
		counter packets 0 bytes 0 drop comment "cali:GSMTU3rHcQPwWEoo; Drop if no profiles matched"
	}
}
user@k8s-node-2:~$ sudo nft list chain ip calico filter-cali-go-iBOnLIGKDZQ62MbnzC50
table ip calico {
	chain filter-cali-go-iBOnLIGKDZQ62MbnzC50 {
		counter packets 61 bytes 4590 jump filter-cali-po-knp/demo/allow-dns comment "cali:4kvO45nccx68evnZ;"
		meta mark & 0x00030000 == 0x00000000 counter packets 21 bytes 1260 jump filter-cali-po-knp/demo/default-deny comment "cali:GE12kpl8uZ68stOw;"
	}
}

У выхода две особенности. Два правила drop перед политиками отбрасывают пакеты, которые под сам завернул в VXLAN (UDP 4789) или IP-in-IP — туннели, в которых Calico возит пакеты подов между узлами (часть 14): так под не может выдать свой пакет за туннельный трафик между узлами. А политики allow-dns и default-deny для выхода выбирают поды одним селектором {}, поэтому Felix собрал их в группу filter-cali-go-…: allow-dns ставит «принято» пакетам к подам CoreDNS (их адреса — в своём наборе cali40s-…), остальные проходят через пустую default-deny, не получают ни «принято», ни Pass и отбрасываются правилом End of tier default. Счётчики соседних правил здесь не сходятся: когда меняются политики, Felix переписывает правила цепочек, и у нового правила счёт начинается с нуля.

GlobalNetworkPolicy. Применяем tier infra и политику и повторяем проверку из sandbox, где своих политик нет:

$ kubectl get tiers.projectcalico.org -o custom-columns=NAME:.metadata.name,ORDER:.spec.order,DEFAULT:.spec.defaultAction
NAME            ORDER      DEFAULT
calico-system   100        Deny
default         1000000    Deny
infra           200        Pass
kube-admin      1000       Pass
kube-baseline   10000000   Pass
$ kubectl -n sandbox exec probe -- nc -zv -w 2 192.168.10.31 445
nc: 192.168.10.31 (192.168.10.31:445): Connection timed out
command terminated with exit code 1
$ kubectl -n sandbox exec probe -- nc -zv -w 2 192.168.10.32 443
192.168.10.32 (192.168.10.32:443) open

К файловому серверу из sandbox не пройти, к остальной сети — пожалуйста: Pass отдал решение дальше, а в sandbox политик нет. Под probe в sandbox работает на k8s-node-1 с адресом 172.20.211.36, и в журнале ядра этого узла видна каждая попытка — первый SYN и его повтор через секунду:

user@k8s-node-1:~$ sudo journalctl -k --since -3min | grep calico-packet
Oct 29 17:14:27 k8s-node-1 kernel: calico-packet: IN=cali798b0e33dc9 OUT=ens18 MAC=ee:ee:ee:ee:ee:ee:e6:ba:dd:df:35:bf:08:00 SRC=172.20.211.36 DST=192.168.10.31 LEN=60 TOS=0x00 PREC=0x00 TTL=63 ID=40761 DF PROTO=TCP SPT=33081 DPT=445 WINDOW=64860 RES=0x00 SYN URGP=0 MARK=0x80000 
Oct 29 17:14:28 k8s-node-1 kernel: calico-packet: IN=cali798b0e33dc9 OUT=ens18 MAC=ee:ee:ee:ee:ee:ee:e6:ba:dd:df:35:bf:08:00 SRC=172.20.211.36 DST=192.168.10.31 LEN=60 TOS=0x00 PREC=0x00 TTL=63 ID=40762 DF PROTO=TCP SPT=33081 DPT=445 WINDOW=64860 RES=0x00 SYN URGP=0 MARK=0x80000 

SRC=172.20.211.36 — адрес пода, ещё до NAT: политика срабатывает раньше, чем natOutgoing подменит его на адрес узла. В цепочке выхода пода теперь первым идёт tier infra:

user@k8s-node-1:~$ sudo nft list chain ip calico filter-cali-fw-cali798b0e33dc9
table ip calico {
	chain filter-cali-fw-cali798b0e33dc9 {
		ct state established,related counter packets 6 bytes 312 accept comment "cali:J_LfkfwvC54l83v2;"
		ct state invalid counter packets 0 bytes 0 drop comment "cali:MfiUREkd1FQP_HzX;"
		counter packets 4 bytes 240 meta mark set meta mark & 0xfffcffff comment "cali:tz4G1cxLiOD-ZEMK;"
		udp dport 4789 counter packets 0 bytes 0 drop comment "cali:kavdU9UVVc0pYmKY; Drop VXLAN encapped packets originating in workloads"
		meta l4proto ipencap counter packets 0 bytes 0 drop comment "cali:-6_2lSk8w-Om-V8p; Drop IPinIP encapped packets originating in workloads"
		counter packets 3 bytes 180 meta mark set meta mark & 0xfffdffff comment "cali:CLeo9XtaTkaB1LqV; Start of tier infra"
		meta mark & 0x00020000 == 0x00000000 counter packets 3 bytes 180 jump filter-cali-po-gnp/infra.deny-pods-to-service-hosts comment "cali:0jLFQEYCXuKDMW2-;"
		meta mark & 0x00010000 == 0x00010000 counter packets 0 bytes 0 return comment "cali:THVQXgMg_INj4vLP; Return if policy accepted"
		counter packets 1 bytes 60 jump filter-cali-pro-kns.sandbox comment "cali:w9NEpwcoZm1K-J81;"
		meta mark & 0x00010000 == 0x00010000 counter packets 1 bytes 60 return comment "cali:uw0CZIYtsHoUOiew; Return if profile accepted"
		counter packets 0 bytes 0 jump filter-cali-pro-ksa.sandbox.default comment "cali:RmTNeXNdE1VRzbIu;"
		meta mark & 0x00010000 == 0x00010000 counter packets 0 bytes 0 return comment "cali:mGWnLQLcplCSpp5i; Return if profile accepted"
		counter packets 0 bytes 0 drop comment "cali:CFU8WikkcUpyKa4J; Drop if no profiles matched"
	}
}
user@k8s-node-1:~$ sudo nft list chain ip calico filter-cali-po-gnp/infra.deny-pods-to-service-hosts
table ip calico {
	chain filter-cali-po-gnp/infra.deny-pods-to-service-hosts {
		counter packets 3 bytes 180 meta mark set meta mark & 0xffe7ffff comment "cali:tPsxEs9yNevw57sV; GlobalNetworkPolicy infra.deny-pods-to-service-hosts egress"
		ip daddr 192.168.10.30 counter packets 0 bytes 0 meta mark set meta mark | 0x00080000 comment "cali:BevwyTLT-AsDpjvJ;"
		ip daddr 192.168.10.31 counter packets 2 bytes 120 meta mark set meta mark | 0x00080000 comment "cali:8c-HdlA1pEPopaz0;"
		meta mark & 0x00080000 == 0x00080000 counter packets 2 bytes 120 log prefix "calico-packet: " level info comment "cali:TS4pb3hyChf-kKaL;"
		counter packets 3 bytes 180 meta mark set meta mark & 0xffe7ffff comment "cali:IbLmPGYDsdd-1P9P;"
		ip daddr 192.168.10.30 counter packets 0 bytes 0 meta mark set meta mark | 0x00080000 comment "cali:r3dpDZl_FK_647kN;"
		ip daddr 192.168.10.31 counter packets 2 bytes 120 meta mark set meta mark | 0x00080000 comment "cali:4IemNuPhSwSiXvWz;"
		meta mark & 0x00080000 == 0x00080000 counter packets 2 bytes 120 meta mark set meta mark | 0x00040000 comment "cali:9cqIl-YWHB6_XRPC;"
		meta mark & 0x00040000 == 0x00040000 counter packets 2 bytes 120 drop comment "cali:KbiVF72qasWlTRW8;"
		counter packets 1 bytes 60 meta mark set meta mark | 0x00020000 comment "cali:BV9Sd-zYRt1FO0Bv;"
	}
}

Каждое правило политики Felix разворачивает в несколько строк: адреса из nets по одному отмечают пакет служебным битом 0x80000 («совпало»), а затем действие выполняется по этому биту — log для правила Log (отсюда MARK=0x80000 в журнале), бит 0x40000 и drop для Deny. Последняя строка — наш Pass: бит 0x20000. После него в цепочке пода нет End of tier infra ... drop (у tier'а defaultAction: Pass), а поскольку в sandbox пакет не выбирает больше ни одна политика, он доходит до профиля kns.sandbox и принимается. И проверка с самого узла — служебный сервер доступен ему как прежде:

user@k8s-node-1:~$ nc -zv -w 2 192.168.10.31 445
Connection to 192.168.10.31 445 port [tcp/microsoft-ds] succeeded!

Источники


Теперь внутри кластера есть стены: неймспейс demo закрыт по умолчанию и открыт ровно настолько, насколько нужно, а служебные серверы сети узлов закрыты от всех подов политикой, которую команды не могут обойти.

Дальше — тома и CSI: где под хранит данные, которые должны пережить перезапуск, как устроены PersistentVolume, PersistentVolumeClaim и StorageClass, и что делает драйвер CSI, чтобы на узле появился смонтированный каталог.

Оставить комментарий могут только зарегистрированные пользователи.

Войдите на сайт или зарегистрируйтесь, чтобы оставить комментарий.