Перейти к содержимому
GUARDCORE Инженерная база знаний Поиск

Блог

NetworkPolicy с нуля: как реально изолировать приложения в Kubernetes

РазделБлог
ФорматПрактический разбор
ЦиклGuardCore
Чтение12 минут

Кратко

Цикл: «Сетевая безопасность Kubernetes: от основ до практической защиты» · материал 5 из 12Формат: практический разборДля кого: ИБ-специалист, DevOps-инженер, архитектор и разработчик, которым нужно безопасно начать сегментацию Kubernetes-кластера. Кратко NetworkPolicy — это не «один firewall на кластер» и не универсальный YAML-шаблон. Это…

Цикл: «Сетевая безопасность Kubernetes: от основ до практической защиты» · материал 5 из 12
Формат: практический разбор
Для кого: ИБ-специалист, DevOps-инженер, архитектор и разработчик, которым нужно безопасно начать сегментацию Kubernetes-кластера.

Кратко

NetworkPolicy — это не «один firewall на кластер» и не универсальный YAML-шаблон. Это набор разрешений для выбранных Pod. В стандартной модели Kubernetes Pod, который не выбран политикой для направления ingress или egress, не изолирован в этом направлении; после default deny приложение перестаёт работать, пока явно не описаны его реальные зависимости. Поэтому безопасный путь — сначала восстановить карту потоков, затем изолировать один namespace, разрешить DNS и необходимые соединения, проверить разрешённый и запрещённый трафик и только после этого расширять охват.

В статье разберём воспроизводимый стенд client → backend, а затем перенесём этот приём на цепочку frontend → backend → PostgreSQL. Покажем минимальные манифесты, команды проверки и признаки ошибок. Примеры используют стандартный API networking.k8s.io/v1; фактическое применение зависит от CNI-плагина.

Что NetworkPolicy делает, а что нет

NetworkPolicy описывает правила L3/L4 для входящего и исходящего трафика Pod. Она выбирает Pod по меткам и определяет, от кого и на какие порты разрешены соединения. Сам Kubernetes хранит объект политики, но пакет блокирует или пропускает сетевой datapath выбранного CNI. Если установленный CNI не поддерживает enforcement, объект будет принят API, но изоляции не произойдёт. Это первое, что нужно проверять до любой работы; ссылка на первоисточник приведена в конце статьи.

NetworkPolicy не заменяет:

  • RBAC и ServiceAccount: политика не запрещает разрешённому workload менять объекты через Kubernetes API;
  • Ingress-controller, WAF или внешний NGFW: они отвечают за другие границы;
  • защиту самого образа и runtime detection;
  • контроль L7-содержимого, DNS-имён или TLS-идентичности, если эти функции не реализует конкретный CNI или отдельный компонент.

Правильная модель проста: NetworkPolicy уменьшает сетевую достижимость после компрометации Pod, но не устраняет саму компрометацию.

Результат, который нужно получить

Для production-сценария в namespace должны выполняться только три связи:

frontend  → backend:8080
backend   → postgres:5432
frontend/backend → cluster DNS:53 UDP/TCP

Не должны выполняться:

frontend  → postgres:5432
frontend  → произвольный Pod в namespace
backend   → Internet и произвольные внутренние адреса

Этот пример намеренно небольшой. В реальной системе к нему добавятся мониторинг, логирование, IdP, registry, NTP, backup, proxy и внешние API. Их нельзя разрешать «на всякий случай»: каждая связь должна иметь владельца и назначение.

До начала: четыре обязательные проверки

1. Зафиксируйте границу изменения

Не начинайте с default deny для всего кластера. Выберите один не критичный namespace или подготовьте отдельный тестовый namespace. Нужны:

  • доступ к API через kubectl с правами создавать Deployment, Service и NetworkPolicy;
  • список CNI и его версии;
  • способ посмотреть заблокированные потоки или хотя бы воспроизвести соединение из тестового Pod;
  • окно изменения и понятный откат.

