Перейти к содержимому

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.

  1. Концепция: admission control в k8s
  2. Production-deep dive: webhooks в Go, OPA Gatekeeper, Kyverno, PSA, ValidatingAdmissionPolicy
  3. Gotchas (10+)
  4. Real cases: Istio injector, cert-manager, OPA Gatekeeper
  5. Вопросы (20+)
  6. Practice
  7. Источники

Любой 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).

  • Mutating — может изменить объект (через JSONPatch). Используется для: добавления labels/annotations, default-значений, инжекции sidecar-контейнеров.
  • Validating — только говорит «allow / reject». Используется для: enforcement политик («все Pod’ы должны иметь requests/limits»).

Порядок: сначала все mutating webhook’и, потом schema validation, потом все validating webhook’и. Этот порядок гарантирует, что validating видит финальный объект.

Webhook — это HTTPS endpoint, доступный из kube-apiserver:

  1. In-cluster — webhook бежит как Pod, выставлен через Service. URL: https://service.namespace.svc:443/path. Так делают 95% операторов.
  2. External — внешний URL (HTTPS), например, SaaS-провайдер политики. Используется реже из-за availability и latency.

API-сервер коннектится по TLS, поэтому нужен caBundle в манифесте MutatingWebhookConfiguration / ValidatingWebhookConfiguration.

apiVersion: admission.k8s.io/v1
kind: AdmissionReview
request:
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/v1
kind: AdmissionReview
response:
uid: 1234-abcd
allowed: true
patchType: JSONPatch
patch: "<base64-encoded JSON patch>"
warnings: ["deprecated label: x"]
status:
message: "rejected: missing label app"
code: 422

controller-runtime даёт встроенный webhook server. Подключение через kubebuilder:

Окно терминала
kubebuilder create webhook --group apps --version v1 --kind Deployment --defaulting --programmatic-validation

Получаем:

api/v1/deployment_webhook.go
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()
}
// +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
}
apiVersion: admissionregistration.k8s.io/v1
kind: MutatingWebhookConfiguration
metadata:
name: mdeploy.example.com
annotations:
cert-manager.io/inject-ca-from: platform/webhook-cert
webhooks:
- 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>

Webhook требует HTTPS. Варианты:

  1. cert-manager: создаём Certificate ресурс, cert-manager выпускает сертификат и кладёт в Secret. Аннотация cert-manager.io/inject-ca-from сообщает cert-manager-у автоматически впрыснуть caBundle в webhook configuration.
  2. kubebuilder Makefile + cert-manager — стандартный путь.
  3. Self-signed bootstrap — оператор сам генерит сертификат при старте, патчит caBundle. Так делают многие легковесные операторы; недостаток — ручная ротация.
  4. CSR API — оператор делает CertificateSigningRequest, ждёт approve.

Хорошая практика — менять сертификаты раз в 90 дней. Cert-manager делает это автоматически.

  • Fail — если webhook не отвечает или возвращает 5xx, запрос отклоняется. Безопаснее в смысле политик, но если ваш webhook упал, никто в кластере не может создавать ресурсы из match-rule.
  • Ignore — если webhook не отвечает, запрос пропускается без mutate/validate. Безопаснее для availability, но политика не применится.

Рекомендация: критичные security-политики — Fail, но с жёсткими namespaceSelector (исключайте kube-system!) и timeout 5s.

  • sideEffects: None / NoneOnDryRun / Some / Unknown. Webhook должен заявить, делает ли он side-effects (например, запись во внешнее хранилище). None идеален; иначе dry-run сломается.
  • reinvocationPolicy: Never / IfNeeded. Если другой mutating webhook уже изменил объект после вашего, kube-apiserver может вызвать вас повторно. IfNeeded нужен, если вы добавляете контейнер, но другой webhook может «оттолкать» его. Идеомпотентность обязательна — иначе при reinvocation вы добавите второй контейнер.
namespaceSelector:
matchLabels:
istio-injection: enabled
objectSelector:
matchExpressions:
- key: skip-webhook
operator: DoesNotExist

Это критически важно — иначе каждый Pod в кластере проходит через ваш webhook, включая kube-system. Если ваш webhook падает или медленный — кластер падает.

