kubernetes kubernetes/cka

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:

PolicyDescription
Registry allowlistOnly permit images from trusted internal registries
No root containersReject workloads running as UID 0 / root
Linux capabilitiesRestrict or drop dangerous capabilities like NET_ADMIN
Required labelsEnsure every pod carries mandatory metadata (e.g. team, env)
Resource limitsRequire CPU and memory limits on all containers
Namespace lifecyclePrevent 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
  1. Authentication - Verifying the identity of the requester
  2. Authorization - Checking if the authenticated user has permission to perform the action
  3. Decoding - Converting the request payload into a structured object, setting default values, etc
  4. Mutating Admission Control - Making additional modifications to the request payload
  5. Schema Validation - Verifying that the request conforms to the expected OpenAPI schema
  6. Validating Admission Control - Performing additional validation or policy enforcement
  7. Storage Version - Converting the object to the storage version
  8. 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: DefaultStorageClass injects 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:

MechanismExecution ContextHow Rules are Defined
1. Built-inIn-processHardcoded in Go (Compiled-in)
2. WebhooksOut-of-processCustom external web server/pod
3. API-driven VAPIn-process (CEL)Declarative YAML (etcd-stored)
4. Manifest VAPIn-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-plugins

Enable:

kube-apiserver --enable-admission-plugins=NamespaceLifecycle,LimitRanger

Disable:

kube-apiserver --disable-admission-plugins=PodNodeSelector,AlwaysDeny

ConfigurationOn 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.yaml

Find 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)

PluginTypeDescription
NamespaceLifecycleValidatingPrevents creation of resources in terminating or non-existent namespaces
LimitRangerMutating + ValidatingEnforces default resource limits and validates against LimitRange objects
ServiceAccountMutatingInjects a default service account token into pods
DefaultStorageClassMutatingAdds a default storage class to PVCs that don’t specify one
ResourceQuotaValidatingEnforces namespace resource quotas
MutatingAdmissionWebhookMutatingEnables custom mutating webhooks
ValidatingAdmissionWebhookValidatingEnables custom validating webhooks

Not Enabled by Default

PluginTypeDescription
AlwaysDenyValidatingDenies all requests — useful for testing
PodNodeSelectorMutating + ValidatingControls which nodes pods can be scheduled on
ImagePolicyWebhookValidatingDelegates 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

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: None

3. 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-apiserver in-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: production

4. 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.

  1. Host-Level Configuration: The kube-apiserver is 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/"
  1. Naming Suffix: Static rules loaded from disk must end in .static.k8s.io (e.g., protect-policies.static.k8s.io).
  2. 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:

FeatureAdmission WebhooksAPI-Driven Policies (VAP)Manifest-Based Policies (v1.36+)
Storage Layeretcd API configurationetcd API configurationLocal Filesystem (kube-apiserver host)
Modification LayerKubernetes API (kubectl)Kubernetes API (kubectl)Direct file updates via SSH/Host access
Execution EngineExternal web serverNative In-Process CEL engineNative In-Process CEL engine
Performance OverheadHigh (Network latency, JSON serializing)Extremely Low (In-memory)Extremely Low (In-memory)
Complexity & MaintenanceHigh (Certificates, scaling, monitoring)Zero (Pure declarative YAML)Zero (Pure declarative YAML)
State DependenciesHigh (Requires active pods)Medium (Supports paramKind data bindings)None (Self-contained, cannot access outside cluster resources)
Policy Self-ProtectionNo (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 caBundle you configure. Use failurePolicy: Ignore carefully — 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.