закреплять стандарты кластера до записи объектов в etcd

Admission controllers в Kubernetes: политики и управление конфигурацией

13 минут

Плохие манифесты попадают в etcd, если на пути admission ничего нет. Сочетайте ValidatingAdmissionPolicy на CEL, движки политик и аккуратно обслуживаемые mutating/validating webhook — с выкатом Ignore→Fail, селекторами namespace и TLS через cert-manager — чтобы дефолты и запреты срабатывали до сохранения.

Почему ошибочные конфигурации всё равно попадают в кластер

Многие инциденты, которые списывают на «странности Kubernetes», начинаются с манифестов, которые не должны были пройти: без лимитов CPU и памяти, от root, с hostPath, с образами из случайных реестров, без меток владельца или cost-center. Вики и код-ревью не отклоняют create. Встроенные контроллеры вроде LimitRanger и NamespaceLifecycle помогают, но не кодируют ваш allowlist реестров или требование probes. Admission — узкое место между API-сервером и etcd после аутентификации и авторизации: здесь задают дефолты и проверяют политики до записи. Без осознанной стратегии admission каждый namespace изобретает свои правила, а аудит превращается в раскопки.

Цепочка admission в правильном порядке

Сначала запрос проходит аутентификацию и авторизацию (RBAC или другой authorizer). Только потом работает admission. Mutating admission может изменить объект — подставить дефолты, метки или sidecar. Далее — проверка схемы объекта. Затем validating admission принимает или отклоняет итоговый объект; validating webhook и ValidatingAdmissionPolicy работают на этом этапе и не должны мутировать. Если все проверки прошли, объект пишется в etcd. Разделяйте роли: mutate — безопасные дефолты, validate — жёсткие запреты. Не оставляйте бизнес-политики только в CI — и GitOps, и kubectl при правильной настройке бьют в один и тот же путь admission.

Выбор слоя принуждения: CEL, движки или свои webhook

Берите самый лёгкий инструмент, который решает задачу. ValidatingAdmissionPolicy считает CEL внутри API-сервера — без пода webhook, с меньшей задержкой, GA с Kubernetes 1.30. Удобен для обязательных меток, потолка реплик и простых проверок securityContext. Kyverno или OPA Gatekeeper — когда нужны богатая мутация, generate, проверка подписи образов, отчёты аудита или Rego за пределами кластера; эти движки всё равно регистрируются как admission webhook. Свой webhook пишите только если логику не выразить CEL и движком или нужно вызвать внутренний сервис на пути admission. Свой код — это TLS, доступность и риск обновлений; большинство платформ должны исчерпать VAP и движок политик, прежде чем выкатывать обработчики на Go.

Нативная проверка через ValidatingAdmissionPolicy

Опишите логику политики один раз, затем привяжите её через ValidatingAdmissionPolicyBinding и validationActions: Deny, Warn или Audit. Начните с Audit или Warn на помеченных namespace, поправьте чарты, затем переключите на Deny. failurePolicy Fail включайте только после проверки выражений — ошибочный CEL с Fail блокирует подходящие запросы. Исключайте kube-system и namespace контроллеров из matchConstraints.

YAML · ValidatingAdmissionPolicy: non-root и метка team
apiVersion: admissionregistration.k8s.io/v1
kind: ValidatingAdmissionPolicy
metadata:
  name: workload-baseline
spec:
  failurePolicy: Fail
  matchConstraints:
    resourceRules:
      - apiGroups: [""]
        apiVersions: ["v1"]
        operations: ["CREATE", "UPDATE"]
        resources: ["pods"]
  validations:
    - expression: "object.metadata.labels.?team.hasValue()"
      message: "Pod must have label team"
    - expression: >-
        object.spec.containers.all(c,
          c.?securityContext.?runAsNonRoot == optional.of(true) ||
          c.?securityContext.?runAsUser.orValue(0) > 0)
      message: "Containers must not run as root"
---
apiVersion: admissionregistration.k8s.io/v1
kind: ValidatingAdmissionPolicyBinding
metadata:
  name: workload-baseline-apps
spec:
  policyName: workload-baseline
  validationActions: ["Deny"]
  matchResources:
    namespaceSelector:
      matchLabels:
        admission.policy/enforce: "true"

Mutating webhook для дефолтов — обслуживайте как продакшен-сервис

Если мутации не хватает LimitRange и mutator'ов движка, зарегистрируйте MutatingWebhookConfiguration с sideEffects None, явным timeoutSeconds и opt-in через namespaceSelector. TLS отдавайте через cert-manager и инжектируйте CA в caBundle аннотацией, чтобы ротация не отрезала API-сервер. На canary-namespace начните с failurePolicy Ignore; на Fail переходите только при двух репликах, пробах и дашбордах. reinvocationPolicy IfNeeded — если поздние mutator'ы требуют второго прохода. Обработчики делайте идемпотентными — не добавляйте второй sidecar на UPDATE. Лучше один процесс с /mutate и /validate на одном порту, чем два контейнера, конфликтующих за 8443.

