Admission Controllers
An admission controller is code compiled into kube-apiserver that intercepts a request to create, update, or delete an API object after authentication and authorization have passed but before the object is persisted to etcd (Kubernetes — Admission Controllers). Admission runs in two phases: a mutating phase that may rewrite the object, followed by a validating phase that may only accept or reject it. Admission controllers do not see read operations —
get,list, andwatchbypass the gate entirely, because there is nothing to mutate and nothing to validate against persistence. Admission is the layer where Kubernetes enforces “what may exist in the cluster,” as distinct from authorization’s “what may this user do.” This note covers the built-in (compiled-in) plugins; the two webhook plugins that delegate to external HTTP services are covered in ValidatingAdmissionWebhook and MutatingAdmissionWebhook, and the in-process CEL alternatives in ValidatingAdmissionPolicy and MutatingAdmissionPolicy.
Mental Model
Admission is stages 4 and 6 of the API Server Request Flow: mutating admission sits after authorization, schema validation sits between, and validating admission is the last gate before storage. The crucial conceptual split is who controls it: authentication and authorization answer questions about the caller; admission answers questions about the object. A user with full RBAC permission to create pods can still be blocked by PodSecurity admission if the Pod requests privileged: true in a Restricted-labelled namespace — RBAC said yes, admission said no.
A second key idea: admission controllers are cluster-administrator territory, not tenant territory. The set of enabled plugins is fixed at apiserver startup via flags; a namespace tenant cannot turn one off. The only tenant-facing extensibility is the webhook plugins (whose configurations are API objects) and the CEL policy objects — both of which are themselves gated by the compiled-in MutatingAdmissionWebhook / ValidatingAdmissionWebhook / ValidatingAdmissionPolicy plugins.
flowchart LR A[Authenticated +<br/>Authorized request<br/>CREATE/UPDATE/DELETE] --> B[Built-in<br/>mutating plugins<br/>ServiceAccount, Priority,<br/>DefaultStorageClass...] B --> C[MutatingAdmissionPolicy<br/>plugin -> in-process CEL] C --> D[MutatingAdmissionWebhook<br/>plugin -> external webhooks] D --> E[Schema / OpenAPI<br/>validation] E --> F[Built-in<br/>validating plugins<br/>PodSecurity, ResourceQuota,<br/>LimitRanger, NamespaceLifecycle...] F --> G[ValidatingAdmissionPolicy<br/>plugin -> in-process CEL] G --> H[ValidatingAdmissionWebhook<br/>plugin -> external webhooks] H --> I[(etcd)] style I fill:#e8f0ff
The admission pipeline. The insight: all mutation finishes before any validation begins. A validating plugin always sees the final, fully-mutated object. The webhook and policy plugins are not a separate stage — they are themselves entries in the same ordered plugin list, which is why MutatingAdmissionWebhook appears among the built-in mutators and ValidatingAdmissionWebhook among the built-in validators. In-process CEL policies run before their webhook siblings on each side — per KEP-3962 the mutating chain is “Mutating admission controllers … Mutating admission policies … Mutating admission webhooks (ordered lexicographically by webhook name),” and the validating chain is documented identically on the admission-controllers reference page (sequence diagram caption: “mutation webhooks, followed by validatingadmissionpolicies and finally validating webhooks” — i.e., VAP before VAW).
Mechanical Walk-through
The phase ordering
Within the apiserver, admission plugins are arranged in a fixed registration order. The dispatcher walks the list twice: once collecting every plugin that implements the mutating interface, then once collecting every plugin that implements the validating interface (a few plugins, e.g. ResourceQuota, are validating-only; others like ServiceAccount are mutating-only; the webhook plugins each implement only one side). Concretely the order is:
- Built-in mutators —
ServiceAccount,DefaultStorageClass,DefaultTolerationSeconds,DefaultIngressClass,Priority,RuntimeClass(its defaulting half),TaintNodesByCondition, etc. MutatingAdmissionPolicy— evaluates in-process CEL mutation policies (GA in Kubernetes v1.36 per the Mutating Admission Policy reference; see MutatingAdmissionPolicy).MutatingAdmissionWebhook— calls out to every matchingMutatingWebhookConfiguration, ordered lexicographically by webhook name (KEP-3962).- Schema / OpenAPI validation — not an admission plugin, but it runs here, between the phases.
- Built-in validators —
LimitRanger,PodSecurity,ResourceQuota,NamespaceLifecycle,NodeRestriction,CertificateApproval,CertificateSubjectRestriction,RuntimeClass(its validating half), etc. ValidatingAdmissionPolicy— in-process CEL validation (GA 1.30, default-on in 1.36 per the admission-controllers reference).ValidatingAdmissionWebhook— calls out to every matchingValidatingWebhookConfiguration.
The “policies before webhooks” placement on each side is the explicit upstream design — policies are in-process so they cost essentially nothing per call, and running them first means a request rejected by a CEL policy never incurs the cost of a webhook round-trip.
If any plugin in any phase rejects, the whole request fails immediately with an HTTP error and nothing is persisted. A mutating plugin may also reject (not only modify). Because a later mutating webhook can change the object after an earlier one already ran, the MutatingAdmissionWebhook plugin supports reinvocation — re-running webhooks whose object input changed (see MutatingAdmissionWebhook).
The built-in plugin catalog
The plugins worth knowing, by what they actually do:
NamespaceLifecycle(validating) — rejects object creation in a namespace that is being deleted (Terminating) or does not exist, and prevents deletion of the three reserved namespacesdefault,kube-system,kube-public. This is why akubectl applyinto a half-deleted namespace fails with “namespace is terminating.”ServiceAccount(mutating) — injects the namespace’sdefaultServiceAccount into Pods that omit one, and adds the projected service-account-token volume so the Pod can authenticate. Without this plugin, Pods would have no in-cluster identity. See ServiceAccount.LimitRanger(mutating + validating) — applies the namespace’sLimitRangedefaults to Pods/Containers that omit requests/limits (mutating), then rejects any that exceed the configured min/max bounds (validating). See LimitRange.ResourceQuota(validating) — rejects a create/update that would push a namespace over itsResourceQuotacaps (object counts, total CPU/memory requests and limits). See ResourceQuota.PodSecurity(validating) — enforces the Pod Security Standards (Privileged/Baseline/Restricted) according to the namespace’spod-security.kubernetes.io/enforcelabel. This is the compiled-in replacement for the removedPodSecurityPolicy. See Pod Security Admission.DefaultStorageClass(mutating) — assigns the cluster’s defaultStorageClassto aPersistentVolumeClaimthat omitsstorageClassName. See StorageClass.DefaultIngressClass(mutating) — the same idea forIngressobjects and the defaultIngressClass.Priority(mutating) — resolves a Pod’spriorityClassNameto the numericpriorityinteger the scheduler uses. See Pod Priority and Preemption.DefaultTolerationSeconds(mutating) — adds thenode.kubernetes.io/not-readyandnode.kubernetes.io/unreachabletolerations with the default 300-second timeout to Pods that lack them — the mechanism behind “Pods are evicted ~5 minutes after a node goes NotReady.”RuntimeClass(mutating + validating) — applies Pod overhead and scheduling constraints declared by a referenced RuntimeClass, and rejects references to non-existent RuntimeClasses.NodeRestriction(validating) — restricts each kubelet to modifying only its ownNodeobject and only the Pods bound to it; works in tandem with theNodeauthorizer to contain a compromised node.CertificateApproval/CertificateSigning/CertificateSubjectRestriction(validating) — guard the CertificateSigningRequest workflow so a CSR cannot be approved or signed by a principal lacking the right permissions, and block CSRs requesting the privilegedsystem:mastersgroup.MutatingAdmissionWebhook/ValidatingAdmissionWebhook— the two plugins that turn the static compiled-in pipeline into a dynamic, extensible one by delegating to external HTTP webhooks registered as API objects.ValidatingAdmissionPolicy— the plugin that evaluates in-process CEL policy objects.
Configuration / API Surface
Admission is configured at apiserver startup, not via API objects:
kube-apiserver \
# Add a non-default plugin to the default set:
--enable-admission-plugins=NamespaceLifecycle,LimitRanger,AlwaysPullImages \
# Remove a plugin from the default set:
--disable-admission-plugins=DefaultTolerationSeconds \
# Optional: per-plugin config (e.g. PodSecurity defaults, ImagePolicyWebhook endpoint):
--admission-control-config-file=/etc/kubernetes/admission-config.yamlLine-by-line:
--enable-admission-plugins— plugins listed here are turned on in addition to the compiled-in default-on set. It does not replace the defaults.--disable-admission-plugins— plugins listed here are turned off, even if they are default-on. DisablingServiceAccountorNamespaceLifecycleis almost always a mistake.- The default-on set as of Kubernetes 1.36, verbatim from the admission-controllers reference, is:
CertificateApproval,CertificateSigning,CertificateSubjectRestriction,DefaultIngressClass,DefaultStorageClass,DefaultTolerationSeconds,LimitRanger,MutatingAdmissionWebhook,NamespaceLifecycle,PersistentVolumeClaimResize,PodSecurity,Priority,ResourceQuota,RuntimeClass,ServiceAccount,StorageObjectInUseProtection,TaintNodesByCondition,ValidatingAdmissionPolicy,ValidatingAdmissionWebhook. Notably this list does not includeMutatingAdmissionPolicyeven though the dedicated Mutating Admission Policy reference page marks itKubernetes v1.36 [stable] (enabled by default). The two upstream docs pages disagree as of this writing (see uncertainty callout below). --admission-control-config-file— points at a config file giving per-plugin parameters; the canonical use is supplyingPodSecurity’s cluster-wide default level and exemptions.
Inspect what is active:
# The set of enabled plugins is reported in apiserver's verbose health output
kubectl get --raw='/livez?verbose' | grep -i admission
# Dynamic webhook configurations (the tenant-visible part)
kubectl get mutatingwebhookconfigurations
kubectl get validatingwebhookconfigurations
kubectl get validatingadmissionpoliciesResolved (2026-05-30)
MutatingAdmissionPolicyis compiled-in as default-on in Kubernetes 1.36. Verified directly againstpkg/kubeapiserver/options/plugins.goatrelease-1.36— note the file moved from the oldercmd/kube-apiserver/app/options/plugins.gopath.mutatingadmissionpolicy.PluginNameis registered inAllOrderedPluginsand appears in thedefaultOnPluginsset insideDefaultOffAdmissionPlugins(); since that function returnsAllOrderedPlugins − defaultOnPlugins, presence in the default-on set means the plugin is not in the default-off set — i.e., it is default-on. Activation is additionally gated by theMutatingAdmissionPolicyfeature gate, which is GA (locked-on) in 1.36 per KEP-3962. The admission-controllers reference page omittingMutatingAdmissionPolicyfrom its default-on list and its “extension points” section is therefore a documentation-currency bug, not a behavior difference. (Caveat: the default-on set drifts each minor — for any specific cluster, verify againstplugins.goat that minor’s tag.)
Failure Modes
- Disabled
ServiceAccountplugin. Pods created afterward have no token volume and no in-cluster identity; every in-Pod API call becomes anonymous. Symptom:403from controllers running in those Pods. - Webhook plugin enabled, webhook unreachable. Because
MutatingAdmissionWebhook/ValidatingAdmissionWebhookare themselves compiled-in plugins, you cannot disable them per-cluster to escape a misbehaving external webhook — you must fix or delete the offendingWebhookConfigurationobject. See the cluster-wedge failure mode in ValidatingAdmissionWebhook. ResourceQuotarace. Because quota is enforced at admission and quota usage is updated by a controller asynchronously, bursts of concurrent creates can briefly over-admit before the usage counter catches up; the apiserver then rejects the next request. Symptom: intermittentexceeded quotaunder high create concurrency.NamespaceLifecycleand stuck finalizers. A namespace stuckTerminatingbecause a finalizer never clears will reject all new objects in it — a common “I can’t deploy and I don’t know why” incident.- Managed-service lockout. EKS, GKE, and AKS do not expose
--enable-admission-plugins. If you need a non-default built-in plugin (e.g.AlwaysPullImages), you must instead implement the policy with a webhook or a CEL policy.
Alternatives and When to Choose Them
- Built-in plugin vs. webhook vs. CEL policy. Prefer a built-in plugin when one exists for your need — it is in-process, cannot fail open, and needs no extra deployment. Use a ValidatingAdmissionPolicy (or MutatingAdmissionPolicy) when the rule fits a CEL expression: still in-process, no certificate management, no network hop. Reach for a webhook (ValidatingAdmissionWebhook / MutatingAdmissionWebhook) only when the policy needs arbitrary logic or external data (image-signature lookups, calls to an external system).
- Admission vs. runtime enforcement. Admission catches a bad object before it exists; runtime tools like Falco catch bad behavior after the workload is running. They are complementary, not substitutes — admission is cheaper and should be the first line.
Production Notes
- The standard guidance (Kubernetes — Admission Webhook Good Practices) is: keep the default plugin set, and express custom policy through
ValidatingAdmissionPolicy(GA 1.30) /MutatingAdmissionPolicy(GA 1.36) first, falling back to webhooks (Gatekeeper, Kyverno) only for what CEL cannot express. The “CEL first, webhooks last” preference is grounded in cost and reliability — a CEL policy runs in-process with no network hop and no fail-open vs. fail-closed dilemma, whereas a webhook adds latency to every matching request and forces an explicitfailurePolicychoice that can wedge the cluster either way. - Policy engines OPA Gatekeeper and Kyverno are not admission controllers in the compiled-in sense — they are external webhook servers reached through the
Validating/MutatingAdmissionWebhookplugins. Confusing “admission controller” with “policy engine” is a common interview slip. - Because the plugin order is fixed in source, dependency-sensitive logic (e.g. a webhook that must run before
PodSecurity) is constrained: webhooks always run before the built-in validators, so a webhook can never observe aPodSecurityrejection — it must reject independently.
See Also
- API Server Request Flow — admission is stages 4 and 6 of this pipeline
- kube-apiserver — the component that hosts the admission plugins
- ValidatingAdmissionWebhook — the external-validation webhook plugin
- MutatingAdmissionWebhook — the external-mutation webhook plugin
- ValidatingAdmissionPolicy — in-process CEL validation
- MutatingAdmissionPolicy — in-process CEL mutation
- Pod Security Admission — the
PodSecuritybuilt-in plugin in depth - ResourceQuota / LimitRange — the resource-management built-in plugins
- Kubernetes Authorization Modes — the stage before admission
- OPA Gatekeeper / Kyverno — webhook-based policy engines
- Kubernetes MOC — parent map (§12 Security)