Цикл: «Сетевая безопасность 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 фактически не применяется.
Как диагностировать проблему после включения политики
Диагностика идёт в строгом порядке:
- Pod жив и имеет ожидаемые labels.
kubectl -n np-lab get pod --show-labels. - Service выбирает нужные endpoints.
kubectl -n np-lab get endpoints backend. - Имя разрешается.
nslookup backend.np-lab.svc.cluster.local. - Политика выбирает адресата. Селектор в
podSelectorпроверяется по labels backend, а не client. - Источник соответствует
from. Namespace и labels должны совпадать одновременно, если используются оба селектора. - Порт совпадает с фактическим containerPort/Service targetPort.
- Нет другой политики с неожиданным широким разрешением.
kubectl -n np-lab get networkpolicy -o yaml. - Есть данные выбранного CNI. Для Cilium это, например, policy verdict/flow; для другого CNI путь будет иным.
Не используйте ping как универсальную проверку: ICMP и его обработка не одинаковы у разных реализаций policy. Проверяйте тот же TCP/UDP-протокол, который использует приложение.
Как безопасно откатить изменение
Для лаборатории:
kubectl delete namespace np-lab
Для production не удаляют все политики одним действием. Откат должен быть заранее зафиксирован: имя конкретной политики, владелец, допустимое окно и тест после удаления. Если default deny уже защищает namespace, его удаление резко меняет границу безопасности. Предпочтительнее временно добавить узкое разрешение для подтверждённой зависимости, задокументировать его и затем пересмотреть.
Что переносить в production
После успешной лаборатории последовательность такая:
- Выбрать один namespace и владельца приложения.
- Собрать реальные потоки из телеметрии и документации приложения.
- Согласовать матрицу связей с ИТ, разработкой и ИБ.
- Ввести режим наблюдения, если его поддерживает выбранный инструмент, либо пилотировать на не критичной среде.
- Включить
default denyтолько после появления минимального набора узких разрешений. - Проверить бизнес-сценарии, DNS, мониторинг, логи, backup и аварийный доступ.
- Хранить политики в Git и ограничить права на их изменение через RBAC.
- Периодически пересматривать исключения и широкие селекторы.
Типичные ошибки
- Политика есть в API, но CNI её игнорирует. Наличие объекта ничего не доказывает без отрицательного теста.
default denyвключён без карты зависимостей. Команда чинит сбой широкимallow allи теряет смысл сегментации.- Разрешён только UDP/53. Часть DNS-сценариев использует TCP.
- Селекторы основаны на неуправляемых labels. Любой, кто может менять security-relevant label, может повлиять на границу доступа.
- Проверяется только успешный сценарий. Нужно показать и разрешённое, и запрещённое соединение.
- Правила не имеют владельца. После смены приложения никто не знает, какое исключение больше не нужно.
Итог
Практическая ценность NetworkPolicy появляется не после применения YAML, а после доказанной работы ограничений. Для каждого критичного взаимодействия должны существовать: назначение, точный источник и получатель, порт, владелец, проверка allow/deny и понятный откат. Именно так NetworkPolicy становится частью управляемой микросегментации, а не набором манифестов в репозитории.
Официальные источники
- Kubernetes: Network Policies
- Kubernetes API: NetworkPolicy v1
- Kubernetes: DNS for Services and Pods
- Kubernetes: Debugging DNS Resolution
- Kubernetes: Declare Network Policy
Навигация по циклу:
← Атака внутри Kubernetes: lateral movement и blast radius · Начало цикла · CNI и eBPF: кто на самом деле фильтрует трафик Kubernetes →