YAML · MutatingWebhookConfiguration с opt-in namespace
apiVersion: admissionregistration.k8s.io/v1
kind: MutatingWebhookConfiguration
metadata:
  name: config-defaults
  annotations:
    cert-manager.io/inject-ca-from: platform-system/webhook-tls
webhooks:
  - name: defaults.platform.example.com
    admissionReviewVersions: ["v1"]
    sideEffects: None
    timeoutSeconds: 5
    failurePolicy: Ignore
    matchPolicy: Equivalent
    reinvocationPolicy: IfNeeded
    clientConfig:
      service:
        name: admission-webhooks
        namespace: platform-system
        path: /mutate
        port: 443
    rules:
      - apiGroups: [""]
        apiVersions: ["v1"]
        operations: ["CREATE"]
        resources: ["pods"]
        scope: Namespaced
    namespaceSelector:
      matchLabels:
        admission-config-defaults: "enabled"
    objectSelector:
      matchExpressions:
        - key: admission.policy/skip
          operator: DoesNotExist
YAML · Certificate cert-manager для TLS webhook
apiVersion: cert-manager.io/v1
kind: Certificate
metadata:
  name: webhook-tls
  namespace: platform-system
spec:
  secretName: webhook-tls
  duration: 2160h
  renewBefore: 360h
  issuerRef:
    name: webhook-issuer
    kind: Issuer
  usages: ["server auth"]
  dnsNames:
    - admission-webhooks.platform-system.svc
    - admission-webhooks.platform-system.svc.cluster.local

Validating-запреты: реестры, probes и privileged

Жёсткие правила выражайте как validation — CEL, Kyverno/Gatekeeper или validating webhook. Типовой минимум: запрет privileged и hostPath, обязательный readiness у подов публичных Service, образы только из корпоративного реестра. Не считайте docker.io/library доверенным по умолчанию в продакшене. Проверяйте через kubectl --dry-run=server, чтобы admission отработал без записи. Если свой validating webhook неизбежен, возвращайте Allowed false с понятным Message; логируйте пользователя, namespace, GVK и решение для аудита и SIEM.

YAML · каркас ValidatingWebhookConfiguration
apiVersion: admissionregistration.k8s.io/v1
kind: ValidatingWebhookConfiguration
metadata:
  name: policy-validate
  annotations:
    cert-manager.io/inject-ca-from: platform-system/webhook-tls
webhooks:
  - name: validate.platform.example.com
    admissionReviewVersions: ["v1"]
    sideEffects: None
    timeoutSeconds: 5
    failurePolicy: Ignore
    clientConfig:
      service:
        name: admission-webhooks
        namespace: platform-system
        path: /validate
        port: 443
    rules:
      - apiGroups: [""]
        apiVersions: ["v1"]
        operations: ["CREATE", "UPDATE"]
        resources: ["pods"]
    namespaceSelector:
      matchLabels:
        admission.policy/enforce: "true"
Bash · проверка admission через server-side dry-run
#!/usr/bin/env bash
set -euo pipefail
kubectl label namespace app-team admission.policy/enforce=true --overwrite
kubectl apply --dry-run=server -f bad-pod-privileged.yaml
# Expect: admission webhook or ValidatingAdmissionPolicy denial
kubectl apply --dry-run=server -f good-pod.yaml

Обслуживайте admission как сервис на критическом пути

Задержка admission — это задержка API: алертируйте на P95 webhook и долю ошибок, а также на метрики apiserver_admission_webhook_* у API-сервера. Держите у Deployment webhook минимум две реплики и PodDisruptionBudget. Не включайте Fail на весь кластер в первый день — расширяйте namespaceSelector постепенно. Версионируйте политики в Git рядом с конфигом кластера и ревьюйте как код приложений. Согласуйте с мультитенантными квотами, чтобы LimitRange, ResourceQuota и отказы admission рассказывали одну историю. Admission controllers — последний шлюз перед состоянием кластера: предпочитайте нативный CEL и зрелые движки, свои webhook оставляйте для настоящих исключений, а Ignore поднимайте до Fail только когда наблюдаемость подтверждает, что путь безопасен.

Библиотеки политик Kyverno и Gatekeeper работают на том же пути admission из гайда по Policy as Code с OPA Gatekeeper и Kyverno.

Admission-контроль продолжает базовую линию из гайда по ужесточению безопасности Kubernetes для production-кластеров.