Минимальная инвентаризация:

kubectl get nodes -o wide
kubectl get pods -A -o wide
kubectl get networkpolicy -A
kubectl get ds -n kube-system

Последняя команда часто помогает увидеть используемый CNI, но не заменяет проверку его документации и конфигурации. Нельзя делать вывод «CNI поддерживает NetworkPolicy» только по имени DaemonSet.

2. Убедитесь, что политика реально применяется

Создайте отдельный namespace:

kubectl create namespace np-lab

До включения изоляции разверните два временных Pod и подтвердите, что они могут взаимодействовать. После применения default deny повторите тот же тест. Если соединение продолжает работать, не переходите к следующим этапам: либо CNI не применяет NetworkPolicy, либо тест обращается не туда, куда вы считаете.

3. Составьте матрицу потоков

До YAML нужна не коллекция селекторов, а таблица зависимостей.

Источник Получатель Порт/протокол Зачем нужен Владелец Как проверяем
client (в стенде) / frontend (в production) backend TCP/8080 API приложения команда приложения HTTP-запрос
backend postgres TCP/5432 хранение данных владелец БД pg_isready/тест приложения
оба CoreDNS UDP+TCP/53 разрешение имён сервисов платформа nslookup

Если строку нельзя объяснить, её нельзя автоматически разрешать. Именно на этой стадии обнаруживаются скрытые зависимости.

4. Не забывайте DNS

Pod обычно обращаются к Service по DNS-имени, а не по постоянному IP. Kubernetes создаёт DNS-записи для Service и настраивает DNS Pod через kubelet. Блокировка доступа к DNS часто выглядит как «backend недоступен», хотя проблема не в backend. Ссылка на официальное описание DNS приведена в конце статьи.

IP и метки CoreDNS различаются между кластерами. Не копируйте порт и namespaceSelector из примера без проверки своих объектов:

kubectl get svc -n kube-system
kubectl get pods -n kube-system -l k8s-app=kube-dns --show-labels

Лабораторный стенд

Ниже — минимальный стенд. Образы намеренно простые; это не production-манифесты.

apiVersion: v1
kind: Namespace
metadata:
  name: np-lab
---
apiVersion: apps/v1
kind: Deployment
metadata:
  name: backend
  namespace: np-lab
spec:
  replicas: 1
  selector:
    matchLabels:
      app: backend
  template:
    metadata:
      labels:
        app: backend
    spec:
      containers:
      - name: backend
        image: hashicorp/http-echo:1.0
        args: ["-text=backend-ok", "-listen=:8080"]
        ports:
        - containerPort: 8080
---
apiVersion: v1
kind: Service
metadata:
  name: backend
  namespace: np-lab
spec:
  selector:
    app: backend
  ports:
  - port: 8080
    targetPort: 8080
---
apiVersion: apps/v1
kind: Deployment
metadata:
  name: client
  namespace: np-lab
spec:
  replicas: 1
  selector:
    matchLabels:
      app: client
  template:
    metadata:
      labels:
        app: client
    spec:
      containers:
      - name: client
        image: curlimages/curl:8.10.1
        command: ["sleep", "infinity"]

Примените и дождитесь готовности:

kubectl apply -f lab.yaml
kubectl -n np-lab get pods -w

Проверка до изоляции:

kubectl -n np-lab exec deploy/client -- curl -fsS --max-time 3 http://backend:8080

Ожидаемый результат: backend-ok. Команда kubectl exec выполняет указанную программу внутри контейнера.

Шаг 1. Включите default deny в одном namespace

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

podSelector: {} выбирает все Pod в np-lab. Пустые правила ingress и egress не добавляют разрешений; после применения каждый выбранный Pod изолирован для соответствующего направления. Явно указанный policyTypes исключает неоднозначность поведения: API допускает Ingress, Egress или оба значения.

