Kubernetes admission controllers in 5 minutes | Sysdig
04-Admission-Controllers.md
Kubernetes: Admission Control
Kubernetes v1.36: Admission Policies That Can’t Be Deleted | Kubernetes
Admission Control in Kubernetes | Kubernetes
Manifest-Based Admission Control | Kubernetes
Core Idea
Every object request passes through admission control: mutating plugins rewrite it first, then validating plugins, webhooks, or policies decide whether it gets stored.
- Two phases in the request lifecycle: mutating (defaults and rewrites) runs before validating (policy checks plus schema validation).
- Four mechanisms: built-in plugins toggled in the admission config, dynamic mutating and validating webhooks, API-driven validating admission policies (e.g. require a cost center label), and manifest-based admission control added in v1.36.
- A common core of built-in plugins ships enabled by default; the rest must be switched on, configured per kubeadm cluster.
What is an Admission Controller ?
An admission controller is a piece of code that intercepts requests to the Kubernetes API server prior to persistence of the resource, but after the request is authenticated and authorized.
Admission controllers are plugins that govern and enforce how a cluster is used. They can:
- Reject requests that violate cluster policy
- Mutate (modify) incoming resource objects before they are stored
- Both mutate and validate in combination
When a request comes in (say, to create a pod), it goes through authentication, authorization, and then through admission controllers before being persisted.
- API Request → Authentication → Authorization → Mutating Admission → Schema Validation → Validating Admission → etcd
With Admission Controllers, you can
Admission controllers are the primary mechanism for cluster-wide policy enforcement. Common use cases include:
| Policy | Description |
|---|---|
| Registry allowlist | Only permit images from trusted internal registries |
| No root containers | Reject workloads running as UID 0 / root |
| Linux capabilities | Restrict or drop dangerous capabilities like NET_ADMIN |
| Required labels | Ensure every pod carries mandatory metadata (e.g. team, env) |
| Resource limits | Require CPU and memory limits on all containers |
| Namespace lifecycle | Prevent resources from being created in terminating namespaces |
Request Lifecycle
Admission controllers sit between authorization and persistence. They have full visibility into the resource being created or modified, and can alter or block it before it ever hits etcd.
Every API request passes through the following pipeline in order:

When a client (such as kubectl, a controller, or any application) sends a request to the Kubernetes API Server, the request goes through several stages before being persisted to storage:
flowchart TD
A[API Request]
subgraph Security
B[Authentication]
C[Authorization]
end
subgraph Request Processing
D[Decoding]
E[Mutating Admission]
F[Schema Validation]
G[Validating Admission]
end
subgraph Storage
H[Storage Version Conversion]
I[Persistence to etcd]
end
A --> B --> C --> D --> E --> F --> G --> H --> I- Authentication - Verifying the identity of the requester
- Authorization - Checking if the authenticated user has permission to perform the action
- Decoding - Converting the request payload into a structured object, setting default values, etc
- Mutating Admission Control - Making additional modifications to the request payload
- Schema Validation - Verifying that the request conforms to the expected OpenAPI schema
- Validating Admission Control - Performing additional validation or policy enforcement
- Storage Version - Converting the object to the storage version
- Storage - Persisting the object to ETCD
If any of these steps fail or explicitly reject the request, processing stops and the API server returns an error.
Important
Not all requests go through every step. Whether a request is processed by a particular stage depends on the type of request.
For example,Â
get,Âlist, andÂwatch requests bypass the admission control layer entirely.

