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
Mutating vs Validating
| Mutating Admission | Validating Admission | |
|---|---|---|
| Can modify? | ✅ Yes — add/change/remove fields | ❌ No — can only accept or reject |
| Order | Runs first (before validation) | Runs after mutations + schema validation |
| Sees | Original request | Final object (after all mutations) |
| Use cases | Inject sidecars, add labels, set defaults | Enforce policies, block violations |
| Examples | LimitRanger (sets defaults), Istio (injects sidecar) | ResourceQuota (blocks if exceeded), PodSecurity |
2. Built-in Admission Controllers
Kubernetes ships with ~30 built-in admission controllers. The most important ones:
| Controller | Type | What It Does |
|---|---|---|
| LimitRanger | Mutating + Validating | Injects default resource requests/limits; rejects if below min or above max |
| ResourceQuota | Validating | Rejects if namespace quota would be exceeded |
| PodSecurity | Validating | Enforces Pod Security Standards (Restricted/Baseline) |
| NamespaceLifecycle | Validating | Blocks operations in terminating namespaces; prevents deleting system namespaces |
| ServiceAccount | Mutating | Assigns default SA, mounts projected token volume |
| DefaultStorageClass | Mutating | Adds default StorageClass to PVCs that don't specify one |
| MutatingAdmissionWebhook | Mutating | Calls external webhook services for custom mutations |
| ValidatingAdmissionWebhook | Validating | Calls external webhook services for custom validation |
| NodeRestriction | Validating | Limits 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
/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
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]
Summary
| Concept | Key Point |
|---|---|
| Admission Controllers | Plugins between AuthZ and etcd — mutate or validate requests |
| Mutating | Runs first — can add/change/remove fields from the object |
| Validating | Runs after — can only accept or reject the final object |
| Built-in | ~30 compiled into API server (LimitRanger, ResourceQuota, PodSecurity, NodeRestriction) |
| Webhooks | External services called by API server for custom logic |
| ValidatingAdmissionPolicy | In-cluster validation via CEL expressions — no webhook needed (1.28+) |
| NodeRestriction | Limits 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?
Q2: Can a validating admission controller modify the Pod spec (e.g., add a label)?
Q3: What would happen if the NodeRestriction admission controller was disabled?
Q4: You need all Deployments to have a team label. What's the simplest approach in K8s 1.28+?
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?
Q6: The API server has --enable-admission-plugins=NodeRestriction. You want to also enable PodSecurity. What do you do?
/etc/kubernetes/manifests/kube-apiserver.yaml) and add PodSecurity to the comma-separated list:--enable-admission-plugins=NodeRestriction,PodSecuritySave 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.