Admission controllers are plugins that intercept API requests after authentication and authorization but before the object is persisted to etcd. They can modify (mutate) the request, reject it (validate), or both. They're the enforcement mechanism behind LimitRanges, ResourceQuotas, Pod Security, and custom policies.

1. Where Admission Controllers Sit

AuthN AuthZ Mutating Can MODIFY object Validation Validating Can only REJECT etcd Response

Mutating vs Validating

Mutating AdmissionValidating Admission
Can modify?✅ Yes — add/change/remove fields❌ No — can only accept or reject
OrderRuns first (before validation)Runs after mutations + schema validation
SeesOriginal requestFinal object (after all mutations)
Use casesInject sidecars, add labels, set defaultsEnforce policies, block violations
ExamplesLimitRanger (sets defaults), Istio (injects sidecar)ResourceQuota (blocks if exceeded), PodSecurity
Mutating runs before validating — this is by design. A mutating controller might inject fields that the validating controller then checks. Example: LimitRanger (mutating) adds default resource limits → ResourceQuota (validating) checks if the total exceeds the quota. The validating controller sees the mutated, final version.

2. Built-in Admission Controllers

Kubernetes ships with ~30 built-in admission controllers. The most important ones:

ControllerTypeWhat It Does
LimitRangerMutating + ValidatingInjects default resource requests/limits; rejects if below min or above max
ResourceQuotaValidatingRejects if namespace quota would be exceeded
PodSecurityValidatingEnforces Pod Security Standards (Restricted/Baseline)
NamespaceLifecycleValidatingBlocks operations in terminating namespaces; prevents deleting system namespaces
ServiceAccountMutatingAssigns default SA, mounts projected token volume
DefaultStorageClassMutatingAdds default StorageClass to PVCs that don't specify one
MutatingAdmissionWebhookMutatingCalls external webhook services for custom mutations
ValidatingAdmissionWebhookValidatingCalls external webhook services for custom validation
NodeRestrictionValidatingLimits kubelet to only modify its own Node object and its Pods

Checking Enabled Controllers

# See which admission controllers are enabled:
kubectl exec -n kube-system kube-apiserver-master -- \
  kube-apiserver --help | grep enable-admission

# Or check the API server manifest:
cat /etc/kubernetes/manifests/kube-apiserver.yaml | grep admission
# --enable-admission-plugins=NodeRestriction,PodSecurity,...

# Default enabled set (K8s 1.28+):
# CertificateApproval, CertificateSigning, CertificateSubjectRestriction,
# DefaultIngressClass, DefaultStorageClass, DefaultTolerationSeconds,
# LimitRanger, MutatingAdmissionWebhook, NamespaceLifecycle,
# PersistentVolumeClaimResize, PodSecurity, Priority,
# ResourceQuota, RuntimeClass, ServiceAccount, StorageObjectInUseProtection,
# TaintNodesByCondition, ValidatingAdmissionPolicy,
# ValidatingAdmissionWebhook

Enabling/Disabling Controllers

# In kube-apiserver manifest:
spec:
  containers:
    - command:
        - kube-apiserver
        - --enable-admission-plugins=NodeRestriction,PodSecurity,ResourceQuota
        - --disable-admission-plugins=AlwaysDeny
