Kubernetes Admission Controllers: Enforcing Policy at Admission Time

Admission controllers intercept API server requests after authentication and authorization, but before persistence. They can mutate objects (add labels, inject sidecars) or validate them (reject non-c

Introduction#

Admission controllers intercept API server requests after authentication and authorization, but before persistence. They can mutate objects (add labels, inject sidecars) or validate them (reject non-compliant resources). They are the correct place to enforce cluster-wide policy.

Built-in Admission Controllers#

1
2
3
4
5
6
7
8
# View enabled admission plugins
kube-apiserver --help | grep enable-admission-plugins

# Common always-enabled controllers:
# NamespaceLifecycle, LimitRanger, ServiceAccount,
# DefaultStorageClass, DefaultTolerationSeconds,
# MutatingAdmissionWebhook, ValidatingAdmissionWebhook,
# ResourceQuota, Priority

LimitRanger: Enforces default resource requests/limits when not specified. ResourceQuota: Limits total resource consumption per namespace. PodSecurity: Enforces Pod Security Standards (baseline, restricted).

Webhook Admission Controllers#

Custom admission logic runs as webhooks. The API server calls your HTTP endpoint with an AdmissionReview object.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
# ValidatingWebhookConfiguration
apiVersion: admissionregistration.k8s.io/v1
kind: ValidatingWebhookConfiguration
metadata:
  name: policy-validator
webhooks:
- name: validate.example.com
  rules:
  - apiGroups: [""]
    apiVersions: ["v1"]
    resources: ["pods"]
    operations: ["CREATE", "UPDATE"]
  clientConfig:
    service:
      name: policy-webhook
      namespace: kube-system
      path: /validate
    caBundle: <base64-encoded-ca-cert>
  admissionReviewVersions: ["v1"]
  sideEffects: None
  failurePolicy: Fail  # Fail or Ignore — Fail blocks on webhook unavailability
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
# Webhook server (FastAPI)
from fastapi import FastAPI, Request
import base64, json

app = FastAPI()

@app.post("/validate")
async def validate(request: Request):
    body = await request.json()
    uid = body["request"]["uid"]
    pod = body["request"]["object"]

    # Reject pods without resource requests
    for container in pod["spec"]["containers"]:
        resources = container.get("resources", {})
        requests = resources.get("requests", {})
        if "memory" not in requests or "cpu" not in requests:
            return {
                "apiVersion": "admission.k8s.io/v1",
                "kind": "AdmissionReview",
                "response": {
                    "uid": uid,
                    "allowed": False,
                    "status": {
                        "message": f"Container '{container['name']}' missing resource requests"
                    }
                }
            }

    return {
        "apiVersion": "admission.k8s.io/v1",
        "kind": "AdmissionReview",
        "response": {"uid": uid, "allowed": True}
    }

MutatingAdmissionWebhook: Sidecar Injection#

Mutation webhooks modify objects before storage. Istio and Linkerd use this to inject proxy sidecars.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
@app.post("/mutate")
async def mutate(request: Request):
    body = await request.json()
    uid = body["request"]["uid"]
    pod = body["request"]["object"]

    # Build JSON patch to add a sidecar container
    patch = [
        {
            "op": "add",
            "path": "/spec/containers/-",
            "value": {
                "name": "metrics-sidecar",
                "image": "prom/statsd-exporter:latest",
                "ports": [{"containerPort": 9102}]
            }
        },
        {
            "op": "add",
            "path": "/metadata/labels/injected",
            "value": "true"
        }
    ]

    patch_b64 = base64.b64encode(json.dumps(patch).encode()).decode()

    return {
        "apiVersion": "admission.k8s.io/v1",
        "kind": "AdmissionReview",
        "response": {
            "uid": uid,
            "allowed": True,
            "patchType": "JSONPatch",
            "patch": patch_b64,
        }
    }

OPA/Gatekeeper for Policy as Code#

Open Policy Agent (OPA) with Gatekeeper is the standard approach for policy enforcement. Policies are written in Rego and stored as CRDs.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
# ConstraintTemplate: defines the policy schema and logic
apiVersion: templates.gatekeeper.sh/v1
kind: ConstraintTemplate
metadata:
  name: k8srequiredlabels
spec:
  crd:
    spec:
      names:
        kind: K8sRequiredLabels
      validation:
        openAPIV3Schema:
          properties:
            labels:
              type: array
              items:
                type: string
  targets:
  - target: admission.k8s.gatekeeper.sh
    rego: |
      package k8srequiredlabels
      violation[{"msg": msg}] {
        provided := {label | input.review.object.metadata.labels[label]}
        required := {label | label := input.parameters.labels[_]}
        missing := required - provided
        count(missing) > 0
        msg := sprintf("Missing required labels: %v", [missing])
      }
---
# Constraint: apply the policy with parameters
apiVersion: constraints.gatekeeper.sh/v1beta1
kind: K8sRequiredLabels
metadata:
  name: require-app-label
spec:
  match:
    kinds:
    - apiGroups: ["apps"]
      kinds: ["Deployment"]
  parameters:
    labels: ["app", "team", "environment"]

Kyverno: Kubernetes-Native Policies#

Kyverno policies are written in YAML, no Rego required.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
apiVersion: kyverno.io/v1
kind: ClusterPolicy
metadata:
  name: require-resource-limits
spec:
  validationFailureAction: Enforce
  rules:
  - name: check-container-resources
    match:
      any:
      - resources:
          kinds: [Pod]
    validate:
      message: "Resource limits are required for all containers."
      pattern:
        spec:
          containers:
          - resources:
              limits:
                memory: "?*"
                cpu: "?*"

Best Practices#

  • Set failurePolicy: Fail only for critical security policies; use Ignore for non-critical webhooks to prevent cluster lockout.
  • Always set appropriate timeouts (timeoutSeconds: 5) — a slow webhook blocks API server requests.
  • Exclude kube-system namespace from most policies to avoid breaking system components.
  • Test webhooks against a staging cluster before deploying to production.
  • Deploy webhook servers with high availability (multiple replicas).

Conclusion#

Admission controllers are the enforcement point for cluster-wide policy. Use built-in controllers for resource quotas and limits. Use OPA/Gatekeeper or Kyverno for custom policies as code. Reserve custom webhook servers for cases where neither handles your requirements.

Contents