Kubernetes Admission Controllers в 2026 году: написание собственных вебхуков на Go для контроля политик кластера
Что такое Admission Controllers и как они работают в Kubernetes
Admission Controllers — это плагины Kubernetes, которые перехватывают запросы к API-серверу после аутентификации и авторизации, но до сохранения объекта в etcd. Они позволяют проверять, изменять или отклонять запросы на создание, обновление и удаление ресурсов кластера.
Цепочка обработки запроса выглядит так:
- Клиент отправляет запрос к kube-apiserver.
- Проходит аутентификация и авторизация (RBAC).
- Запрос поступает в цепочку Admission Controllers.
- Мутирующие контроллеры изменяют объект (например, добавляют метки).
- Валидирующие контроллеры проверяют финальное состояние объекта.
- Объект сохраняется в etcd и распределяется по кластеру.
Встроенные контроллеры (LimitRanger, ResourceQuota, PodSecurity) покрывают базовые сценарии, однако для специфических корпоративных политик необходимы Webhook Admission Controllers — внешние HTTP-сервисы, реализующие пользовательскую логику. В 2026 году это стандартная практика для Platform Engineering команд, работающих с Kubernetes.
ValidatingWebhookConfiguration vs MutatingWebhookConfiguration
Kubernetes поддерживает два типа вебхуков:
- MutatingAdmissionWebhook — изменяет объект перед его сохранением. Используется для автоматического добавления sidecar-контейнеров, принудительного проставления labels/annotations, установки resource limits по умолчанию.
- ValidatingAdmissionWebhook — только проверяет объект и возвращает разрешение или отказ. Не может изменять объект. Используется для проверки политик безопасности: запрета образов без тегов, соответствия naming-конвенциям, наличия обязательных меток.
Ключевое правило: если нужно исправить объект — используйте Mutating. Если нужно запретить невалидный объект — используйте Validating. На практике оба типа часто разворачиваются вместе: Mutating задаёт умолчания, Validating проверяет финальное состояние. Это особенно важно при написании политик Kubernetes на Go, где чёткое разделение ответственности упрощает тестирование.
Написание собственного Admission Webhook на Go
Структура проекта
Минимальная структура проекта для Admission Webhook на Go:
admission-webhook/
├── cmd/
│ └── webhook/
│ └── main.go
├── internal/
│ ├── handler/
│ │ ├── validate.go
│ │ └── mutate.go
│ └── policy/
│ ├── image_tag.go
│ └── resource_limits.go
├── deploy/
│ ├── deployment.yaml
│ ├── service.yaml
│ └── webhook-config.yaml
├── certs/
│ └── generate.sh
├── go.mod
└── go.sum
Зависимости go.mod
module github.com/yourorg/admission-webhook
go 1.23
require (
k8s.io/api v0.30.0
k8s.io/apimachinery v0.30.0
sigs.k8s.io/controller-runtime v0.18.0
)
Точка входа: main.go
package main
import (
"crypto/tls"
"log"
"net/http"
"github.com/yourorg/admission-webhook/internal/handler"
)
func main() {
mux := http.NewServeMux()
mux.HandleFunc("/validate", handler.Validate)
mux.HandleFunc("/mutate", handler.Mutate)
mux.HandleFunc("/healthz", func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusOK)
})
cert, err := tls.LoadX509KeyPair("/certs/tls.crt", "/certs/tls.key")
if err != nil {
log.Fatalf("Failed to load TLS certs: %v", err)
}
server := &http.Server{
Addr: ":8443",
TLSConfig: &tls.Config{
Certificates: []tls.Certificate{cert},
MinVersion: tls.VersionTLS13,
},
Handler: mux,
}
log.Println("Starting webhook server on :8443")
if err := server.ListenAndServeTLS("", ""); err != nil {
log.Fatalf("Server failed: %v", err)
}
}
Обработка AdmissionReview
Kubernetes отправляет на вебхук объект AdmissionReview и ожидает такой же объект в ответ с заполненным полем response. Базовый обработчик для валидирующего вебхука:
package handler
import (
"encoding/json"
"fmt"
"io"
"log"
"net/http"
admissionv1 "k8s.io/api/admission/v1"
corev1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"github.com/yourorg/admission-webhook/internal/policy"
)
func Validate(w http.ResponseWriter, r *http.Request) {
body, err := io.ReadAll(r.Body)
if err != nil {
http.Error(w, "failed to read body", http.StatusBadRequest)
return
}
var review admissionv1.AdmissionReview
if err := json.Unmarshal(body, &review); err != nil {
http.Error(w, "failed to unmarshal AdmissionReview", http.StatusBadRequest)
return
}
req := review.Request
var pod corev1.Pod
if err := json.Unmarshal(req.Object.Raw, &pod); err != nil {
http.Error(w, "failed to unmarshal Pod", http.StatusBadRequest)
return
}
allowed, reason := policy.ValidateImageTags(pod)
review.Response = &admissionv1.AdmissionResponse{
UID: req.UID,
Allowed: allowed,
}
if !allowed {
review.Response.Result = &metav1.Status{
Message: reason,
Code: 403,
}
}
resp, err := json.Marshal(review)
if err != nil {
log.Printf("Error marshaling response: %v", err)
http.Error(w, fmt.Sprintf("failed to marshal response: %v", err), http.StatusInternalServerError)
return
}
w.Header().Set("Content-Type", "application/json")
w.Write(resp)
}
Примеры политик Kubernetes на Go
Политика 1: запрет образов без тегов
Образы без явного тега (например, nginx вместо nginx:1.27.0) используют latest по умолчанию, что нарушает воспроизводимость деплоев. Реализуем проверку:
package policy
import (
"fmt"
"strings"
corev1 "k8s.io/api/core/v1"
)
// ValidateImageTags проверяет, что все контейнеры используют явные теги
// и не используют тег 'latest'.
func ValidateImageTags(pod corev1.Pod) (bool, string) {
allContainers := append(pod.Spec.InitContainers, pod.Spec.Containers...)
for _, c := range allContainers {
image := c.Image
// Проверяем наличие тега
parts := strings.Split(image, ":")
if len(parts) < 2 || parts[len(parts)-1] == "" {
return false, fmt.Sprintf(
"container '%s' uses image '%s' without explicit tag",
c.Name, image,
)
}
// Запрещаем тег 'latest'
tag := parts[len(parts)-1]
if tag == "latest" {
return false, fmt.Sprintf(
"container '%s' uses 'latest' tag, which is not allowed",
c.Name,
)
}
}
return true, ""
}
Политика 2: принудительные resource limits (Mutating Webhook)
Мутирующий вебхук добавляет resource limits контейнерам, у которых они не заданы. Для изменения объекта используется JSON Patch:
package handler
import (
"encoding/json"
"io"
"log"
"net/http"
admissionv1 "k8s.io/api/admission/v1"
corev1 "k8s.io/api/core/v1"
"k8s.io/apimachinery/pkg/api/resource"
)
type patchOp struct {
Op string `json:"op"`
Path string `json:"path"`
Value interface{} `json:"value,omitempty"`
}
func Mutate(w http.ResponseWriter, r *http.Request) {
body, _ := io.ReadAll(r.Body)
var review admissionv1.AdmissionReview
json.Unmarshal(body, &review)
var pod corev1.Pod
json.Unmarshal(review.Request.Object.Raw, &pod)
var patches []patchOp
defaultCPU := resource.MustParse("500m")
defaultMem := resource.MustParse("256Mi")
for i, c := range pod.Spec.Containers {
if c.Resources.Limits == nil {
patches = append(patches, patchOp{
Op: "add",
Path: fmt.Sprintf("/spec/containers/%d/resources/limits", i),
Value: corev1.ResourceList{
corev1.ResourceCPU: defaultCPU,
corev1.ResourceMemory: defaultMem,
},
})
}
}
patchBytes, _ := json.Marshal(patches)
patchType := admissionv1.PatchTypeJSONPatch
review.Response = &admissionv1.AdmissionResponse{
UID: review.Request.UID,
Allowed: true,
Patch: patchBytes,
PatchType: &patchType,
}
resp, _ := json.Marshal(review)
w.Header().Set("Content-Type", "application/json")
w.Write(resp)
_ = log.Writer()
}
Политика 3: принудительное добавление labels
В мутирующем вебхуке также легко добавить обязательные метки. Достаточно сформировать JSON Patch для пути /metadata/labels:
func buildLabelPatch(existingLabels map[string]string) []patchOp {
required := map[string]string{
"app.kubernetes.io/managed-by": "platform-team",
"env": "production",
}
var ops []patchOp
if existingLabels == nil {
ops = append(ops, patchOp{Op: "add", Path: "/metadata/labels", Value: required})
return ops
}
for k, v := range required {
if _, exists := existingLabels[k]; !exists {
// Экранируем '/' в ключах для JSON Pointer (RFC 6901)
safeKey := strings.ReplaceAll(k, "/", "~1")
ops = append(ops, patchOp{
Op: "add",
Path: "/metadata/labels/" + safeKey,
Value: v,
})
}
}
return ops
}
TLS и безопасность вебхуков
Kubernetes требует, чтобы все вебхуки работали по HTTPS. API-сервер проверяет сертификат вебхука по CA Bundle, указанному в WebhookConfiguration.
Генерация самоподписанных сертификатов
#!/bin/bash
# certs/generate.sh
SERVICE="admission-webhook"
NAMESPACE="webhook-system"
SECRET="admission-webhook-tls"
# Генерация CA
openssl genrsa -out ca.key 4096
openssl req -new -x509 -days 3650 -key ca.key \
-subj "/CN=Admission Webhook CA" \
-out ca.crt
# Генерация ключа и CSR для сервера
openssl genrsa -out tls.key 4096
openssl req -new -key tls.key \
-subj "/CN=${SERVICE}.${NAMESPACE}.svc" \
-out tls.csr
# Подпись сертификата
cat > ext.cnf <
Ротация сертификатов
В продакшене рекомендуется использовать cert-manager для автоматической ротации сертификатов. Cert-manager поддерживает аннотацию cert-manager.io/inject-ca-from для автоматического обновления caBundle в WebhookConfiguration. Это исключает ручную ротацию и снижает операционную нагрузку на Platform Engineering команды.
Деплой вебхука в Kubernetes
Namespace и RBAC
apiVersion: v1
kind: Namespace
metadata:
name: webhook-system
labels:
app.kubernetes.io/managed-by: platform-team
Deployment
apiVersion: apps/v1
kind: Deployment
metadata:
name: admission-webhook
namespace: webhook-system
spec:
replicas: 2
selector:
matchLabels:
app: admission-webhook
template:
metadata:
labels:
app: admission-webhook
spec:
securityContext:
runAsNonRoot: true
runAsUser: 65534
containers:
- name: webhook
image: yourorg/admission-webhook:1.2.0
ports:
- containerPort: 8443
resources:
requests:
cpu: 100m
memory: 64Mi
limits:
cpu: 200m
memory: 128Mi
volumeMounts:
- name: tls-certs
mountPath: /certs
readOnly: true
livenessProbe:
httpGet:
path: /healthz
port: 8443
scheme: HTTPS
initialDelaySeconds: 5
periodSeconds: 10
volumes:
- name: tls-certs
secret:
secretName: admission-webhook-tls
Service
apiVersion: v1
kind: Service
metadata:
name: admission-webhook
namespace: webhook-system
spec:
selector:
app: admission-webhook
ports:
- port: 443
targetPort: 8443
ValidatingWebhookConfiguration
apiVersion: admissionregistration.k8s.io/v1
kind: ValidatingWebhookConfiguration
metadata:
name: image-policy-webhook
webhooks:
- name: validate-image-tags.webhook-system.svc
admissionReviewVersions: ["v1"]
sideEffects: None
failurePolicy: Fail
namespaceSelector:
matchExpressions:
- key: kubernetes.io/metadata.name
operator: NotIn
values: ["kube-system", "webhook-system"]
rules:
- apiGroups: [""]
apiVersions: ["v1"]
operations: ["CREATE", "UPDATE"]
resources: ["pods"]
clientConfig:
service:
name: admission-webhook
namespace: webhook-system
path: /validate
caBundle: BASE64_ENCODED_CA_CERT
Параметр failurePolicy: Fail означает, что при недоступности вебхука запрос будет отклонён. Это безопасное поведение для production. Для staging можно использовать failurePolicy: Ignore.
Тестирование и отладка вебхуков
Unit-тесты политик
Политики легко тестируются изолированно, поскольку принимают и возвращают стандартные Kubernetes-объекты:
package policy_test
import (
"testing"
corev1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"github.com/yourorg/admission-webhook/internal/policy"
)
func TestValidateImageTags(t *testing.T) {
tests := []struct {
name string
image string
allowed bool
}{
{"valid image with tag", "nginx:1.27.0", true},
{"image without tag", "nginx", false},
{"image with latest tag", "nginx:latest", false},
{"image with sha digest", "nginx@sha256:abc123", true},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
pod := corev1.Pod{
ObjectMeta: metav1.ObjectMeta{Name: "test-pod"},
Spec: corev1.PodSpec{
Containers: []corev1.Container{
{Name: "app", Image: tt.image},
},
},
}
allowed, _ := policy.ValidateImageTags(pod)
if allowed != tt.allowed {
t.Errorf("expected allowed=%v, got %v", tt.allowed, allowed)
}
})
}
}
Интеграционное тестирование с envtest
Пакет sigs.k8s.io/controller-runtime/pkg/envtest позволяет запускать локальный kube-apiserver с зарегистрированными вебхуками без реального кластера. Это ускоряет итерации разработки.
Отладка в кластере
Полезные команды для диагностики вебхуков:
kubectl logs -n webhook-system deploy/admission-webhook— просмотр логов вебхука.kubectl describe validatingwebhookconfiguration image-policy-webhook— проверка конфигурации.kubectl get events --field-selector reason=FailedCreate— ошибки создания ресурсов из-за вебхука.- Включение аудит-лога kube-apiserver с
level: RequestResponseдля записи всех AdmissionReview запросов и ответов.
Интеграция проверок в CI/CD пайплайн
Для обеспечения полного покрытия политиками выстраивают многоуровневую защиту в CI/CD пайплайне. Это позволяет выявлять нарушения на ранних этапах, не дожидаясь отклонения деплоя в кластере.
Уровень 1: статический анализ манифестов
Инструменты kube-linter и datree проверяют YAML-манифесты на соответствие политикам ещё в репозитории, до применения в Kubernetes. Пример шага в GitHub Actions:
- name: Lint Kubernetes manifests
uses: stackrox/kube-linter-action@v1
with:
directory: deploy/
config: .kube-linter.yaml
Уровень 2: тестирование вебхука в ephemeral-кластере
В CI/CD пайплайне разворачивается временный Kubernetes-кластер (например, через kind или k3s), в котором устанавливается вебхук и прогоняются интеграционные тесты. Пример .gitlab-ci.yml:
integration-test:
stage: test
image: golang:1.23
services:
- docker:dind
script:
- curl -Lo ./kind https://kind.sigs.k8s.io/dl/v0.24.0/kind-linux-amd64
- chmod +x ./kind && mv ./kind /usr/local/bin/
- kind create cluster --name ci-cluster
- kubectl apply -f deploy/
- go test ./... -tags=integration -v
after_script:
- kind delete cluster --name ci-cluster
Уровень 3: политики как код с OPA/Gatekeeper
Для сложных сценариев рекомендуется рассмотреть OPA Gatekeeper — он реализует Admission Controller поверх Open Policy Agent. Кастомные Go-вебхуки и Gatekeeper хорошо сосуществуют: первые обрабатывают специфическую бизнес-логику, второй — декларативные политики через ConstraintTemplate.
Admission Webhook — это не замена RBAC, а дополнительный слой защиты. Грамотная комбинация RBAC, Admission Controllers, Network Policies и аудит-логирования формирует зрелую модель безопасности Kubernetes-кластера.
Заключение
Написание собственного Admission Webhook на Go — мощный инструмент для Platform Engineering команд, позволяющий централизованно контролировать политики Kubernetes-кластера. В 2026 году этот подход стал стандартом: он обеспечивает гибкость там, где встроенных механизмов недостаточно, и органично интегрируется в CI/CD пайплайны. Ключ к успеху — чёткое разделение между Mutating и Validating вебхуками, корректная настройка TLS, comprehensive тестирование и минимальные привилегии для сервисного аккаунта вебхука.
Технологии
Теги
Руслан Исмаилов
Senior Web / Backend разработчик. Senior web/backend разработчик с 9-летним опытом. Стек: PHP, Laravel, PostgreSQL, Redis, Docker, Kubernetes, REST, микросервисы, CI/CD. Подробнее обо мне →