Skip to content
daniel@cosenza:~/blog
SRE & DevOpsDeep Dive March 12, 2026 3 min read

Kubernetes Admission Controllers and Policy Enforcement

How admission controllers intercept API requests before they're persisted, and how OPA/Gatekeeper turn that hook into cluster-wide policy enforcement.

Every kubectl apply passes through a pipeline before it’s ever persisted to etcd — authentication, authorization, and finally admission control, the last checkpoint where a request can still be modified or rejected. It’s the mechanism that turns “we have a policy” into “the cluster mechanically enforces that policy,” rather than relying on developer discipline alone.

Where admission control sits in the request pipeline

A request to the Kubernetes API server flows through authentication (who are you), authorization (are you allowed to do this at all, via RBAC), and only then through admission control — which runs in two distinct phases: mutating admission (webhooks that can modify the request) followed by validating admission (webhooks that can only accept or reject it, not change it).

Client → Authentication → Authorization (RBAC) → Mutating Admission → Validating Admission → etcd

This ordering matters: mutation always happens before validation, so a validating webhook sees the final, already-mutated version of an object — never a request that a later mutation might still change.

Built-in admission controllers

Kubernetes ships several admission controllers enabled by default, each enforcing one specific concern — LimitRanger (enforcing default/min/max resource limits), ResourceQuota (enforcing namespace-level resource caps), PodSecurity (enforcing Pod Security Standards, the modern replacement for the deprecated PodSecurityPolicy):

kube-apiserver --enable-admission-plugins=LimitRanger,ResourceQuota,PodSecurity
apiVersion: v1
kind: Namespace
metadata:
  name: production
  labels:
    pod-security.kubernetes.io/enforce: restricted

Dynamic admission webhooks: your own policy logic

Beyond the built-ins, ValidatingWebhookConfiguration and MutatingWebhookConfiguration resources register your own HTTP services as admission checks — the API server calls out to them with the incoming object, and the webhook responds allow/deny (plus, for mutating webhooks, a JSON patch to apply):

apiVersion: admissionregistration.k8s.io/v1
kind: ValidatingWebhookConfiguration
metadata:
  name: require-labels
webhooks:
  - name: require-labels.example.com
    rules:
      - apiGroups: [""]
        apiVersions: ["v1"]
        operations: ["CREATE"]
        resources: ["pods"]
    clientConfig:
      service:
        name: policy-webhook
        namespace: policy-system
        path: "/validate"

OPA and Gatekeeper: policy as declarative code

Writing and hosting a custom webhook service for every policy doesn’t scale well — Open Policy Agent (OPA), via the Gatekeeper project, provides a general-purpose policy engine that plugs into this exact webhook mechanism, letting policies be expressed declaratively in Rego rather than as bespoke webhook code per rule:

package k8srequiredlabels

violation[{"msg": msg}] {
    required := input.parameters.labels
    provided := input.review.object.metadata.labels
    missing := required[_]
    not provided[missing]
    msg := sprintf("missing required label: %v", [missing])
}
apiVersion: constraints.gatekeeper.sh/v1beta1
kind: K8sRequiredLabels
metadata:
  name: require-team-label
spec:
  match:
    kinds:
      - apiGroups: [""]
        kinds: ["Namespace"]
  parameters:
    labels: ["team"]

Gatekeeper itself is just one (very common) validating webhook that delegates the actual decision logic to OPA’s Rego evaluation — the extensibility point is the same standard admission webhook mechanism described above, wrapped in a purpose-built policy framework.

Real-world policies this enables

Common production Gatekeeper/OPA policies include requiring images to come from an approved registry, requiring a signature verification pass, disallowing :latest tags, requiring specific labels for cost-allocation tracking, and enforcing that containers never run as root — each expressed once, as a Rego constraint, and automatically applied cluster-wide to every matching resource going forward.

violation[{"msg": msg}] {
    image := input.review.object.spec.containers[_].image
    not startswith(image, "registry.corp.example.com/")
    msg := "images must come from the approved internal registry"
}

Dry-run and audit modes: rolling out policy safely

Both raw webhooks and Gatekeeper constraints support a dry-run/audit-only mode — logging what would be rejected without actually blocking anything — which is the standard safe rollout pattern for a new policy: observe what it would have caught across real cluster traffic, fix any unexpected false positives, then flip enforcement on.

spec:
  enforcementAction: dryrun

Why this matters beyond “having rules”

The value of admission control isn’t that it lets you write down a policy — a wiki page does that too. It’s that the policy is mechanically, uniformly enforced at the one chokepoint every single resource in the cluster must pass through, regardless of which team, pipeline, or kubectl user is creating it. That’s a fundamentally different (and far more reliable) guarantee than “the deployment pipeline is supposed to check for this,” since admission control catches direct kubectl apply calls, other automation, and any pipeline that skipped a check equally.