PodSecurityPolicy (PSP) был удалён в 1.25. Замена — Pod Security Admission, встроенный в kube-apiserver (не webhook!). Конфигурация через namespace labels:

apiVersion: v1
kind: Namespace
metadata:
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.

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.

Kyverno — Kubernetes-native policy engine. Политики на YAML (нет внешнего DSL):

apiVersion: kyverno.io/v1
kind: ClusterPolicy
metadata:
name: require-labels
spec:
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 мощнее для сложных правил.

С 1.30 GA — ValidatingAdmissionPolicy (VAP). Это нативное расширение admission control: политика на CEL, без HTTPS-сервера и TLS-сертификатов. Работает быстрее и надёжнее, чем webhook.

apiVersion: admissionregistration.k8s.io/v1
kind: ValidatingAdmissionPolicy
metadata:
name: deny-latest-image
spec:
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/v1
kind: ValidatingAdmissionPolicyBinding
metadata:
name: deny-latest-image-binding
spec:
policyName: deny-latest-image
validationActions: [Deny]

В 2026 году MutatingAdmissionPolicy ещё в alpha/beta, но многие простые webhook’и можно мигрировать на VAP, убрав целый класс операционных проблем.

Если 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/v1
kind: APIService
metadata:
name: v1beta1.custom.metrics.k8s.io
spec:
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.

Когда кластер большой (десятки тысяч pods, сотни RPS admission requests), webhook становится узким местом. Рекомендации:

  1. HA-deployment: минимум 3 replicas, PodDisruptionBudget (maxUnavailable: 1), anti-affinity по node/zone.
  2. CPU/memory tuning: webhook server обычно CPU-bound (JSON marshal/unmarshal, CEL eval). Прогоните бенчмарки, заложите 100-200ms headroom.
  3. Connection pooling: kube-apiserver открывает long-lived HTTPS connections. ReadTimeout: 30s, WriteTimeout: 30s, IdleTimeout: 5m.
  4. Profiling endpoint (на admin-port, не на webhook-port).
  5. Concurrency: controller-runtime использует один HTTP-сервер; используйте MaxConcurrentReconciles на admission’е через worker pool.
  6. Cache: если webhook читает дополнительные ресурсы (ConfigMap, Service), используйте informer-based cache (см. controller-runtime client).
  7. 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 shutdown
go func() {
<-ctx.Done()
shutCtx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
defer cancel()
_ = srv.Shutdown(shutCtx)
}()

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")
}

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 — KUbernetes Test TooL. Декларативные test files (apply, assert):

apiVersion: kuttl.dev/v1beta1
kind: TestStep
apply:
- pod-without-label.yaml
assert:
- status-rejected.yaml

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’а.

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-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).

Если ваш 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.

Если ваш mutating webhook возвращает несколько patch-операций, и они зависят друг от друга, порядок важен. Также: при reinvocationPolicy: IfNeeded патч может быть применён к уже изменённому объекту — операции должны быть идемпотентны.

Gotcha 5: ⚠️ Не делайте долгие сетевые вызовы внутри webhook

Заголовок раздела «Gotcha 5: ⚠️ Не делайте долгие сетевые вызовы внутри webhook»

timeoutSeconds максимум 30 (на самом деле — 10 для production-safety). Если webhook ходит во внешнюю систему — деградация → API server деградирует. Лучше — кэшировать решения, либо реализовать через controller (асинхронно).

Если 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 ретраить, а не падать.

admissionReviewVersions: ["v1"]. Старые webhook’и поддерживали v1beta1, который удалён. Если ваш webhook не отвечает в формате v1, API server отклоняет ответ.

При rolling-update deployment’а, который шёл через ваш webhook: webhook рестартует, pods, проходящие через него, не создаются → deployment роллбэкается → создаются ещё больше pods → storm. Решение: webhook deployment должен использовать PodDisruptionBudget и быть highly available (минимум 2-3 реплики, anti-affinity по нодам).

После 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, что увеличивает диск.


При создании Pod в namespace istio-injection=enabled, mutating webhook Istio инжектит контейнер istio-proxy (Envoy) и init-container для iptables-настройки. Конфигурация в IstioOperator CR.

Урок: используйте namespaceSelector для opt-in.