kubectl apply -f 01-default-deny.yaml
kubectl -n np-lab exec deploy/client -- curl -fsS --max-time 3 http://backend:8080 || echo "blocked as expected"

Ожидаемый результат: соединение не проходит, а команда выводит blocked as expected. Если она успешно получает backend-ok, остановитесь и разберите enforcement. Если она зависает, это нормально для сетевой блокировки; всегда ставьте небольшой --max-time.

Шаг 2. Сначала восстановите DNS

default deny для egress также блокирует DNS. Поэтому повторно проверять приложение по имени до отдельного разрешения на DNS бессмысленно: ошибка резолвинга будет маскироваться под ошибку доступа к backend.

Сначала определите фактический DNS Service и его endpoints:

kubectl -n kube-system get svc kube-dns
kubectl -n kube-system get endpoints kube-dns

После этого создайте разрешение на UDP и TCP/53 для Pod DNS. Вариант ниже применим только если DNS Pod находятся в kube-system и имеют метку k8s-app: kube-dns; это распространённая, но не универсальная конфигурация.

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

Сохраните манифест как 02-allow-dns.yaml и примените его:

kubectl apply -f 02-allow-dns.yaml

Проверьте DNS из отдельного диагностического Pod. Официальная документация Kubernetes использует для этого образ agnhost и nslookup; аналогичный тест безопаснее выполнять именно в целевом namespace.

kubectl -n np-lab run dnsutils \
  --image=registry.k8s.io/e2e-test-images/agnhost:2.39 \
  --restart=Never --command -- sleep infinity
kubectl -n np-lab wait --for=condition=Ready pod/dnsutils --timeout=60s
kubectl -n np-lab exec dnsutils -- nslookup backend.np-lab.svc.cluster.local

Если тест не проходит, не открывайте egress в 0.0.0.0/0 ради «быстрого исправления». Проверьте адрес DNS, метки Pod DNS, namespace, resolv.conf внутри Pod и правила CNI.

Шаг 3. Разрешите одну связь в обоих направлениях политики

Для соединения от одного Pod к другому после default deny должны разрешить оба конца: egress у источника и ingress у получателя. Ниже два объекта в одном файле 03-client-backend.yaml.

apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
  name: allow-client-egress-to-backend
  namespace: np-lab
spec:
  podSelector:
    matchLabels:
      app: client
  policyTypes:
  - Egress
  egress:
  - to:
    - podSelector:
        matchLabels:
          app: backend
    ports:
    - protocol: TCP
      port: 8080
---
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
  name: allow-client-ingress-to-backend
  namespace: np-lab
spec:
  podSelector:
    matchLabels:
      app: backend
  policyTypes:
  - Ingress
  ingress:
  - from:
    - podSelector:
        matchLabels:
          app: client
    ports:
    - protocol: TCP
      port: 8080

Первая политика выбирает источник — Pod client — и разрешает ему egress к Pod backend. Вторая выбирает получателя backend и разрешает ingress только от client. Оба podSelector без namespaceSelector относятся к namespace самой политики: здесь это np-lab. Для связи между namespace добавляют namespaceSelector и проверяют, что он находится в том же элементе списка, что и podSelector.

NetworkPolicy — аддитивная модель: несколько политик складывают разрешения, а правило «запретить поверх разрешения» стандартный API не создаёт. Поэтому аудитируют итоговый набор политик для конкретного Pod, а не только последний применённый файл.

kubectl apply -f 03-client-backend.yaml
kubectl -n np-lab exec deploy/client -- curl -fsS --max-time 3 http://backend:8080

Ожидаемый результат: снова backend-ok.

Проверьте, что другой Pod не получил доступ только из-за нахождения в том же namespace:

kubectl -n np-lab run other --image=curlimages/curl:8.10.1 --restart=Never --command -- sleep 300
kubectl -n np-lab wait --for=condition=Ready pod/other --timeout=60s
kubectl -n np-lab exec other -- curl -fsS --max-time 3 http://backend:8080 || echo "blocked as expected"

