Network Policy and Pod Security
Свежий под Kubernetes полностью открыт: любой под достаёт любой другой по плоской сети, а без SecurityContext контейнер работает под root с доступом к хосту. NetworkPolicy и Pod Security Standards — два opt-in барьера, превращающих эту позицию default-allow в default-deny.
Пентестер пробивает один малоценный под — ресайзер картинок за публичным эндпоинтом загрузки, уязвимый к SSRF. На захардененном хосте на этом бы всё и кончилось: один скомпрометированный процесс в одном контейнере. Но это дефолтный кластер Kubernetes, поэтому две двери уже открыты. Первая: под может открыть TCP-соединение к каждому другому поду в кластере — к базе, внутреннему admin-API, эндпоинту метрик, что течёт токенами, к сервису облачных метаданных на 169.254.169.254. Файрвола между нагрузками нет, потому что его никто не написал. Вторая: контейнер пода работает под UID 0 с дефолтным SecurityContext, так что цепочка SSRF→RCE отдаёт атакующему root внутри контейнера, который может mount, грузить модули ядра через щедрый набор capabilities и зондировать ноду. За двадцать минут ресайзер становится точкой пивота во весь кластер. Kubernetes сделал ровно то, что обещал: запланировал под и дал ему говорить со всеми. Никто не велел ему этого не делать.
К концу урока ты сможешь написать NetworkPolicy с default-deny и прочитать существующую на предмет дыры, что тихо открывает её обратно, применить профиль Pod Security Standards, блокирующий побеги root/privileged/hostPath, и объяснить, почему оба барьера opt-in и почему этот дефолт кусает команды каждый раз.
Две двери default-allow: сеть и спека пода
У безопасности Kubernetes повторяющаяся форма: платформа поставляется в режиме default-allow, а безопасность — это то, что ты добавляешь. Две такие двери — тема этого урока, и они независимы: закрытие одной ничего не делает для другой.
Дверь первая — плоская сеть. Сетевая модель Kubernetes требует, чтобы каждый под мог достать любой другой под по IP, через все неймспейсы, без NAT. Это не баг; это контракт, который обязан выполнять CNI-плагин. Следствие: east-west-трафик (под-под) по умолчанию полностью открыт. NetworkPolicy — это opt-in объект, который его ограничивает, но только если твой CNI реализует enforcement NetworkPolicy (Calico, Cilium и большинство managed-CNI это делают; легаси-дефолт flannel — нет, так что политика, которую ты kubectl apply, успешно применяется и тихо не enforce-ит ничего).
Дверь вторая — SecurityContext пода. Контейнер без securityContext работает под тем UID, который задаёт образ — часто root (UID 0) — наследует дефолтный набор Linux-capabilities, может получить privileged: true, монтировать тома hostPath и делить с хостом namespace PID или сети. Каждое из этого — задокументированный примитив побега из контейнера. Pod Security Standards (PSS) определяют три профиля — privileged (без ограничений), baseline (блокировать известные побеги) и restricted (жёстко закрыт: non-root, без добавленных capabilities, seccomp включён) — а Pod Security Admission (PSA) — это встроенный admission-контроллер, enforce-ящий выбранный профиль для неймспейса через метки.
NetworkPolicy: аддитивный allow и паттерн default-deny
NetworkPolicy только разрешающая и аддитивная, ровно как RBAC — deny-правила нет. Трюк, заставляющий её вести себя как файрвол, — это селектор: в момент, когда любая NetworkPolicy выбирает под для заданного направления (ingress или egress), этот под перещёлкивается с default-allow на default-deny для этого направления, и пропускается только тот трафик, что явно перечислен в любой выбирающей его политике. Объединение всех совпавших политик — это allow-список.
Поэтому каноничный приём хардненинга — это default-deny на неймспейс: политика, что выбирает каждый под, но не перечисляет разрешённых соседей:
| Политика | Селектор + направление | Эффект | Грабли при пропуске |
|---|---|---|---|
| default-deny-all | podSelector: {}, оба Ingress + Egress | Каждый под в неймспейсе дропает весь трафик, пока не разрешено | Плоская сеть — пробитый под достаёт весь кластер |
| allow-dns | egress к kube-dns по UDP/TCP 53 | Даёт подам резолвить имена, когда egress запрещён | Каждый под ломается — DNS первым убивает default-deny |
| allow-from-frontend | ingress к app: api от app: web | Только web-ярус может достать API-ярус | Любой под бьёт прямо в API в обход края |
| block-metadata | egress deny к 169.254.169.254/32 | Останавливает кражу облачных кредов через SSRF из любого пода | SSRF в одном поде читает IAM-роль / креды ноды |
Неочевидный режим отказа живёт во второй строке. В тот миг, когда ты применяешь default-deny egress-политику, ты заодно блокируешь DNS-запросы пода к kube-dns, потому что это egress к другому поду на порт 53. Каждый запрос тогда падает не с «connection refused», а с ошибкой резолва имени, что отправляет инженеров отлаживать не тот слой на час. Корректный default-deny поэтому всегда пара: deny-all плюс явное allow-DNS egress-правило. Это самая частая причина, по которой команды откатывают NetworkPolicy и объявляют её «слишком сложной».
▸Частая ошибка
NetworkPolicy, которую kubectl apply принимает, — не то же самое, что NetworkPolicy, которая enforce-ится. NetworkPolicy — это API-объект, хранимый в etcd; enforcement — работа CNI-плагина, и не каждый CNI её делает. На кластере с голым flannel (или с неверным режимом Cilium/Calico) твой тщательно написанный default-deny сохранён, виден в kubectl get netpol и дропает ровно ноль пакетов. Всегда проверяй enforcement эмпирически — kubectl exec в под, который политика должна изолировать, и попробуй достать соседа, которого не должна — а не доверяй тому, что объект существует. «Политика применена» и «трафик заблокирован» — два разных утверждения.
Pod Security Standards: блокировать побег, а не только сеть
NetworkPolicy сдерживает латеральное движение между подами. Она ничего не делает с подом, который сбегает вниз к ноде — а это разрешает неограниченный SecurityContext. Три побега, что важны, и что делает с каждым профиль restricted:
- Работа под root.
runAsNonRoot: trueплюс ненулевойrunAsUserозначает, что побег из контейнера высаживает атакующего под непривилегированным UID, а не root, резко ограничивая, что он может на ноде, если вырвется из namespace. privileged: trueи добавленные capabilities. У privileged-контейнера по сути все capabilities и доступ к устройствам — это почти прямой путь к хосту.restrictedзапрещаетprivileged, требует сброситьALLcapabilities и возвращает обратно толькоNET_BIND_SERVICE.- Host-namespaces и
hostPath.hostPID: true/hostNetwork: trueделят процессный или сетевой вид ноды; томhostPathможет примонтировать/и прочитать креды kubelet.restrictedиbaselineоба это блокируют.
Pod Security Admission enforce-ит профиль по метке неймспейса, в трёх режимах — enforce (отклонять несоответствующие поды), audit (логировать их) и warn (предупреждать применяющего) — так что можно выкатить сперва в warn/audit и посмотреть, что сломалось бы, перед переключением на enforce:
Ты хардишь неймспейс stateless-подов приложения, которым никогда не нужны root, доступ к хосту или лишние capabilities. Выбери позицию Pod Security.
Ты применяешь default-deny `Ingress` + `Egress` NetworkPolicy к неймспейсу, и сразу каждый под падает с ошибками резолва имени. Что произошло?
Неймспейс enforce-ит профиль `restricted` Pod Security, но CNI кластера не реализует NetworkPolicy. Под скомпрометирован через RCE. Каков реалистичный следующий ход атакующего?
Упорядочь шаги, чтобы безопасно захарденить неймспейс без простоя, от первого к последнему:
- 1 Пометь неймспейс `warn`/`audit` для профиля `restricted` и смотри, что было бы отклонено
- 2 Почини SecurityContext подов, помеченных audit (runAsNonRoot, сброс capabilities)
- 3 Переключи Pod Security на `enforce: restricted`, когда audit чист
- 4 Примени default-deny ingress+egress NetworkPolicy плюс явное allow-DNS правило
- 5 Добавь узкие allow-правила для реального трафика, затем проверь enforcement через kubectl exec
- 01Пройди шаг за шагом, почему `kubectl apply` NetworkPolicy может успешно пройти и всё равно блокировать ноль трафика, и как бы ты доказал enforcement.
- 02Почему NetworkPolicy и Pod Security Standards дополняют друг друга, а не дублируют, и что каждый не покрывает?
У свежего пода Kubernetes две независимые двери default-allow. Сеть плоская: каждый под достаёт любой другой по IP через неймспейсы, так что east-west-трафик нараспашку, пока NetworkPolicy его не ограничит. NetworkPolicy аддитивна и только разрешающая (deny-правила нет) — выбор пода перещёлкивает его в default-deny для этого направления, и каноничный хардненинг — это пара default-deny: deny-all ingress+egress плюс явное allow-DNS egress-правило, потому что первое, что ломает наивный запрет egress, — резолв имён к kube-dns. Критично: NetworkPolicy enforce-ится, только если CNI её реализует, так что «применено» надо проверять против «enforce-ится», тестируя dataplane через kubectl exec. Вторая дверь — SecurityContext пода: без ограничения контейнер работает под root с дефолтным набором capabilities и может запросить privileged, hostPath и host-namespaces — каждое примитив побега на ноду. Pod Security Standards определяют профили privileged/baseline/restricted, а Pod Security Admission enforce-ит один на неймспейс через метку, в режимах enforce/audit/warn, так что ты стейджишь выкат перед тем, как он начнёт отклонять поды. Эти два контроля ортогональны: NetworkPolicy сдерживает латеральное движение, PSS блокирует побег на ноду, а компрометации нужна лишь одна открытая дверь — поэтому применяй оба, плюс минимальный RBAC у ServiceAccount. Когда в следующий раз будешь ревьюить неймспейс, три вопроса: есть ли default-deny NetworkPolicy (и enforce-ит ли её CNI), какой профиль Pod Security enforce-ится и может ли любой под всё ещё достать эндпоинт метаданных?
Практика
Начни сверху. Задачи идут от простого к сложному: вспомнить факт, применить к случаю, затем senior-уровень. Открой, попробуй, потом открой ответ.
Что-то непонятно?
Задай вопрос по этому уроку. Вопросы анонимны и попадают напрямую автору — урок станет лучше.
Примени это
Примени этот урок в реальном проекте.