Представь ситуацию. Разработчик выкатывает Deployment, а в нём - контейнер с image: myapp:latest, securityContext с privileged: true, hostPath на /var, ни одного resource limit и образ тянется с какого-то dockerhub.io/randomuser. RBAC проверил, что у разработчика есть право create pods в этом namespace - и всё, etcd радостно записал манифест, kubelet запустил привилегированный контейнер с доступом к корню ноды. Поздравляю, у тебя в проде дыра.
RBAC отвечает на вопрос "кто и что может делать" (create/get/delete на ресурсе), но он слеп к содержимому. Ему всё равно, privileged там контейнер или нет, latest у тебя тег или конкретная версия, есть ли limits. Вот этот пробел - "запрос разрешён, но его содержимое опасно" - и закрывает admission control. Это последний рубеж между API-запросом и записью в etcd, где можно сказать "нет" или тихо подправить манифест. В этом уроке разберём admission controller kubernetes по слоям: как устроена цепочка, чем mutating отличается от validating, как работает pod security admission на namespace-метках вместо удалённого PodSecurityPolicy, и почему в 2026 связка validatingadmissionpolicy + CEL начинает вытеснять вебхуки. Это фундамент темы kubernetes security, без которого policy-as-code дальше не построить.

Механика: где живёт admission в пути запроса
Любой запрос к kube-apiserver (kubectl apply, контроллер, оператор - неважно) проходит конвейер строго по порядку:
- Authentication - кто ты (сертификат, токен, OIDC).
- Authorization - что тебе можно (RBAC, Node, Webhook).
- Admission - можно ли пропустить именно ЭТОТ объект, и не надо ли его подправить.
- Schema validation и запись в etcd.
- Mutating фаза - контроллеры, которые МЕНЯЮТ объект. Проставляют дефолты, инжектят sidecar, добавляют лейблы, подставляют ServiceAccount-токен. Сюда же относится MutatingAdmissionWebhook.
- Validating фаза - контроллеры, которые только ПРОВЕРЯЮТ и либо пропускают, либо отклоняют запрос с ошибкой. Менять объект они уже не могут. Сюда относятся ValidatingAdmissionWebhook, ValidatingAdmissionPolicy и сам Pod Security Admission.
Важная тонкость про идемпотентность mutating-вебхуков. Apiserver гарантирует, что в одной фазе вебхуки вызываются, но при ре-инвокации (reinvocationPolicy: IfNeeded) твой вебхук могут позвать повторно, если другой вебхук после него поменял объект. Поэтому мутация обязана быть идемпотентной: добавляешь sidecar - сначала проверь, что его ещё нет. Классические грабли - оператор service mesh, который при двойном вызове инжектит два sidecar-контейнера в один Pod.
Pod Security Admission: профили на namespace вместо PodSecurityPolicy
PodSecurityPolicy (PSP) удалили в Kubernetes 1.25 - он был сложным, легко ломался на порядке привязок и плохо масштабировался. На замену пришёл встроенный контроллер Pod Security Admission, который реализует три уровня Pod Security Standards:
- privileged - не ограничивает ничего. Для системных нагрузок (CNI, CSI, агенты мониторинга), которым реально нужен доступ к ноде.
- baseline - запрещает явно опасное: privileged-контейнеры, hostNetwork, hostPID, hostIPC, hostPath, добавление опасных capabilities. Это разумный минимум для прикладных namespace.
- restricted - жёсткий профиль по best practices: runAsNonRoot, запрет allowPrivilegeEscalation, seccompProfile: RuntimeDefault, drop ALL capabilities, только проектные тома. Для security-sensitive приложений.
- enforce - нарушающий Pod отклоняется. Жёстко.
- audit - Pod создаётся, но в audit-лог пишется нарушение.
- warn - Pod создаётся, но клиенту (тебе в kubectl) возвращается warning.
Код: Выделить всё
apiVersion: v1
kind: Namespace
metadata:
name: payments
labels:
pod-security.kubernetes.io/enforce: restricted
pod-security.kubernetes.io/enforce-version: v1.31
pod-security.kubernetes.io/audit: restricted
pod-security.kubernetes.io/warn: restricted
Теперь практика. Создадим namespace и попробуем запустить заведомо плохой Pod:
Код: Выделить всё
$ kubectl label ns payments pod-security.kubernetes.io/enforce=restricted
$ kubectl -n payments run bad --image=nginx
Error from server (Forbidden): pods "bad" is forbidden: violates PodSecurity "restricted:latest":
allowPrivilegeEscalation != false (container "bad" must set securityContext.allowPrivilegeEscalation=false),
unrestricted capabilities (container "bad" must set securityContext.capabilities.drop=["ALL"]),
runAsNonRoot != true (pod or container "bad" must set securityContext.runAsNonRoot=true),
seccompProfile (pod or container "bad" must set securityContext.seccompProfile.type to "RuntimeDefault" or "Localhost")
Код: Выделить всё
spec:
securityContext:
runAsNonRoot: true
seccompProfile:
type: RuntimeDefault
containers:
- name: app
image: nginx:1.27
securityContext:
allowPrivilegeEscalation: false
capabilities:
drop: ["ALL"]
Грабли PSA: контроллер работает на уровне namespace и проверяет именно Pod-шаблон. Если разработчик создаёт Deployment с плохим шаблоном, PSA на этапе создания Deployment выдаст только warning (контроллер реплик создаст Pod уже от имени системного сервис-аккаунта), а реальное отклонение случится при попытке поднять Pod - и ты увидишь это в событиях ReplicaSet, а не в выводе kubectl apply. Поэтому warn так ценен: он подсвечивает проблему сразу человеку.
Где PSA не справляется и нужен policy-as-code
Pod Security Admission намеренно простой: три фиксированных уровня, и всё. А что если тебе нужно "запретить тег latest", "разрешить образы только из registry.company.ru", "требовать лейбл team на каждом Deployment", "запретить external LoadBalancer Service"? PSA это не умеет в принципе - он про securityContext Pod, и точка. Тут два пути: ValidatingAdmissionPolicy (нативный, на CEL) или внешние движки policy-as-code (Kyverno, OPA Gatekeeper) поверх admission-вебхуков.
ValidatingAdmissionPolicy: политики на CEL без вебхуков
До недавнего времени любая кастомная политика означала свой admission-вебхук: отдельный под, TLS-сертификат, сетевой хоп на каждый запрос к apiserver, точка отказа. Упал вебхук с failurePolicy: Fail - и apiserver не может создавать поды вообще. В Kubernetes 1.30 в стабильную версию (GA) вышел ValidatingAdmissionPolicy - механизм, который выполняет проверки прямо ВНУТРИ apiserver на языке выражений CEL (Common Expression Language). Никакого пода, никакого TLS, никакого сетевого хопа. apiVersion - admissionregistration.k8s.io/v1.
Связка из двух объектов. Сама политика описывает ЧТО проверять:
Код: Выделить всё
apiVersion: admissionregistration.k8s.io/v1
kind: ValidatingAdmissionPolicy
metadata:
name: disallow-latest-tag
spec:
failurePolicy: Fail
matchConstraints:
resourceRules:
- apiGroups: ["apps"]
apiVersions: ["v1"]
operations: ["CREATE", "UPDATE"]
resources: ["deployments"]
validations:
- expression: >-
object.spec.template.spec.containers.all(c, !c.image.endsWith(':latest') && c.image.contains(':'))
message: "образ обязан иметь явный тег, latest и образы без тега запрещены"
Код: Выделить всё
apiVersion: admissionregistration.k8s.io/v1
kind: ValidatingAdmissionPolicyBinding
metadata:
name: disallow-latest-tag-binding
spec:
policyName: disallow-latest-tag
validationActions: ["Deny"]
matchResources:
namespaceSelector:
matchLabels:
environment: production
Код: Выделить всё
$ kubectl apply -f deploy-latest.yaml
The deployments "web" is invalid: ValidatingAdmissionPolicy 'disallow-latest-tag'
with binding 'disallow-latest-tag-binding' denied request:
образ обязан иметь явный тег, latest и образы без тега запрещены
Admission webhook: когда нужна логика снаружи
Webhook admission - это твой HTTP-сервер, который apiserver дёргает на каждый подходящий запрос. Регистрируется через ValidatingWebhookConfiguration или MutatingWebhookConfiguration. Apiserver шлёт AdmissionReview с объектом, сервер возвращает allowed: true/false (и patch для mutating). Именно так работают Kyverno и Gatekeeper - это готовые вебхук-движки, где политики ты пишешь декларативно (Kyverno - YAML, Gatekeeper - Rego), а не кодишь сервер руками.
Два параметра, на которых горят все:
- failurePolicy. Fail - если вебхук недоступен, запрос ОТКЛОНЯЕТСЯ. Ignore - запрос ПРОПУСКАЕТСЯ. Поставишь Fail на критичный namespace и уронишь вебхук - перестанут создаваться все поды, включая сам вебхук, если он там же. Классический deadlock.
- namespaceSelector / objectSelector. ОБЯЗАТЕЛЬНО исключай kube-system и namespace самого вебхука, иначе при рестарте кластера ничего не поднимется.
- enforce restricted сразу на боевой namespace без фазы warn/audit. Результат - массовое падение подов. Сначала наблюдай, потом запрещай.
- Не запиннена enforce-version у PSA. После апгрейда кластера стандарт ужесточается, вчерашние рабочие поды отклоняются. Всегда указывай версию.
- failurePolicy: Fail на вебхуке, который сам крутится в проверяемом namespace - circular dependency, кластер не стартует с нуля.
- Неидемпотентный mutating-вебхук - двойная инъекция sidecar при ре-инвокации.
- Расчёт, что PSA отклонит плохой Deployment в kubectl apply. Нет - отклонение случится на Pod внутри ReplicaSet, ищи в событиях, не в выводе apply.
- Один гигантский вебхук на все ресурсы вместо узких matchConstraints. Лишняя нагрузка на каждый запрос и риск общего отказа.
- Дублирование: PSA уже закрывает privileged/hostNetwork - не пиши то же самое ещё раз в Kyverno. Используй слои по назначению.
На любом кластере (подойдёт kind, k3s, Minikube или Yandex Managed Kubernetes):
- Создай namespace lab и повесь pod-security.kubernetes.io/warn=restricted и audit=restricted (пока без enforce).
- Запусти kubectl -n lab run test --image=nginx и поймай warning в выводе - прочитай, какие поля нарушены.
- Перепиши Pod в YAML с корректным securityContext (runAsNonRoot, drop ALL, allowPrivilegeEscalation: false, seccompProfile RuntimeDefault) и убедись, что warning исчез.
- Переключи warn на enforce и проверь, что плохой Pod теперь отклоняется с 403.
- Создай ValidatingAdmissionPolicy disallow-latest-tag и Binding к namespace lab. Попробуй задеплоить Deployment с image: nginx:latest - получи Deny.
- Поменяй в Binding validationActions с ["Deny"] на ["Warn"] и убедись, что теперь только предупреждение - так выглядит мягкое внедрение политики.
- Почему mutating-фаза admission идёт строго ДО validating, и что сломается при обратном порядке?
- Чем pod security admission на namespace-лейблах принципиально отличается от удалённого PodSecurityPolicy, и зачем нужно поле enforce-version?
- В каких случаях достаточно ValidatingAdmissionPolicy на CEL, а когда придётся брать admission webhook (Kyverno/Gatekeeper)?
- Почему failurePolicy: Fail на вебхуке в kube-system может полностью заблокировать запуск кластера?
Admission - последний шлюз перед etcd, где кластер решает, пускать объект и в каком виде. Mutating правит (дефолты, sidecar), validating отклоняет. Pod Security Admission даёт три готовых профиля (privileged/baseline/restricted) прямо на лейблах namespace - это замена PodSecurityPolicy и базовый уровень kubernetes security. Для кастомных правил в 2026 первым делом смотри на validatingadmissionpolicy с CEL - он работает внутри apiserver без вебхуков и точек отказа. А когда нужна сложная логика, мутация или генерация - переходишь к policy-as-code: Kyverno и OPA Gatekeeper. В следующем уроке как раз погрузимся в Kyverno и напишем боевой набор политик.