Two Phases: Mutating vs Validating
The admission control process runs in two sequential phases:
Phase 1 - Mutating
Mutating Admission Control - Modifies incoming requests
- Controllers in this phase can modify the resource object.
- Useful for injecting defaults, adding sidecars, or enriching metadata.
- Example:
DefaultStorageClassinjects a default storage class into PVCs that don’t specify one.
Common use cases for mutating controllers include:
- Defaulting missing field values
- Injecting sidecar containers or volumes
- Modifying resource requests and limits
- Adding labels, annotations, or metadata
Phase 2 - Validating
Validating Admission Control - Validates requests without making modifications
- Controllers in this phase cannot modify the resource — they can only accept or reject it.
- Runs after mutation, so it always sees the fully-defaulted, final object.
- Example: A webhook that rejects pods without required labels.
Validating controllers can take one of three actions:
- Accept the request (allow it to proceed)
- Reject the request (return an error)
- Warn about policy violations while allowing the request to proceed
Tip
Order matters. Mutating always runs before validating. This ensures validation policies evaluate the complete, finalized resource — not a partial spec that would later be modified.
Schema Validation
Schema validation occurs between mutating and validating admission controllers in the API request pipeline. During this phase, the Kubernetes API server validates that the request object (after all mutations have been applied) conforms to the expected OpenAPI schema for that resource type.
This validation is distinct from earlier deserialization errors. Deserialization failures occur when the API server cannot parse the incoming request into a valid Kubernetes object structure due to:
- Malformed YAML or JSON
- Invalid data types in fields
- Unknown fields (in case of strict field validation)
In contrast, schema validation ensures that a successfully parsed object meets the specific requirements for its resource type.
Example: Deserialization would catch a malformed YAML file, while schema validation would catch a Pod that doesn’t have any containers.
Schema validation occurs after mutating admission controllers because mutations often add required fields or modify values to meet schema requirements, so the validator needs to see the final object state.
If schema validation fails, the API server immediately returns an error, and the request never reaches validating admission controllers or storage. This fail-fast approach prevents invalid objects from entering the cluster and provides clear feedback about what needs to be corrected.
The Four Types of Admission Controls
Kubernetes supports 4 main types of admission controls. Depending on your operational requirements, these range from fully hardcoded to entirely native-declarative.
Since admission controllers are compiled into the Kubernetes API server, you cannot create new custom admission controllers. However, there are two special controllers that allow you to customize admission control behavior:
- Webhooks (aka. Dynamic Admission Control) - External services called via HTTP requests
- Policies - Kubernetes objects that define rules using CEL (Common Expression Language) expressions
| Mechanism | Execution Context | How Rules are Defined |
|---|---|---|
| 1. Built-in | In-process | Hardcoded in Go (Compiled-in) |
| 2. Webhooks | Out-of-process | Custom external web server/pod |
| 3. API-driven VAP | In-process (CEL) | Declarative YAML (etcd-stored) |
| 4. Manifest VAP | In-process (CEL) | Static Files on apiserver Node |
1. Built-in Plugins
Kubernetes ships with a set of built-in admission controller plugins. Some are enabled by default.
These are hardcoded Go plugins built directly into the kube-apiserver binary.
Enable and Disable Plugin
👉 Static Pods
View Enabled Plugins:
kubectl exec kube-apiserver-controlplane -n kube-system -- kube-apiserver -h | grep enable-admission-pluginsEnable:
kube-apiserver --enable-admission-plugins=NamespaceLifecycle,LimitRangerDisable:
kube-apiserver --disable-admission-plugins=PodNodeSelector,AlwaysDenyConfigurationOn kubeadm Clusters
Edit the static pod manifest directly. The API server will restart automatically to pick up changes.
sudo vi /etc/kubernetes/manifests/kube-apiserver.yamlFind the --enable-admission-plugins or --disable-admission-plugins line under spec.containers[0].command and update it in place.
Plugins Enabled by Default (Common Core)
| Plugin | Type | Description |
|---|---|---|
NamespaceLifecycle | Validating | Prevents creation of resources in terminating or non-existent namespaces |
LimitRanger | Mutating + Validating | Enforces default resource limits and validates against LimitRange objects |
ServiceAccount | Mutating | Injects a default service account token into pods |
DefaultStorageClass | Mutating | Adds a default storage class to PVCs that don’t specify one |
ResourceQuota | Validating | Enforces namespace resource quotas |
MutatingAdmissionWebhook | Mutating | Enables custom mutating webhooks |
ValidatingAdmissionWebhook | Validating | Enables custom validating webhooks |
Not Enabled by Default
| Plugin | Type | Description |
|---|---|---|
AlwaysDeny | Validating | Denies all requests — useful for testing |
PodNodeSelector | Mutating + Validating | Controls which nodes pods can be scheduled on |
ImagePolicyWebhook | Validating | Delegates image policy decisions to an external webhook |
| Full list: kubernetes.io — Which plugins are enabled by default? |
2. Dynamic Admission Webhooks
If you need dynamic custom policy enforcement that is not built directly into Kubernetes, you can deploy external Admission Webhooks.
In order for the API server to call a webhook in the admission control pipeline, the webhook must be configured using one of the following Kubernetes resources:
Mutating Admission Webhook
- Runs in Phase 1 — can modify the object
- Common uses: inject sidecar containers(Istio > 4.1 The istio-init Container), add default labels, set security context defaults
Validating Admission Webhook
- Runs in Phase 2 — can only accept or reject
- Common uses: enforce naming conventions, require specific labels, block privileged containers
Architecture
When an API request matches a registered webhook’s rules, the API server serializes the request into an AdmissionReview JSON payload and sends an HTTP POST network request to your custom webhook server (such as Gatekeeper, Kyverno, or a custom application).
# Example: ValidatingWebhookConfiguration
apiVersion: admissionregistration.k8s.io/v1
kind: ValidatingWebhookConfiguration
metadata:
name: enforce-policy-webhook
webhooks:
- name: check-labels.example.com
clientConfig:
service: # Indicates the external service managing the webhook logic
name: webhook-validation-service
namespace: security-system
path: "/validate-labels"
rules:
- operations: ["CREATE", "UPDATE"]
apiGroups: [""]
apiVersions: ["v1"]
resources: ["pods"]
admissionReviewVersions: ["v1"]
sideEffects: None3. Validating Admission Policies (API-Driven)
Starting in v1.30 (GA), Kubernetes introduced Validating Admission Policies (ValidatingAdmissionPolicy). This is a declarative, in-process alternative to webhooks.
Policies are a relatively recent addiction to Kubernetes:
Why use them?
- No external network overhead: Runs compiled expressions directly inside the
kube-apiserverin-memory. - No web servers to manage: No certificates, deployment pipelines, scaling, or health checks needed for webhook pods.
- CEL Language: Uses Common Expression Language (CEL) to write fast, inline validation rules directly in your YAML specs.
Example: Requiring a Cost Center Label
To enforce policies, you declare a Policy specifying the validation expressions, and a Binding to connect that policy to specific namespaces or resources.
apiVersion: admissionregistration.k8s.io/v1
kind: ValidatingAdmissionPolicy
metadata:
name: force-cost-center
spec:
failurePolicy: Fail
matchConstraints:
resourceRules:
- apiGroups: [""]
apiVersions: ["v1"]
operations: ["CREATE", "UPDATE"]
resources: ["pods"]
validations:
- expression: "has(object.metadata.labels) && 'cost-center' in object.metadata.labels"
message: "Pods in this namespace must specify a 'cost-center' label."
---
apiVersion: admissionregistration.k8s.io/v1
kind: ValidatingAdmissionPolicyBinding
metadata:
name: force-cost-center-binding
spec:
policyName: force-cost-center
validationActions: [Deny]
matchResources:
namespaceSelector:
matchLabels:
environment: production4. Manifest-Based Admission Control (v1.36+)
Introduced in v1.36, Manifest-Based (File-Based) Admission Control is a design shift built to solve a critical limitation of API-driven admission systems: “Who guards the guardians?”
Manifest Based Admission Control Config · Issue #5793 · kubernetes/enhancements · GitHub
The Lockout Dilemma
Historically, dynamic webhooks and API-driven ValidatingAdmissionPolicies could not safely intercept operations on admission configurations (like deleting a webhook configuration).
- Why? If a validation rule could reject configurations, a bug in that rule or an outage on the webhook server could cause the entire cluster to lock up, with no way to delete the broken rule through the API.
- The Risk: This left standard security configurations vulnerable to deletion or tampering by standard administrators.
How Manifest-Based Control Solves This
Manifest-Based Admission Control loads policies directly from physical files on the control plane’s filesystem, bypassing the standard etcd database lookup entirely.
- Host-Level Configuration: The
kube-apiserveris configured to monitor a local static directory:
# Part of /etc/kubernetes/admission/admission-config.yaml
plugins:
- name: ValidatingAdmissionPolicy
configuration:
apiVersion: apiserver.config.k8s.io/v1
kind: ValidatingAdmissionPolicyConfiguration
staticManifestsDir: "/etc/kubernetes/admission/validating-policies/"- Naming Suffix: Static rules loaded from disk must end in
.static.k8s.io(e.g.,protect-policies.static.k8s.io). - Circular Dependency Resolved: Because these policies are loaded from disk, if an admin accidentally misconfigures a static policy, you do not lock yourself out. You simply SSH into the master node, modify or delete the local YAML manifest file on disk, and the API server instantly reloads the change.
Example: Protecting API-Driven Admission Policies from Deletion
Since Manifest-Based policies bypass the API server’s webhook lockout mechanisms, they can be safely used to prevent cluster admins from modifying or deleting your primary API-driven policies.
apiVersion: admissionregistration.k8s.io/v1
kind: ValidatingAdmissionPolicy
metadata:
name: "protect-policies.static.k8s.io" # Must use the .static.k8s.io suffix
spec:
failurePolicy: Fail
matchConstraints:
resourceRules:
- apiGroups: ["admissionregistration.k8s.io"]
apiVersions: ["*"]
operations: ["DELETE", "UPDATE"]
resources:
- "validatingadmissionpolicies"
- "validatingadmissionpolicybindings"
- "validatingwebhookconfigurations"
validations:
- expression: >-
!has(oldObject.metadata.labels) ||
!('[platform.example.com/protected](https://platform.example.com/protected)' in oldObject.metadata.labels) ||
oldObject.metadata.labels['[platform.example.com/protected](https://platform.example.com/protected)'] != 'true'
message: "Deletion or alteration of protected admission policies is strictly prohibited."
---
apiVersion: admissionregistration.k8s.io/v1
kind: ValidatingAdmissionPolicyBinding
metadata:
name: "protect-policies-binding.static.k8s.io"
spec:
policyName: "protect-policies.static.k8s.io"
validationActions: [Deny]Architectural Comparison
Here is an architectural comparison of the main admission mechanisms across Kubernetes clusters:
| Feature | Admission Webhooks | API-Driven Policies (VAP) | Manifest-Based Policies (v1.36+) |
|---|---|---|---|
| Storage Layer | etcd API configuration | etcd API configuration | Local Filesystem (kube-apiserver host) |
| Modification Layer | Kubernetes API (kubectl) | Kubernetes API (kubectl) | Direct file updates via SSH/Host access |
| Execution Engine | External web server | Native In-Process CEL engine | Native In-Process CEL engine |
| Performance Overhead | High (Network latency, JSON serializing) | Extremely Low (In-memory) | Extremely Low (In-memory) |
| Complexity & Maintenance | High (Certificates, scaling, monitoring) | Zero (Pure declarative YAML) | Zero (Pure declarative YAML) |
| State Dependencies | High (Requires active pods) | Medium (Supports paramKind data bindings) | None (Self-contained, cannot access outside cluster resources) |
| Policy Self-Protection | No (Cannot intercept own configs) | No (Cannot intercept own configs) | Yes (Safely protects other webhooks/policies from deletion) |
Key Mental Models
Mutate first, validate second.
This is non-negotiable in the pipeline. It means your validating policies always see a fully-defaulted, complete resource — not a skeleton that would have been enriched later.
Rejection is immediate and final.
If any validating controller rejects a request, the whole request fails. The API server does not continue to other controllers.
Webhooks are external services.
The API server calls your webhook over HTTPS. Your webhook must be reachable from the control plane and must serve a valid TLS certificate signed by the
caBundleyou configure. UsefailurePolicy: Ignorecarefully — it silently bypasses your policy when the webhook is down.
Built-in plugins are compiled in; webhooks are runtime extensions.
Built-in plugins run as part of the API server binary. Webhooks run as separate services that you deploy and manage. For most custom policies, webhooks are the right tool — consider frameworks like OPA (Open Policy Agent)/Gatekeeper or Kyverno rather than writing raw webhook servers.
Day 51 - Kubernetes Admission Controller Explained With Demo