Если other проходит, значит селекторы слишком широки, существует другая разрешающая политика или NetworkPolicy фактически не применяется.

Как диагностировать проблему после включения политики

Диагностика идёт в строгом порядке:

  1. Pod жив и имеет ожидаемые labels. kubectl -n np-lab get pod --show-labels.
  2. Service выбирает нужные endpoints. kubectl -n np-lab get endpoints backend.
  3. Имя разрешается. nslookup backend.np-lab.svc.cluster.local.
  4. Политика выбирает адресата. Селектор в podSelector проверяется по labels backend, а не client.
  5. Источник соответствует from. Namespace и labels должны совпадать одновременно, если используются оба селектора.
  6. Порт совпадает с фактическим containerPort/Service targetPort.
  7. Нет другой политики с неожиданным широким разрешением. kubectl -n np-lab get networkpolicy -o yaml.
  8. Есть данные выбранного CNI. Для Cilium это, например, policy verdict/flow; для другого CNI путь будет иным.

Не используйте ping как универсальную проверку: ICMP и его обработка не одинаковы у разных реализаций policy. Проверяйте тот же TCP/UDP-протокол, который использует приложение.

Как безопасно откатить изменение

Для лаборатории:

kubectl delete namespace np-lab

Для production не удаляют все политики одним действием. Откат должен быть заранее зафиксирован: имя конкретной политики, владелец, допустимое окно и тест после удаления. Если default deny уже защищает namespace, его удаление резко меняет границу безопасности. Предпочтительнее временно добавить узкое разрешение для подтверждённой зависимости, задокументировать его и затем пересмотреть.

Что переносить в production

После успешной лаборатории последовательность такая:

  1. Выбрать один namespace и владельца приложения.
  2. Собрать реальные потоки из телеметрии и документации приложения.
  3. Согласовать матрицу связей с ИТ, разработкой и ИБ.
  4. Ввести режим наблюдения, если его поддерживает выбранный инструмент, либо пилотировать на не критичной среде.
  5. Включить default deny только после появления минимального набора узких разрешений.
  6. Проверить бизнес-сценарии, DNS, мониторинг, логи, backup и аварийный доступ.
  7. Хранить политики в Git и ограничить права на их изменение через RBAC.
  8. Периодически пересматривать исключения и широкие селекторы.

Типичные ошибки

  • Политика есть в API, но CNI её игнорирует. Наличие объекта ничего не доказывает без отрицательного теста.
  • default deny включён без карты зависимостей. Команда чинит сбой широким allow all и теряет смысл сегментации.
  • Разрешён только UDP/53. Часть DNS-сценариев использует TCP.
  • Селекторы основаны на неуправляемых labels. Любой, кто может менять security-relevant label, может повлиять на границу доступа.
  • Проверяется только успешный сценарий. Нужно показать и разрешённое, и запрещённое соединение.
  • Правила не имеют владельца. После смены приложения никто не знает, какое исключение больше не нужно.

Итог

Практическая ценность NetworkPolicy появляется не после применения YAML, а после доказанной работы ограничений. Для каждого критичного взаимодействия должны существовать: назначение, точный источник и получатель, порт, владелец, проверка allow/deny и понятный откат. Именно так NetworkPolicy становится частью управляемой микросегментации, а не набором манифестов в репозитории.

Официальные источники


Навигация по циклу:
← Атака внутри Kubernetes: lateral movement и blast radius · Начало цикла · CNI и eBPF: кто на самом деле фильтрует трафик Kubernetes →

Редакция GuardCore

Материал подготовлен на основе указанных в тексте нормативных источников и практических оснований. При существенном обновлении дата актуализации отображается рядом с датой публикации.

Обсуждение материала

Дополните материал практическим опытом или сообщите о неточности. Комментарий появится после проверки.

Форма защищена от автоматических публикаций. Все комментарии проходят модерацию.