Admission Webhooks и расширения Kubernetes API
Зачем знать на Middle 3: Admission webhooks — это hooks, через которые проходят все запросы записи в Kubernetes API. На них держится Istio sidecar injection, cert-manager, OPA Gatekeeper, Kyverno, любой serious operator с валидацией. Middle 3 Go-инженер должен уметь спроектировать и реализовать MutatingWebhookConfiguration и ValidatingWebhookConfiguration, правильно обрабатывать TLS, FailurePolicy, timeouts, понимать risks (валящий кластер webhook — это норма), и знать альтернативы (CEL admission policies, Pod Security Admission, Gatekeeper, Kyverno). Это пограничный stack между Go-разработкой и платформенным SRE.
Содержание
Заголовок раздела «Содержание»- Концепция: admission control в k8s
- Production-deep dive: webhooks в Go, OPA Gatekeeper, Kyverno, PSA, ValidatingAdmissionPolicy
- Gotchas (10+)
- Real cases: Istio injector, cert-manager, OPA Gatekeeper
- Вопросы (20+)
- Practice
- Источники
1. Концепция
Заголовок раздела «1. Концепция»1.1 Жизненный цикл API-запроса
Заголовок раздела «1.1 Жизненный цикл API-запроса»Любой kubectl apply / API-запрос проходит через цепочку:
Client (kubectl, controller, operator) │ ▼ ┌─────────────────────────────────────────────────────┐ │ kube-apiserver pipeline │ │ │ │ 1. Authentication (kubeconfig, ServiceAccount, │ │ OIDC, webhook token) │ │ │ │ │ ▼ │ │ 2. Authorization (RBAC, ABAC, Webhook) │ │ │ │ │ ▼ │ │ 3. MUTATING admission webhooks │ │ (modify object: add labels, sidecars, │ │ set defaults) │ │ │ │ │ ▼ │ │ 4. OpenAPI schema validation │ │ │ │ │ ▼ │ │ 5. VALIDATING admission webhooks │ │ (accept / reject) │ │ │ │ │ ▼ │ │ 6. Storage (etcd) │ └─────────────────────────────────────────────────────┘Каждый webhook — это HTTPS-сервер, который принимает AdmissionReview и возвращает AdmissionReview с решением (allow/reject + опциональный JSON Patch).
1.2 Mutating vs Validating
Заголовок раздела «1.2 Mutating vs Validating»- Mutating — может изменить объект (через JSONPatch). Используется для: добавления labels/annotations, default-значений, инжекции sidecar-контейнеров.
- Validating — только говорит «allow / reject». Используется для: enforcement политик («все Pod’ы должны иметь requests/limits»).
Порядок: сначала все mutating webhook’и, потом schema validation, потом все validating webhook’и. Этот порядок гарантирует, что validating видит финальный объект.
1.3 Где запускаются webhook’и
Заголовок раздела «1.3 Где запускаются webhook’и»Webhook — это HTTPS endpoint, доступный из kube-apiserver:
- In-cluster — webhook бежит как Pod, выставлен через Service. URL:
https://service.namespace.svc:443/path. Так делают 95% операторов. - External — внешний URL (HTTPS), например, SaaS-провайдер политики. Используется реже из-за availability и latency.
API-сервер коннектится по TLS, поэтому нужен caBundle в манифесте MutatingWebhookConfiguration / ValidatingWebhookConfiguration.
1.4 AdmissionReview structure
Заголовок раздела «1.4 AdmissionReview structure»apiVersion: admission.k8s.io/v1kind: AdmissionReviewrequest: uid: 1234-abcd kind: { group: "", version: "v1", kind: "Pod" } resource: { group: "", version: "v1", resource: "pods" } namespace: "default" name: "" operation: "CREATE" # CREATE/UPDATE/DELETE/CONNECT userInfo: { username: "...", groups: [...] } object: { ... } # текущий объект oldObject: { ... } # старый объект (на UPDATE/DELETE) dryRun: falseОтвет от webhook’а:
apiVersion: admission.k8s.io/v1kind: AdmissionReviewresponse: uid: 1234-abcd allowed: true patchType: JSONPatch patch: "<base64-encoded JSON patch>" warnings: ["deprecated label: x"] status: message: "rejected: missing label app" code: 4222. Production-deep dive
Заголовок раздела «2. Production-deep dive»2.1 Mutating webhook в Go (controller-runtime)
Заголовок раздела «2.1 Mutating webhook в Go (controller-runtime)»controller-runtime даёт встроенный webhook server. Подключение через kubebuilder:
kubebuilder create webhook --group apps --version v1 --kind Deployment --defaulting --programmatic-validationПолучаем:
package v1
import ( appsv1 "k8s.io/api/apps/v1" ctrl "sigs.k8s.io/controller-runtime" "sigs.k8s.io/controller-runtime/pkg/webhook" "sigs.k8s.io/controller-runtime/pkg/webhook/admission")
// +kubebuilder:webhook:path=/mutate-apps-v1-deployment,mutating=true,failurePolicy=fail,sideEffects=None,groups=apps,resources=deployments,verbs=create;update,versions=v1,name=mdeploy.example.com,admissionReviewVersions=v1
type DeploymentMutator struct{}
func (m *DeploymentMutator) Default(ctx context.Context, obj runtime.Object) error { d := obj.(*appsv1.Deployment)
if d.Labels == nil { d.Labels = map[string]string{} } if _, ok := d.Labels["managed-by"]; !ok { d.Labels["managed-by"] = "platform" }
// Default-значения для resources. for i := range d.Spec.Template.Spec.Containers { c := &d.Spec.Template.Spec.Containers[i] if c.Resources.Requests == nil { c.Resources.Requests = corev1.ResourceList{ corev1.ResourceCPU: resource.MustParse("100m"), corev1.ResourceMemory: resource.MustParse("128Mi"), } } } return nil}
func SetupDeploymentWebhookWithManager(mgr ctrl.Manager) error { return ctrl.NewWebhookManagedBy(mgr). For(&appsv1.Deployment{}). WithDefaulter(&DeploymentMutator{}). Complete()}2.2 Validating webhook
Заголовок раздела «2.2 Validating webhook»// +kubebuilder:webhook:path=/validate-apps-v1-deployment,mutating=false,failurePolicy=fail,sideEffects=None,groups=apps,resources=deployments,verbs=create;update,versions=v1,name=vdeploy.example.com,admissionReviewVersions=v1
type DeploymentValidator struct{}
func (v *DeploymentValidator) ValidateCreate(ctx context.Context, obj runtime.Object) (admission.Warnings, error) { d := obj.(*appsv1.Deployment)
for _, c := range d.Spec.Template.Spec.Containers { if c.Resources.Limits == nil { return nil, fmt.Errorf("container %q: resources.limits is required", c.Name) } if c.Image == "" || strings.Contains(c.Image, ":latest") { return nil, fmt.Errorf("container %q: image must be pinned (no :latest)", c.Name) } } return nil, nil}
func (v *DeploymentValidator) ValidateUpdate(ctx context.Context, old, new runtime.Object) (admission.Warnings, error) { return v.ValidateCreate(ctx, new)}
func (v *DeploymentValidator) ValidateDelete(ctx context.Context, obj runtime.Object) (admission.Warnings, error) { return nil, nil}2.3 Манифест MutatingWebhookConfiguration
Заголовок раздела «2.3 Манифест MutatingWebhookConfiguration»apiVersion: admissionregistration.k8s.io/v1kind: MutatingWebhookConfigurationmetadata: name: mdeploy.example.com annotations: cert-manager.io/inject-ca-from: platform/webhook-certwebhooks: - name: mdeploy.example.com admissionReviewVersions: ["v1"] sideEffects: None failurePolicy: Fail timeoutSeconds: 5 matchPolicy: Equivalent reinvocationPolicy: IfNeeded namespaceSelector: matchExpressions: - key: kubernetes.io/metadata.name operator: NotIn values: [kube-system, kube-public] objectSelector: matchExpressions: - key: skip-webhook operator: DoesNotExist rules: - apiGroups: ["apps"] apiVersions: ["v1"] operations: ["CREATE", "UPDATE"] resources: ["deployments"] scope: "Namespaced" clientConfig: service: namespace: platform name: webhook-server path: /mutate-apps-v1-deployment port: 443 # caBundle: <PEM, populated by cert-manager>2.4 TLS-сертификаты для webhook’ов
Заголовок раздела «2.4 TLS-сертификаты для webhook’ов»Webhook требует HTTPS. Варианты:
- cert-manager: создаём
Certificateресурс, cert-manager выпускает сертификат и кладёт в Secret. Аннотацияcert-manager.io/inject-ca-fromсообщает cert-manager-у автоматически впрыснутьcaBundleв webhook configuration. - kubebuilder Makefile + cert-manager — стандартный путь.
- Self-signed bootstrap — оператор сам генерит сертификат при старте, патчит
caBundle. Так делают многие легковесные операторы; недостаток — ручная ротация. - CSR API — оператор делает CertificateSigningRequest, ждёт approve.
Хорошая практика — менять сертификаты раз в 90 дней. Cert-manager делает это автоматически.
2.5 FailurePolicy: Ignore vs Fail
Заголовок раздела «2.5 FailurePolicy: Ignore vs Fail»Fail— если webhook не отвечает или возвращает 5xx, запрос отклоняется. Безопаснее в смысле политик, но если ваш webhook упал, никто в кластере не может создавать ресурсы из match-rule.Ignore— если webhook не отвечает, запрос пропускается без mutate/validate. Безопаснее для availability, но политика не применится.
Рекомендация: критичные security-политики — Fail, но с жёсткими namespaceSelector (исключайте kube-system!) и timeout 5s.
2.6 SideEffects, Reinvocation
Заголовок раздела «2.6 SideEffects, Reinvocation»sideEffects:None/NoneOnDryRun/Some/Unknown. Webhook должен заявить, делает ли он side-effects (например, запись во внешнее хранилище).Noneидеален; иначе dry-run сломается.reinvocationPolicy:Never/IfNeeded. Если другой mutating webhook уже изменил объект после вашего, kube-apiserver может вызвать вас повторно.IfNeededнужен, если вы добавляете контейнер, но другой webhook может «оттолкать» его. Идеомпотентность обязательна — иначе при reinvocation вы добавите второй контейнер.
2.7 namespaceSelector / objectSelector
Заголовок раздела «2.7 namespaceSelector / objectSelector»namespaceSelector: matchLabels: istio-injection: enabledobjectSelector: matchExpressions: - key: skip-webhook operator: DoesNotExistЭто критически важно — иначе каждый Pod в кластере проходит через ваш webhook, включая kube-system. Если ваш webhook падает или медленный — кластер падает.
2.8 Pod Security Admission (PSA)
Заголовок раздела «2.8 Pod Security Admission (PSA)»PodSecurityPolicy (PSP) был удалён в 1.25. Замена — Pod Security Admission, встроенный в kube-apiserver (не webhook!). Конфигурация через namespace labels:
apiVersion: v1kind: Namespacemetadata: name: my-app labels: pod-security.kubernetes.io/enforce: restricted pod-security.kubernetes.io/enforce-version: latest pod-security.kubernetes.io/audit: restricted pod-security.kubernetes.io/warn: restrictedТри уровня:
- privileged — без ограничений.
- baseline — запрещены небезопасные опции (hostPath, hostNetwork, privileged: true).
- restricted — строгий: runAsNonRoot, seccompProfile: RuntimeDefault, no capabilities (кроме NET_BIND_SERVICE), readOnlyRootFilesystem (рекомендован).
Действия:
enforce— блокирует Pod при нарушении.audit— пишет audit log.warn— возвращает warning в kubectl.
PSA не заменяет полностью PSP — нет custom политик. Для сложных правил всё ещё нужны Gatekeeper/Kyverno.
2.9 OPA Gatekeeper
Заголовок раздела «2.9 OPA Gatekeeper»Gatekeeper — admission controller на основе Open Policy Agent (OPA). Политики пишутся на Rego.
package k8srequiredlabels
violation[{"msg": msg}] { input.review.kind.kind == "Pod" required := {"app", "version"} provided := {label | input.review.object.metadata.labels[label]} missing := required - provided count(missing) > 0 msg := sprintf("missing labels: %v", [missing])}Объекты:
- ConstraintTemplate — определение политики (Rego + параметры).
- Constraint — инстанс политики с конкретными параметрами.
Преимущества: библиотека готовых политик (gatekeeper-library), декларативность.
Минусы: Rego — отдельный язык, learning curve.
2.10 Kyverno
Заголовок раздела «2.10 Kyverno»Kyverno — Kubernetes-native policy engine. Политики на YAML (нет внешнего DSL):
apiVersion: kyverno.io/v1kind: ClusterPolicymetadata: name: require-labelsspec: validationFailureAction: Enforce rules: - name: check-labels match: any: - resources: kinds: [Pod] validate: message: "pod must have labels app and version" pattern: metadata: labels: app: "?*" version: "?*"Возможности:
- validate — валидация.
- mutate — добавление/изменение полей.
- generate — генерация ресурсов (например, NetworkPolicy по namespace).
- verifyImages — Cosign / sigstore image verification.
- cleanup — удаление по cron.
Kyverno в 2026 выигрывает у Gatekeeper в простоте, но Gatekeeper мощнее для сложных правил.
2.11 ValidatingAdmissionPolicy (CEL, без webhook)
Заголовок раздела «2.11 ValidatingAdmissionPolicy (CEL, без webhook)»С 1.30 GA — ValidatingAdmissionPolicy (VAP). Это нативное расширение admission control: политика на CEL, без HTTPS-сервера и TLS-сертификатов. Работает быстрее и надёжнее, чем webhook.
apiVersion: admissionregistration.k8s.io/v1kind: ValidatingAdmissionPolicymetadata: name: deny-latest-imagespec: matchConstraints: resourceRules: - apiGroups: ["apps"] apiVersions: ["v1"] operations: ["CREATE","UPDATE"] resources: ["deployments"] validations: - expression: "object.spec.template.spec.containers.all(c, !c.image.endsWith(':latest'))" message: "image tag :latest is not allowed"---apiVersion: admissionregistration.k8s.io/v1kind: ValidatingAdmissionPolicyBindingmetadata: name: deny-latest-image-bindingspec: policyName: deny-latest-image validationActions: [Deny]В 2026 году MutatingAdmissionPolicy ещё в alpha/beta, но многие простые webhook’и можно мигрировать на VAP, убрав целый класс операционных проблем.
2.12 Аналог Aggregated API server
Заголовок раздела «2.12 Аналог Aggregated API server»Если CRD недостаточно — например, нужна сложная логика чтения (computed values на лету), альтернативное хранилище, версионирование — можно сделать Aggregated API server. Это полноценный Go API server, который регистрируется через APIService ресурс и принимает запросы для определённого group/version.
Используется редко (metrics.k8s.io, custom.metrics.k8s.io, KEDA v0, в OpenShift в части ресурсов). Текущая рекомендация — CRD + операторы покрывают 99% задач.
apiVersion: apiregistration.k8s.io/v1kind: APIServicemetadata: name: v1beta1.custom.metrics.k8s.iospec: service: namespace: monitoring name: prometheus-adapter group: custom.metrics.k8s.io version: v1beta1 insecureSkipTLSVerify: true groupPriorityMinimum: 100 versionPriority: 100Чем сложнее, тем больше обязанностей:
- Реализация storage (etcd/SQL/in-memory).
- Поддержка
LIST,WATCH, pagination, RV (resourceVersion). - Реализация OpenAPI schema.
- Аутентификация / авторизация (delegation на kube-apiserver).
В Go стек для этого — apimachinery, apiserver-runtime, apiserver-builder-alpha. Но даже Red Hat в последние годы переводит свои aggregated API на CRDs.
2.13 Scaling webhook’ов в production
Заголовок раздела «2.13 Scaling webhook’ов в production»Когда кластер большой (десятки тысяч pods, сотни RPS admission requests), webhook становится узким местом. Рекомендации:
- HA-deployment: минимум 3 replicas,
PodDisruptionBudget(maxUnavailable: 1), anti-affinity по node/zone. - CPU/memory tuning: webhook server обычно CPU-bound (JSON marshal/unmarshal, CEL eval). Прогоните бенчмарки, заложите 100-200ms headroom.
- Connection pooling: kube-apiserver открывает long-lived HTTPS connections.
ReadTimeout: 30s,WriteTimeout: 30s,IdleTimeout: 5m. - Profiling endpoint (на admin-port, не на webhook-port).
- Concurrency:
controller-runtimeиспользует один HTTP-сервер; используйтеMaxConcurrentReconcilesна admission’е через worker pool. - Cache: если webhook читает дополнительные ресурсы (ConfigMap, Service), используйте informer-based cache (см. controller-runtime client).
- Graceful shutdown: при SIGTERM сначала перестаньте принимать новые requests, доделайте текущие, потом exit.
srv := &http.Server{ Addr: ":9443", Handler: webhookMux, ReadTimeout: 10 * time.Second, WriteTimeout: 10 * time.Second, IdleTimeout: 300 * time.Second, TLSConfig: tlsConf,}
// Graceful shutdowngo func() { <-ctx.Done() shutCtx, cancel := context.WithTimeout(context.Background(), 30*time.Second) defer cancel() _ = srv.Shutdown(shutCtx)}()2.14 Тестирование webhook’ов
Заголовок раздела «2.14 Тестирование webhook’ов»Unit tests
Заголовок раздела «Unit tests»controller-runtime даёт admission.Decoder для распаковки AdmissionReview. Тестируется как обычная Go-функция:
func TestMutator_Default(t *testing.T) { m := &DeploymentMutator{} d := &appsv1.Deployment{ObjectMeta: metav1.ObjectMeta{Name: "test"}}
err := m.Default(context.Background(), d) require.NoError(t, err) require.Equal(t, "platform", d.Labels["managed-by"])}
func TestValidator_RejectsLatest(t *testing.T) { v := &DeploymentValidator{} d := &appsv1.Deployment{ Spec: appsv1.DeploymentSpec{ Template: corev1.PodTemplateSpec{ Spec: corev1.PodSpec{ Containers: []corev1.Container{{Name: "app", Image: "myapp:latest"}}, }, }, }, } _, err := v.ValidateCreate(context.Background(), d) require.Error(t, err) require.Contains(t, err.Error(), ":latest")}envtest (integration)
Заголовок раздела «envtest (integration)»sigs.k8s.io/controller-runtime/pkg/envtest поднимает локальный kube-apiserver + etcd:
testEnv := &envtest.Environment{ CRDDirectoryPaths: []string{filepath.Join("..", "..", "config", "crd", "bases")}, WebhookInstallOptions: envtest.WebhookInstallOptions{ Paths: []string{filepath.Join("..", "..", "config", "webhook")}, },}cfg, err := testEnv.Start()require.NoError(t, err)defer testEnv.Stop()Затем создаёте Pod через клиент, проверяете, что mutating webhook добавил labels.
kuttl / e2e
Заголовок раздела «kuttl / e2e»kuttl — KUbernetes Test TooL. Декларативные test files (apply, assert):
apiVersion: kuttl.dev/v1beta1kind: TestStepapply: - pod-without-label.yamlassert: - status-rejected.yaml2.15 Observability webhook’ов
Заголовок раздела «2.15 Observability webhook’ов»Webhook критичен — он должен иметь те же SRE-практики:
- Метрики Prometheus: количество admission requests, длительность, отказы, по
operation/resource/namespace. - Логи:
slog+ trace_id (см. файл 35). - Tracing: каждый admission review — span с attributes (
admission.review.uid,kind,namespace). - Алерты: P95 latency > 2s, error rate > 1%, RPS падение (значит API-server не общается с нами).
Пример метрик:
admissionDurationHist = prometheus.NewHistogramVec( prometheus.HistogramOpts{ Name: "admission_review_duration_seconds", Buckets: prometheus.ExponentialBucketsRange(0.001, 10, 12), }, []string{"webhook", "operation", "kind", "result"},)В Grafana dashboard сразу видно «webhook X стал медленным после deploy» — повод для немедленного rollback’а.
2.16 Сравнение admission control approaches
Заголовок раздела «2.16 Сравнение admission control approaches»| Approach | Сложность | Производительность | Гибкость | Идемпотентность |
|---|---|---|---|---|
| Built-in admission controllers | Низкая | Лучшая | Низкая | Stable |
| Pod Security Admission | Низкая | Высокая | Средняя | Конфиг через namespace labels |
| ValidatingAdmissionPolicy | Средняя | Высокая (in-proc) | Средняя | CEL DSL |
| Kyverno | Низкая | Средняя | Высокая | YAML, дружелюбен |
| OPA Gatekeeper | Средняя | Средняя | Очень высокая | Rego, мощный |
| Custom Go webhook | Высокая | Зависит | Максимальная | Полный контроль |
| Aggregated API server | Очень высокая | Зависит | Максимальная | Редко нужен |
Рекомендация для большинства команд: PSA + ValidatingAdmissionPolicy + Kyverno покрывают 95% потребностей без написания Go-кода. Custom webhook — когда логика выходит за рамки декларативного.
2.17 Эволюция в 2026
Заголовок раздела «2.17 Эволюция в 2026»Тренды последних 2-3 лет:
- Перенос политик из webhooks в kube-apiserver (VAP, MAP) — снижает latency и upgrade-risk.
- CEL становится новым «admission DSL».
- Sigstore + Cosign — обязательны для supply-chain (ZDN/SRE требования).
- Operator-driven политики (Kyverno, Gatekeeper) — стандарт enterprise.
- Меньше mutating webhook’ов в пользу defaulting в CRD schema (через
default:поля в OpenAPI).
3. Gotchas
Заголовок раздела «3. Gotchas»Gotcha 1: ⚠️ Webhook на kube-system ломает кластер
Заголовок раздела «Gotcha 1: ⚠️ Webhook на kube-system ломает кластер»Если ваш webhook применяется к kube-system и failurePolicy: Fail, и pod, реализующий webhook, упал — kube-controller-manager не может пересоздавать pods (включая ваш!). Кластер мёртв. Всегда исключайте kube-system через namespaceSelector. Пример:
namespaceSelector: matchExpressions: - key: kubernetes.io/metadata.name operator: NotIn values: [kube-system, kube-public, kube-node-lease]Gotcha 2: ⚠️ Webhook на собственный namespace == chicken-and-egg
Заголовок раздела «Gotcha 2: ⚠️ Webhook на собственный namespace == chicken-and-egg»Webhook применяется к Pod’ам namespace X. Сам webhook — Pod в namespace X. Pod падает. Apiserver не может создать новый — ждёт webhook. Решение: вынесите webhook в отдельный namespace (например, platform-system).
Gotcha 3: ⚠️ Не валидируйте DELETE-операции под oldObject == nil
Заголовок раздела «Gotcha 3: ⚠️ Не валидируйте DELETE-операции под oldObject == nil»При delete-операции в request.object приходит nil, в request.oldObject — старый объект. Многие реализации спотыкаются: obj := req.Object.Object.(*v1.Pod) → nil panic.
Gotcha 4: ⚠️ JSON Patch порядок имеет значение
Заголовок раздела «Gotcha 4: ⚠️ JSON Patch порядок имеет значение»Если ваш mutating webhook возвращает несколько patch-операций, и они зависят друг от друга, порядок важен. Также: при reinvocationPolicy: IfNeeded патч может быть применён к уже изменённому объекту — операции должны быть идемпотентны.
Gotcha 5: ⚠️ Не делайте долгие сетевые вызовы внутри webhook
Заголовок раздела «Gotcha 5: ⚠️ Не делайте долгие сетевые вызовы внутри webhook»timeoutSeconds максимум 30 (на самом деле — 10 для production-safety). Если webhook ходит во внешнюю систему — деградация → API server деградирует. Лучше — кэшировать решения, либо реализовать через controller (асинхронно).
Gotcha 6: ⚠️ Mutating webhook + Server-Side Apply конфликт
Заголовок раздела «Gotcha 6: ⚠️ Mutating webhook + Server-Side Apply конфликт»Если SSA-клиент пишет поле X, а ваш mutating webhook сразу же его меняет — клиент при следующем apply получит «conflict». Решение: webhook должен помечать managed fields с собственным FieldManager, и SSA-клиент должен знать, что эти поля не его.
Gotcha 7: ⚠️ Caching cert-manager: webhook не стартует без сертификата
Заголовок раздела «Gotcha 7: ⚠️ Caching cert-manager: webhook не стартует без сертификата»Pattern: webhook deployment стартует до того, как cert-manager выдал секрет. Container не может прочитать TLS-сертификат и крашится. Решения:
- Init container, который ждёт secret.
- Использовать readinessProbe.
- Webhook server должен gracefully ретраить, а не падать.
Gotcha 8: ⚠️ Versioning AdmissionReview
Заголовок раздела «Gotcha 8: ⚠️ Versioning AdmissionReview»admissionReviewVersions: ["v1"]. Старые webhook’и поддерживали v1beta1, который удалён. Если ваш webhook не отвечает в формате v1, API server отклоняет ответ.
Gotcha 9: ⚠️ FailurePolicy: Fail + Restart deployment storm
Заголовок раздела «Gotcha 9: ⚠️ FailurePolicy: Fail + Restart deployment storm»При rolling-update deployment’а, который шёл через ваш webhook: webhook рестартует, pods, проходящие через него, не создаются → deployment роллбэкается → создаются ещё больше pods → storm. Решение: webhook deployment должен использовать PodDisruptionBudget и быть highly available (минимум 2-3 реплики, anti-affinity по нодам).
Gotcha 10: ⚠️ Mutating webhook не видит final-state
Заголовок раздела «Gotcha 10: ⚠️ Mutating webhook не видит final-state»После mutating webhook’ов может ещё идти validating, который опять mutate (через reinvocation) или OpenAPI schema валидация может отвергнуть. Ваш mutating должен генерировать валидный JSON Patch — иначе будет ошибка после.
Gotcha 11: ⚠️ ObjectSelector не работает для namespace-scoped без labels
Заголовок раздела «Gotcha 11: ⚠️ ObjectSelector не работает для namespace-scoped без labels»objectSelector.matchLabels смотрит на labels самого объекта. Если объект только что создаётся и labels пустые — селектор «match nothing». Часто webhook’и используют namespaceSelector + labels на namespace.
Gotcha 12: ⚠️ Audit логи показывают AdmissionReview, но без object
Заголовок раздела «Gotcha 12: ⚠️ Audit логи показывают AdmissionReview, но без object»При высокой нагрузке Audit включает только metadata. Чтобы расследовать, какой объект был отвергнут — нужен audit на уровне RequestResponse, что увеличивает диск.
4. Real cases
Заголовок раздела «4. Real cases»4.1 Istio sidecar injection
Заголовок раздела «4.1 Istio sidecar injection»При создании Pod в namespace istio-injection=enabled, mutating webhook Istio инжектит контейнер istio-proxy (Envoy) и init-container для iptables-настройки. Конфигурация в IstioOperator CR.
Урок: используйте namespaceSelector для opt-in.
4.2 cert-manager CA inject
Заголовок раздела «4.2 cert-manager CA inject»cert-manager выпускает TLS-сертификат, кладёт в Secret. Затем сам же через mutating webhook читает cert-manager.io/inject-ca-from аннотацию и впрыскивает caBundle в ValidatingWebhookConfiguration, MutatingWebhookConfiguration, APIService, CRD — везде, где нужен CA bundle.
Урок: операторы могут патчить cluster-scoped ресурсы через webhook без участия пользователя.
4.3 OPA Gatekeeper в банке
Заголовок раздела «4.3 OPA Gatekeeper в банке»Use case (реальный): запрет образов вне приватного registry, требование labels team, cost-center, запрет HostPath. Политика на Rego, audit-режим в dev, enforce в prod. CI-бот собирает violations и блокирует PR.
4.4 Kyverno + Cosign
Заголовок раздела «4.4 Kyverno + Cosign»Kyverno + Cosign проверяют, что все image’ы подписаны (sigstore). Подпись — на этапе CI/CD. Webhook не пускает Pod, если signature не валидна. Так делают в supply-chain security.
4.5 PSA + namespace per team
Заголовок раздела «4.5 PSA + namespace per team»Кластер с 50+ командами: каждый namespace помечается pod-security.kubernetes.io/enforce: restricted. Команды, которым нужно privileged (CI, drivers) — получают baseline через label. PSA подменил Pod Security Policy без custom-кода.
5. Вопросы
Заголовок раздела «5. Вопросы»- Чем mutating отличается от validating webhook?
- Когда применяется OpenAPI schema validation — до или после mutating webhooks?
- Что такое AdmissionReview и какова его структура?
- Какие версии AdmissionReview поддерживаются?
- Что такое JSON Patch в ответе mutating webhook?
- Зачем нужен
caBundleи кто его заполняет? - Как cert-manager инжектит caBundle через
cert-manager.io/inject-ca-from? - Чем отличаются
failurePolicy: FailиIgnore? - Что такое
sideEffects: None? - Что такое
reinvocationPolicy: IfNeeded? - Как защититься от того, что webhook сломает kube-system?
- Что такое Pod Security Admission и чем оно лучше/хуже PSP?
- Какие три уровня PSA (privileged, baseline, restricted)?
- Что такое OPA Gatekeeper и Rego?
- Чем Kyverno отличается от Gatekeeper?
- Что такое ValidatingAdmissionPolicy и зачем он нужен?
- Может ли VAP заменить все webhook’и в кластере?
- Чем CEL отличается от Rego?
- Что такое APIService и Aggregated API server?
- Почему Aggregated API сейчас почти не используется?
- Как работает
objectSelectorvsnamespaceSelector? - Что произойдёт, если webhook отвечает 503 и
failurePolicy: Fail? - Сколько одновременных AdmissionReview может обработать ваш webhook?
- Как тестировать webhook локально (без кластера)?
- Какие риски ставить
timeoutSeconds: 30?
6. Practice
Заголовок раздела «6. Practice»- Defaulting webhook. Реализуйте mutating webhook, который добавляет label
cost-center=unknown, если не задан. - Validating webhook. Запретите Pod без
resources.limitsи без readinessProbe. - Image policy. Запретите образы с тегом
:latestчерез ValidatingAdmissionPolicy (без webhook’а). - Sidecar injector. Реализуйте mutating webhook, который инжектит контейнер с envoy при labelе
inject-envoy=true. - Kyverno policy. Напишите политику, требующую label
app.kubernetes.io/nameна всех ресурсах в namespaceprod. - OPA Gatekeeper. Создайте ConstraintTemplate
K8sAllowedReposи Constraint, разрешающий толькоmyregistry.io/*. - PSA migration. Переведите namespace с custom PSP на PSA restricted, перечислите все pods, которые не соответствуют.
- TLS bootstrap. Реализуйте self-signed bootstrap для webhook (без cert-manager).
- Reinvocation idempotent. Сделайте webhook, который добавляет sidecar, и проверьте, что повторный invocation не создаёт второй sidecar.
- Chaos test. Удалите cert-manager и проверьте, как ваш webhook восстановится.
7. Источники
Заголовок раздела «7. Источники»- Kubernetes Admission Controllers Reference — официальный список встроенных контроллеров.
- Dynamic Admission Control — webhooks.
- ValidatingAdmissionPolicy guide — CEL-based.
- Pod Security Admission.
- Pod Security Standards.
- OPA Gatekeeper docs.
- Kyverno docs.
- Kubebuilder webhook book chapter.
- Sigstore policy-controller — verifyImages альтернатива.
- cert-manager CA injector.
- APIService / Aggregation Layer.
- Kubernetes Deprecation Policy.