08 · Admission Control & Policy Enforcement¶
Not run against a live cluster
Manifests and output below follow the documented admission-control request flow; not executed against a live cluster in this session.
Where admission sits in a request's lifecycle¶
Every write request to the API server passes, in order: authentication (who are you) → authorization (RBAC — are you allowed to do this verb on this resource) → admission control (should this specific object, as submitted, be allowed, and should it be modified first) → persisted to etcd. RBAC answers "can this identity create Pods"; admission answers "is this Pod, with these fields, acceptable" — a completely different, content-aware check that RBAC has no concept of.
Built-in admission plugins you've already used¶
ResourceQuota (Module 07), LimitRange (Level 2, Module 03), and the
DefaultStorageClass plugin (auto-filling storageClassName if a PVC
omits it) are all built-in admission plugins compiled into the API server
and enabled via --enable-admission-plugins. They run for every relevant
request, unconditionally, with no way to add custom logic beyond what's
compiled in — which is exactly the gap dynamic admission webhooks fill.
ValidatingAdmissionWebhook: reject non-compliant objects¶
apiVersion: admissionregistration.k8s.io/v1
kind: ValidatingWebhookConfiguration
metadata:
name: require-resource-limits
webhooks:
- name: require-limits.policy.example.com
admissionReviewVersions: ["v1"]
sideEffects: None
clientConfig:
service:
name: policy-webhook
namespace: policy-system
path: /validate-pods
rules:
- apiGroups: [""]
apiVersions: ["v1"]
resources: ["pods"]
operations: ["CREATE"]
failurePolicy: Fail
The named Service (policy-webhook) fronts a Pod that implements the
webhook: the API server sends it an AdmissionReview JSON payload
containing the object being created, and the webhook responds allowed:
true/false with an optional reason.
// AdmissionReview response (webhook's own logic, e.g. rejecting a Pod with no resource limits)
{
"apiVersion": "admission.k8s.io/v1",
"kind": "AdmissionReview",
"response": {
"uid": "...",
"allowed": false,
"status": { "message": "every container must set resources.limits.memory" }
}
}
kubectl apply -f pod-no-limits.yaml
# Error from server: admission webhook "require-limits.policy.example.com" denied the request:
# every container must set resources.limits.memory
MutatingAdmissionWebhook: rewrite objects before they're stored¶
apiVersion: admissionregistration.k8s.io/v1
kind: MutatingWebhookConfiguration
metadata:
name: inject-sidecar
webhooks:
- name: sidecar-injector.policy.example.com
admissionReviewVersions: ["v1"]
sideEffects: None
clientConfig:
service: { name: sidecar-injector, namespace: policy-system, path: /mutate }
rules:
- apiGroups: [""]
apiVersions: ["v1"]
resources: ["pods"]
operations: ["CREATE"]
A mutating webhook returns a JSONPatch describing edits, applied by the API server to the object before it's persisted — this is exactly how service-mesh sidecar injection (Istio, Linkerd) works: every Pod creation is intercepted and a proxy container is patched in, with no change needed to any Deployment manifest itself. Mutating webhooks always run before validating ones, so a mutation can itself be checked by subsequent validation.
Policy-as-code: OPA Gatekeeper / Kyverno¶
Hand-writing a webhook server for every rule doesn't scale; policy engines let you express rules declaratively and run as the webhook implementation for you.
# Kyverno ClusterPolicy example
apiVersion: kyverno.io/v1
kind: ClusterPolicy
metadata:
name: require-labels
spec:
validationFailureAction: Enforce
rules:
- name: require-team-label
match:
resources: { kinds: ["Pod"] }
validate:
message: "every Pod must have a 'team' label"
pattern:
metadata:
labels:
team: "?*"
kubectl apply -f pod-no-team-label.yaml
# Error from server: admission webhook "validate.kyverno.svc-fail" denied the request:
# policy require-labels/require-team-label fail: validation error: every Pod must have a 'team' label
Kyverno/Gatekeeper register one webhook with the API server and dispatch internally to however many declarative policies are loaded — this is why real clusters run one policy engine rather than dozens of hand-rolled webhooks: fewer network hops per admission request, and a single, auditable place to see every active rule.
Pod Security Admission: the built-in baseline¶
apiVersion: v1
kind: Namespace
metadata:
name: prod
labels:
pod-security.kubernetes.io/enforce: restricted
pod-security.kubernetes.io/audit: restricted
Since Kubernetes 1.25, Pod Security Admission is a built-in admission
plugin (no separate webhook install needed) enforcing the privileged,
baseline, or restricted Pod Security Standards purely via namespace
labels — Level 4, Module 04 covers what restricted actually requires in
depth.
Worked example: a policy rejects a privileged Pod at creation¶
kubectl apply -f privileged-pod.yaml
# Error from server: admission webhook "validate.kyverno.svc-fail" denied the request:
# policy disallow-privileged fail: Privileged mode is disallowed
kubectl get pods
# never created -- the API server never persisted the object at all
How It Actually Works¶
- Admission runs synchronously, in the request path, with a strict
timeout — a slow or down webhook has direct, immediate consequences for
every matching request. The API server calls each applicable webhook
over HTTPS (mutual TLS, via the referenced Service, which the API
server's in-cluster networking must be able to reach) and blocks
completing the request until it responds or
timeoutSeconds(max 30s) elapses;failurePolicy: Failthen rejects the request outright on timeout/error, whileIgnorelets it through unpoliced — an under-provisioned or crash-looping policy webhook withFailset can therefore block all matching object creation cluster-wide, which is why webhook availability itself needs the same production rigor as any critical-path service. - Multiple webhooks for the same resource/operation all run, and all must approve — but mutating webhooks execute serially (each seeing the previous one's patch) while validating webhooks run against the final post-mutation object. This ordering guarantee (all mutating, then all validating) is why policy engines validate the sidecar-injected version of a Pod, not the version the user originally submitted — a validation rule checking "does this Pod have a resource limit" will see limits a mutating webhook added, correctly, even though the user never wrote them.
- JSONPatch mutations are diffed and applied by the API server itself,
not executed as arbitrary code from the webhook. The webhook's response
contains a
patchfield (base64-encoded JSONPatch operations) andpatchType: JSONPatch; the API server applies this patch using the standard JSONPatch RFC 6902 semantics against the object as it existed when the webhook was called — this bounded, declarative patch format is a deliberate security boundary: a webhook can only describe what to change, never directly write to etcd or bypass subsequent validation. - Gatekeeper implements its policy language (Rego, via OPA) as a
library invoked from within its webhook server process — Kubernetes has
no native concept of Rego or CEL-based policy at the webhook layer
itself (CEL-based
ValidatingAdmissionPolicy, a newer built-in alternative, is the one exception, compiled and evaluated directly by the API server with no webhook round-trip at all). This is a real performance distinction: aValidatingAdmissionPolicy(stable since 1.30) runs in-process in the API server with no network hop, while a Gatekeeper/Kyverno-based policy always costs at least one HTTPS round-trip per admission check.
Exercise¶
Deploy Kyverno (or write a minimal validating webhook by hand) with a
policy requiring every Pod to declare resources.limits.memory. Attempt to
create a Pod without it and confirm the API server rejects the request with
the webhook's message before the object is ever persisted (kubectl get
pods shows nothing created). Then add a mutating policy that injects a
default team: unspecified label on any Pod missing one, and confirm a
Pod created without that label ends up with it set after creation.