Admission controllers: mutating и validating
Между моментом, когда apiserver принял запрос от kubectl, и моментом записи в etcd, есть фаза, в которой объект можно изменить (mutating) или отвергнуть (validating). Это admission control — третий и последний барьер после authentication и authorization. Через admission в Kubernetes реализованы десятки критических вещей: автомонтирование SA-токенов, defaults LimitRange, enforcement Pod Security Standards, sidecar injection Istio/Linkerd, policy engines (Kyverno, OPA). Понимать admission — обязательно для любой production-системы.
Анатомия HTTP-запроса: что внутри request и response
Где admission стоит в request lifecycle
Жизненный цикл API request на apiserver:
- Authentication — кто это? (mTLS cert, bearer token, OIDC token, ServiceAccount token). Если неизвестный →
401 Unauthorized. - Authorization — может ли он делать это действие? (RBAC, ABAC, Node authorizer). Если нет →
403 Forbidden. - Mutating admission — можно ли модифицировать запрос? (built-in MutatingAdmissionWebhook + новая MutatingAdmissionPolicy на CEL — alpha с v1.32). Этот этап МЕНЯЕТ объект.
- Object schema validation — соответствует ли OpenAPI schema (для CRDs) / built-in типу.
- Validating admission — финальная проверка после всех мутаций (built-in ValidatingAdmissionWebhook + ValidatingAdmissionPolicy). Может отвергнуть, но не менять.
- Запись в etcd.
Mutating всегда запускается ПЕРЕД validating, и не наоборот. Это критический инвариант: validating видит финальный объект после всех мутаций, поэтому может проверить consistency после изменений. Если бы validating шёл раньше, mutating мог бы испортить уже одобренный объект.
Built-in admission controllers
apiserver запускается с флагом --enable-admission-plugins (и --disable-admission-plugins). Список встроенных — около 30 штук, многие включены по умолчанию. Это in-process controllers, написанные в коде apiserver.
| Controller | Mut / Val | Что делает |
|---|---|---|
NamespaceLifecycle | V | Block создание объектов в not-existing или terminating namespaces. Прозрачно защищает от orphan state. |
LimitRanger | M+V | Применяет defaults из LimitRange в namespace (resources.requests/limits), валидирует, не превышает ли spec лимиты. |
ResourceQuota | V | Считает, не превысит ли создание этого объекта ResourceQuota namespace-а. |
ServiceAccount | M+V | Auto-mount ServiceAccount tokens в Pod (projected volume). Создаёт default SA. Подставляет imagePullSecrets. |
PodSecurity (с v1.25) | V | Enforces Pod Security Standards: privileged / baseline / restricted на основе labels namespace-а. Замена deprecated PSP. |
DefaultStorageClass | M | Если PVC создаётся без storageClassName и есть default StorageClass — подставляет его. |
NodeRestriction | V | kubelet может modify только свои Node и Pods на ней. Защищает от compromised kubelet. |
RuntimeClass | V | Validates RuntimeClass references в Pods. |
TaintNodesByCondition | M | Авто-taint Node при изменении conditions (NotReady, MemoryPressure). |
Priority | M+V | Resolve PriorityClass → integer priority. |
EventRateLimit | V | Throttle Events создание (rate limit для шумных контроллеров). |
MutatingAdmissionWebhook | M | Запускает все external mutating webhooks (см. ниже). |
ValidatingAdmissionWebhook | V | Запускает все external validating webhooks. |
CertificateApproval, CertificateSigning | V | Внутренние проверки workflow CertificateSigningRequest. |
Чтобы увидеть, какие admission plugins включены в кластере, можно посмотреть kubectl -n kube-system get pod kube-apiserver-<node> -o yaml | grep enable-admission-plugins. На managed-кластере (EKS/GKE) этот флаг скрыт provider-ом, но обычно включены все рекомендованные.
Mutating vs Validating: разница
- Mutating — может изменить объект через JSON Patch. Запускается первым.
- Inject sidecar (Istio Envoy, Linkerd proxy).
- Set defaults (LimitRange defaults, default StorageClass).
- Inject labels / annotations (e.g. cost-center attribution).
- Replace image tag через image policy (e.g.
:latest→:v1.2.3).
- Validating — только говорит accept или reject. Не меняет объект. Запускается после всех mutating.
- Enforce policy: «все Pods должны иметь resource limits», «нельзя использовать
:latestimage». - Verify cross-field constraints, которые не выражаются в OpenAPI schema.
- Custom security checks: image signature verification, hash, registry whitelist.
- Enforce policy: «все Pods должны иметь resource limits», «нельзя использовать
Архитектурное последствие: mutating webhook может выпустить broken объект, если он сам сделал ошибку (например, добавил неверный label). Validating сразу после него поймает problem и отвергнет. Поэтому правило: mutating должен быть консервативным, validating — гарантирует invariants.
Webhook admission controllers: расширение через HTTP endpoint
Built-in admission хорошо, но что если нужна custom logic? Например, «все Deployment должны иметь label team: <team-name>, и this label должен матчить registered team в нашей системе». Это нельзя выразить через built-in admission. Для этого есть webhook admission — extensibility через HTTP endpoint.
Регистрация — через MutatingWebhookConfiguration / ValidatingWebhookConfiguration objects:
apiVersion: admissionregistration.k8s.io/v1
kind: ValidatingWebhookConfiguration
metadata:
name: enforce-team-label
webhooks:
- name: team-label.example.com
clientConfig:
service: # webhook — это Service в кластере
name: team-policy-webhook
namespace: policy-system
path: /validate
port: 443
caBundle: <base64 CA cert> # для mTLS apiserver → webhook
rules:
- operations: [CREATE, UPDATE]
apiGroups: [apps]
apiVersions: [v1]
resources: [deployments]
failurePolicy: Fail # если webhook не отвечает — REJECT request
sideEffects: None
admissionReviewVersions: [v1]
matchConditions: # v1.30+ CEL prefilter
- name: exclude-system-ns
expression: 'object.metadata.namespace != "kube-system"'
timeoutSeconds: 5
namespaceSelector: # ограничить scope namespace-ами
matchLabels: {policy-enforced: "true"}
Сам webhook — это HTTP endpoint, обычно Pod в кластере (Service). apiserver делает POST на endpoint с AdmissionReview запросом (JSON с объектом, operation, user info), webhook возвращает AdmissionReview ответ с allowed: true/false + patches (для mutating).
failurePolicy: Fail — критичная настройка. Если webhook Pod упал или сетевой timeout, apiserver не может получить ответ. С failurePolicy: Fail (по умолчанию для validating) все CREATE Deployments будут блокироваться по всему кластеру, пока webhook не починят. Это часто становится outage incident-ом: deploy CI/CD блокируется, scaling блокируется. С failurePolicy: Ignore рискуете bypass policy, но кластер живёт. Production-best practice: использовать failurePolicy: Fail плюс namespaceSelector с явным excludelist для kube-system, cert-manager, и т.д. — иначе ловите cycles при первом upgrade.
ValidatingAdmissionPolicy: CEL без webhook (v1.30 GA)
ValidatingAdmissionPolicy (VAP) — новая фича Kubernetes (GA в v1.30), которая позволяет писать validation policies через CEL expressions прямо в apiserver, без external webhook service. Это in-tree evaluator — быстрее, дешевле, нет network roundtrip, нет caBundle, нет service mesh.
apiVersion: admissionregistration.k8s.io/v1
kind: ValidatingAdmissionPolicy
metadata:
name: require-resource-limits
spec:
failurePolicy: Fail
matchConstraints:
resourceRules:
- apiGroups: [""]
apiVersions: [v1]
operations: [CREATE, UPDATE]
resources: [pods]
validations:
- expression: |
object.spec.containers.all(c,
has(c.resources.limits) &&
has(c.resources.limits.cpu) &&
has(c.resources.limits.memory)
)
message: "All containers must have CPU and memory limits"
reason: Forbidden
---
apiVersion: admissionregistration.k8s.io/v1
kind: ValidatingAdmissionPolicyBinding
metadata:
name: require-resource-limits-binding
spec:
policyName: require-resource-limits
validationActions: [Deny]
matchResources:
namespaceSelector:
matchLabels: {policy: enforce}
VAP — две сущности: ValidatingAdmissionPolicy (политика, переиспользуемая) и ValidatingAdmissionPolicyBinding (применение к scope). С v1.32 появилась mutating версия MutatingAdmissionPolicy (beta) — defaults через CEL.
Когда выбрать VAP, когда webhook:
- VAP — простые правила, выразимые на CEL: проверки наличия полей, regex, comparisons. Быстрее, надёжнее (нет внешнего сервиса).
- Webhook — сложная логика: нужно вызвать внешний API (e.g. registry trust), хранить state, делать ML inference, integrate с CMDB.
Kyverno и OPA Gatekeeper: policy engines
Писать webhooks с нуля для каждой policy утомительно. Существуют policy engines, которые ставятся как операторы и предоставляют декларативный язык policies:
- Kyverno — Kubernetes-native, YAML-based policy language. Не требует CEL/Rego. Policies — это CR (
ClusterPolicy,Policy). Поддерживает validate, mutate, generate (auto-create ConfigMap при namespace создании), verifyImages (cosign signatures). Стандарт de-facto в 2024-2026. - OPA Gatekeeper — wrapper над Open Policy Agent. Policies на Rego (более мощный, но steeper learning curve). Templates (
ConstraintTemplate) + Constraints. Хорош для compliance-driven orgs.
Оба работают через ValidatingAdmissionWebhook (и MutatingAdmissionWebhook для Kyverno mutate). С точки зрения K8s — обычные webhooks; уникальность в developer experience выше webhook protocol.
Пример Kyverno policy:
apiVersion: kyverno.io/v1
kind: ClusterPolicy
metadata:
name: require-labels
spec:
validationFailureAction: Enforce # или Audit
rules:
- name: require-team-label
match:
any:
- resources:
kinds: [Deployment, StatefulSet, DaemonSet]
validate:
message: "Workload must have label 'team'"
pattern:
metadata:
labels:
team: "?*" # любое непустое значение
PodSecurity admission — встроенная замена PSP
С v1.25 в kube-apiserver включён PodSecurity admission — реализация Pod Security Standards через built-in admission. Это замена deprecated PodSecurityPolicy (PSP), которая была удалена в v1.25.
Работает через namespace labels:
apiVersion: v1
kind: Namespace
metadata:
name: production
labels:
pod-security.kubernetes.io/enforce: restricted
pod-security.kubernetes.io/enforce-version: latest
pod-security.kubernetes.io/audit: restricted # log violation
pod-security.kubernetes.io/warn: restricted # warning в kubectl output
Три levels: privileged (no restrictions), baseline (запрет очевидно опасных вещей), restricted (hardened defaults). Три actions: enforce (block), audit (audit log), warn (warning).
Подробно про PSS — в модуле 14 «Security». Здесь важно понимать: PSS — это admission. Это не «отдельный механизм», это плагин в общей admission цепочке.
CKAD scope
CKAD требует концептуального понимания admission:
- Знать, что admission — это фаза перед записью в etcd.
- Понимать разницу mutating и validating.
- Знать про Pod Security Standards — это admission, и он работает через namespace labels.
- Опознавать, что данный YAML — это
MutatingWebhookConfiguration/ValidatingAdmissionPolicy. - НЕ требуется писать custom webhooks или CEL expressions.
# Discovery в кластере
kubectl get mutatingwebhookconfigurations
kubectl get validatingwebhookconfigurations
kubectl get validatingadmissionpolicy
# Если что-то блокируется — посмотреть, какие webhooks применяются
kubectl get validatingwebhookconfigurations -o yaml | less
Killer-моменты
- Admission — это in-cluster middleware для всех API requests, после authn/authz перед etcd. Любая полезная политика в кластере проходит через admission.
- Mutating всегда до validating. Validating видит финальное состояние объекта.
failurePolicy: Failдля validating webhook = single point of failure. Если webhook Pod упадёт — все matching requests блокируются. Тщательный namespaceSelector обязателен.- ValidatingAdmissionPolicy (CEL) — будущее policy. In-tree, без external service, без caBundle, без deployment. Используйте VAP вместо webhook когда возможно.
- Sidecar injection (Istio, Linkerd) — это mutating webhook. Просто observation: «в namespace с label
istio-injection: enabledкаждый Pod на create получает sidecar contaier» — это classic admission webhook pattern. - PSP мёртв с v1.25. PodSecurity admission заменил его — проще, через namespace labels.