Skip to content
SRE & DevOpsDeep Dive Published Updated 5 min readViews unavailable

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.

Webhook availability becomes API availability

A dynamic webhook is on the API write path. Its failurePolicy decides whether a timeout or call failure rejects the request (Fail) or lets it continue (Ignore). Neither choice is universally correct: fail-closed protects a security boundary but can block deployments and recovery; fail-open preserves availability but creates a policy gap. Match the choice to the risk, monitor rejected and failed calls, and provide multiple webhook replicas with disruption protection and topology spread.

Keep timeouts short, match only required resources and operations, and avoid dependencies that can recursively require the same webhook to recover. Exclude the webhook’s own namespace or bootstrap objects where justified. Use a trusted CA bundle and rotate serving certificates before expiry. Kubernetes recommends idempotent mutation because mutating webhooks may be reinvoked, and side effects must follow the admission API’s declared side-effect behavior.

Prefer built-in declarative policy when it fits

Kubernetes includes ValidatingAdmissionPolicy, which evaluates Common Expression Language expressions in the API server. It avoids an external network call and is a strong fit for validations expressible against the request, parameters, and supported variables. Dynamic webhooks remain appropriate for policy engines, external data, complex mutation, or features not covered by built-ins. Choosing the smallest mechanism reduces both operational and security surface.

Whatever engine is used, version the policy and its tests. Include valid fixtures, invalid fixtures, updates, deletes, subresources, generated objects, and emergency workflows. A policy that blocks creation but allows a dangerous update, or validates Pods but not the controllers that generate them, provides incomplete coverage.

Rollout and forensic operation

Begin with audit, warning, or dry-run behavior supported by the chosen engine. Inventory violations, assign owners, and distinguish legacy exceptions from false positives. Enforce first in a representative nonproduction cluster, then in bounded production scope. Exceptions should identify subject, reason, approver, and expiry; a global namespace exclusion becomes permanent policy debt.

Alert on webhook latency, timeout, TLS failure, rejection rate, policy compilation errors, and unavailable replicas. Preserve API audit records and the policy version that made a decision. During an outage, operators need a reviewed break-glass procedure that cannot silently become the normal path. After recovery, reconcile every bypassed object and remove temporary exceptions.

Related:

Sources:

Comments