cert-manager выпускает TLS-сертификат, кладёт в Secret. Затем сам же через mutating webhook читает cert-manager.io/inject-ca-from аннотацию и впрыскивает caBundle в ValidatingWebhookConfiguration, MutatingWebhookConfiguration, APIService, CRD — везде, где нужен CA bundle.

Урок: операторы могут патчить cluster-scoped ресурсы через webhook без участия пользователя.

Use case (реальный): запрет образов вне приватного registry, требование labels team, cost-center, запрет HostPath. Политика на Rego, audit-режим в dev, enforce в prod. CI-бот собирает violations и блокирует PR.

Kyverno + Cosign проверяют, что все image’ы подписаны (sigstore). Подпись — на этапе CI/CD. Webhook не пускает Pod, если signature не валидна. Так делают в supply-chain security.

Кластер с 50+ командами: каждый namespace помечается pod-security.kubernetes.io/enforce: restricted. Команды, которым нужно privileged (CI, drivers) — получают baseline через label. PSA подменил Pod Security Policy без custom-кода.


  1. Чем mutating отличается от validating webhook?
  2. Когда применяется OpenAPI schema validation — до или после mutating webhooks?
  3. Что такое AdmissionReview и какова его структура?
  4. Какие версии AdmissionReview поддерживаются?
  5. Что такое JSON Patch в ответе mutating webhook?
  6. Зачем нужен caBundle и кто его заполняет?
  7. Как cert-manager инжектит caBundle через cert-manager.io/inject-ca-from?
  8. Чем отличаются failurePolicy: Fail и Ignore?
  9. Что такое sideEffects: None?
  10. Что такое reinvocationPolicy: IfNeeded?
  11. Как защититься от того, что webhook сломает kube-system?
  12. Что такое Pod Security Admission и чем оно лучше/хуже PSP?
  13. Какие три уровня PSA (privileged, baseline, restricted)?
  14. Что такое OPA Gatekeeper и Rego?
  15. Чем Kyverno отличается от Gatekeeper?
  16. Что такое ValidatingAdmissionPolicy и зачем он нужен?
  17. Может ли VAP заменить все webhook’и в кластере?
  18. Чем CEL отличается от Rego?
  19. Что такое APIService и Aggregated API server?
  20. Почему Aggregated API сейчас почти не используется?
  21. Как работает objectSelector vs namespaceSelector?
  22. Что произойдёт, если webhook отвечает 503 и failurePolicy: Fail?
  23. Сколько одновременных AdmissionReview может обработать ваш webhook?
  24. Как тестировать webhook локально (без кластера)?
  25. Какие риски ставить timeoutSeconds: 30?

  1. Defaulting webhook. Реализуйте mutating webhook, который добавляет label cost-center=unknown, если не задан.
  2. Validating webhook. Запретите Pod без resources.limits и без readinessProbe.
  3. Image policy. Запретите образы с тегом :latest через ValidatingAdmissionPolicy (без webhook’а).
  4. Sidecar injector. Реализуйте mutating webhook, который инжектит контейнер с envoy при labelе inject-envoy=true.
  5. Kyverno policy. Напишите политику, требующую label app.kubernetes.io/name на всех ресурсах в namespace prod.
  6. OPA Gatekeeper. Создайте ConstraintTemplate K8sAllowedRepos и Constraint, разрешающий только myregistry.io/*.
  7. PSA migration. Переведите namespace с custom PSP на PSA restricted, перечислите все pods, которые не соответствуют.
  8. TLS bootstrap. Реализуйте self-signed bootstrap для webhook (без cert-manager).
  9. Reinvocation idempotent. Сделайте webhook, который добавляет sidecar, и проверьте, что повторный invocation не создаёт второй sidecar.
  10. Chaos test. Удалите cert-manager и проверьте, как ваш webhook восстановится.

  1. Kubernetes Admission Controllers Reference — официальный список встроенных контроллеров.
  2. Dynamic Admission Control — webhooks.
  3. ValidatingAdmissionPolicy guide — CEL-based.
  4. Pod Security Admission.
  5. Pod Security Standards.
  6. OPA Gatekeeper docs.
  7. Kyverno docs.
  8. Kubebuilder webhook book chapter.
  9. Sigstore policy-controller — verifyImages альтернатива.
  10. cert-manager CA injector.
  11. APIService / Aggregation Layer.
  12. Kubernetes Deprecation Policy.