CKS exam: you may be asked to enable a specific admission controller. Edit /etc/kubernetes/manifests/kube-apiserver.yaml and add the controller name to --enable-admission-plugins. The API server auto-restarts (it's a static Pod). Verify it's running: kubectl get pods -n kube-system.
MutatingAdmissionWebhook and ValidatingAdmissionWebhook are the extensibility points. They delegate to YOUR services (running in-cluster) to implement custom admission logic. This is how Istio injects sidecars, Gatekeeper enforces policies, and cert-manager validates certificates. They're always enabled by default.

3. Built-in Controllers in Action

LimitRanger — Mutating + Validating

# Namespace has a LimitRange:
# default.memory: 256Mi, defaultRequest.memory: 128Mi, max.memory: 1Gi

# User creates Pod WITHOUT resource spec:
kubectl run nginx --image=nginx
# LimitRanger MUTATES: injects requests.memory=128Mi, limits.memory=256Mi

# User creates Pod with limits.memory: 2Gi:
# LimitRanger VALIDATES: 2Gi > max 1Gi → REJECTED

ResourceQuota — Validating

# Namespace quota: requests.cpu: 4, used: 3.5
# User creates Pod with requests.cpu: 600m
# ResourceQuota VALIDATES: 3.5 + 0.6 = 4.1 > 4 → REJECTED
# "exceeded quota: compute-quota, requested: cpu=600m, used: cpu=3500m, limited: cpu=4"

ServiceAccount — Mutating

# User creates a Pod without specifying serviceAccountName:
# ServiceAccount controller MUTATES:
#   - Sets spec.serviceAccountName: default
#   - Adds projected volume for SA token
#   - Adds volumeMount to each container

# This is why every Pod has /var/run/secrets/kubernetes.io/serviceaccount/
# — it's injected by the ServiceAccount admission controller

NodeRestriction — Validating (CKS Important)

# Without NodeRestriction:
# A compromised kubelet could modify ANY node object or read ANY Pod/Secret

# With NodeRestriction (enabled by default):
# kubelet on worker-1 can ONLY:
#   - Modify its own Node object (node/worker-1)
#   - Create/modify/delete Pods bound to its own node
#   - Read Secrets/ConfigMaps referenced by Pods on its node
# → A compromised node can't attack other nodes' workloads
NodeRestriction is critical for cluster security. Without it, a compromised kubelet (via container escape) could modify other nodes' labels (redirecting workloads), read other namespaces' Secrets, or disrupt the entire cluster. Always keep NodeRestriction enabled. It's the wall between a single-node compromise and a cluster-wide breach.

4. Dynamic Admission — Webhooks (Preview)

Built-in controllers are compiled into the API server. For custom logic, you use admission webhooks — your own services that the API server calls during admission. This is covered in depth in the next lesson.

# Two types of webhook configurations:
# MutatingWebhookConfiguration → your service can modify the object
# ValidatingWebhookConfiguration → your service can accept/reject

# Real-world examples:
# - Istio: MutatingWebhook that injects Envoy sidecar into every Pod
# - Gatekeeper: ValidatingWebhook that enforces OPA policies
# - cert-manager: ValidatingWebhook that checks Certificate resources
# - Kyverno: Both mutating and validating for policy enforcement

ValidatingAdmissionPolicy (K8s 1.28+ GA) — No Webhook Needed

# New: express validation rules directly in K8s objects using CEL:
apiVersion: admissionregistration.k8s.io/v1
kind: ValidatingAdmissionPolicy
metadata:
  name: require-labels
spec:
  matchConstraints:
    resourceRules:
      - apiGroups: ["apps"]
        resources: ["deployments"]
        apiVersions: ["v1"]
        operations: ["CREATE", "UPDATE"]
  validations:
    - expression: "has(object.metadata.labels) && 'team' in object.metadata.labels"
      message: "All Deployments must have a 'team' label"
---
apiVersion: admissionregistration.k8s.io/v1
kind: ValidatingAdmissionPolicyBinding
metadata:
  name: require-labels-binding
spec:
  policyName: require-labels
  validationActions: [Deny]
ValidatingAdmissionPolicy is a game-changer. Before this, any custom validation required running a webhook service (Deployment + Service + TLS). Now you can express policies as K8s objects with CEL expressions — no external service, no network latency, no availability dependency. For simple validation rules, prefer this over Gatekeeper/Kyverno.
Use this layered approach: (1) Built-in controllers (LimitRanger, ResourceQuota, PodSecurity) for standard enforcement. (2) ValidatingAdmissionPolicy for simple custom rules (label requirements, naming conventions). (3) Gatekeeper/Kyverno webhooks for complex policies (image scanning, network policy requirements, custom business logic).

Summary

ConceptKey Point
Admission ControllersPlugins between AuthZ and etcd — mutate or validate requests
MutatingRuns first — can add/change/remove fields from the object
ValidatingRuns after — can only accept or reject the final object
Built-in~30 compiled into API server (LimitRanger, ResourceQuota, PodSecurity, NodeRestriction)
WebhooksExternal services called by API server for custom logic
ValidatingAdmissionPolicyIn-cluster validation via CEL expressions — no webhook needed (1.28+)
NodeRestrictionLimits kubelet to only its own node's resources — critical for security
Enable/disable--enable-admission-plugins in API server manifest

📝 Quiz: Admission Controllers

Q1: A namespace has a LimitRange and a ResourceQuota. A Pod is created without resource specs. What's the order of operations?

(1) LimitRanger (mutating) injects default resource requests/limits into the Pod. (2) Schema validation confirms the object is well-formed. (3) ResourceQuota (validating) checks if the namespace total (including the newly-injected defaults) exceeds the quota. If it does → rejected. If not → persisted to etcd.

Q2: Can a validating admission controller modify the Pod spec (e.g., add a label)?

No. Validating admission controllers can only accept or reject. They cannot modify the object. If you need to add/change fields, use a mutating admission controller (or webhook). Mutating runs before validating, so the validating controller sees the already-modified version.

Q3: What would happen if the NodeRestriction admission controller was disabled?

A compromised kubelet (via container escape) could: (1) Modify other nodes' labels (redirecting scheduled workloads). (2) Read Secrets from any namespace (not just Pods on its node). (3) Create Pods on other nodes. (4) Escalate from single-node compromise to full cluster compromise. NodeRestriction contains the blast radius of a kubelet compromise.

Q4: You need all Deployments to have a team label. What's the simplest approach in K8s 1.28+?

Use a ValidatingAdmissionPolicy with a CEL expression:
validations:
  - expression: "'team' in object.metadata.labels"
    message: "Deployments must have a 'team' label"
No webhook service needed — it's evaluated directly by the API server. Bind it to the cluster with a ValidatingAdmissionPolicyBinding. Simpler than deploying Gatekeeper/Kyverno for this use case.

Q5: How does Istio inject the Envoy sidecar into every Pod without users modifying their Deployments?

Istio uses a MutatingAdmissionWebhook. When a Pod is created in an Istio-labeled namespace, the API server calls Istio's webhook service. The webhook modifies the Pod spec to add the Envoy sidecar container (and init container). The user's original manifest is unchanged — the sidecar is transparently injected during admission.

Q6: The API server has --enable-admission-plugins=NodeRestriction. You want to also enable PodSecurity. What do you do?

Edit the API server manifest (/etc/kubernetes/manifests/kube-apiserver.yaml) and add PodSecurity to the comma-separated list:
--enable-admission-plugins=NodeRestriction,PodSecurity
Save the file — the static Pod auto-restarts. Note: in K8s 1.25+, PodSecurity is enabled by default. You only need to manually enable it if it was explicitly removed or you're on an older version.