Module 6: Runtime Security Admission Control And Image Scanning
2. What is an admission controller, and why it runs before the object exists
Description
kube-apiserver, since Module 1, lesson 6, is "the cluster's single entry door." That same lesson already foreshadowed, in one sentence, this module's destination: "Everything — kubectl, any operator like ArgoCD (Module 5), any admission controller (Module 6) — talks to Kubernetes exclusively through this component (...) and — most important for Module 6 — it's the exact point where admission control runs before an object gets stored." This lesson unpacks that sentence word by word: exactly which steps a kubectl apply goes through before reaching etcd, exactly where an admission controller stands within those steps, and why that — and nothing else — is what makes this layer "preventive" in the exact same sense cloud-security-and-guardrails-guide already used that word.
Connection to the module
This lesson is purely conceptual — no new command, no manifest — and it exists for the same reason cloud-security-and-guardrails-guide's Module 4, lesson 2 was purely conceptual before installing conftest: so that when lesson 4 has you actually install Gatekeeper, you're not learning the complete request lifecycle and a ConstraintTemplate's syntax at the same time.
Analogy: the gatekeeper, with the full cycle this time
Lesson 1 left the gatekeeper standing at the door, checking a list. This lesson opens that scene in slow motion, with the exact steps that happen before anyone crosses the threshold: first, someone at the entrance confirms you are who you say you are — your ID, not your intent — (authentication); then, someone else checks whether that identity, specifically, is allowed into this section of the building — maybe you have a valid ID, but your card doesn't open floor 12 (authorization); and only then, with identity and permission already confirmed, lesson 1's gatekeeper checks a different list — not who you are, but what you're carrying: does the suitcase meet the weight limit? does the package have the right label? (admission control). Only then, if all three doors were crossed with no objection, does the event get logged in the building's official record (etcd). Changing any of the first three steps — your identity, your permission, or your suitcase's contents — never changes the fact that the record only exists after the three checks, never before.
The complete lifecycle of a request against kube-apiserver
When you run kubectl apply -f bad-pod-no-limits.yaml, that YAML doesn't reach etcd in one jump. It goes through, in strict order, four phases inside kube-apiserver — and all four are part of the same component, the same process, the same "single entry door" from Module 1:
kubectl apply -f bad-pod-no-limits.yaml
│
▼
┌─────────────────────────────────────────────────────────────────┐
│ kube-apiserver │
│ │
│ 1. AUTHENTICATION who are you? (certificate, token) │
│ │ fails → 401 Unauthorized │
│ ▼ │
│ 2. AUTHORIZATION (RBAC) do you have permission for this action? │
│ │ fails → 403 Forbidden │
│ ▼ │
│ 3. ADMISSION CONTROL is this specific object allowed │
│ │ to exist? (MutatingWebhook, then │
│ │ ValidatingWebhook — THIS MODULE) │
│ │ fails → 403 Forbidden, "admission webhook│
│ │ denied the request" │
│ ▼ │
│ 4. PERSISTENCE the object gets written to etcd │
└─────────────────────────────────────────────┬─────────────────────┘
▼
etcd
(the object EXISTS, only now)
Notice something important: until this module, your requests always went through phases 1, 2, and 4 with no objection — your kind kubeconfig authenticates you automatically as kubernetes-admin, with a ClusterRoleBinding that authorizes you for everything. Phase 3, admission control, technically existed since this guide's very first kubectl apply (kind ships with some internal admission controllers enabled by default, like NamespaceLifecycle or LimitRanger), but it never had any policy of yours loaded — it never rejected anything because you never asked it to evaluate anything. This module doesn't invent phase 3: it populates it, for the first time, with rules you wrote.
MutatingWebhookConfiguration and ValidatingWebhookConfiguration, by their exact name
The admission control phase actually has two sub-phases, in strict order:
MutatingWebhookConfiguration— runs first. A webhook of this type can modify the object before it continues — for example, injecting a label, or a missing default value. Gatekeeper and Kyverno register webhooks of this type (Gatekeeper uses them for itsAssign/ModifySetengine, outside this module's scope), but neither of this module's policies mutates anything — the two policies you're going to build (lessons 4 and 6) are purely validating.ValidatingWebhookConfiguration— runs afterward, on the already-mutated object (if something mutated it). A webhook of this type can only respondallowed: trueorallowed: false— it changes nothing, it only decides whether the object passes. This is the exact piece Gatekeeper and Kyverno register for this module's policies: when you install Gatekeeper (lesson 4), you're going to see an object appear literally calledgatekeeper-validating-webhook-configuration; when you install Kyverno (lesson 6), you're going to seekyverno-resource-validating-webhook-cfg. Both names, with thevalidating-webhooksuffix, give away exactly which sub-phase they act in.
When a ValidatingWebhookConfiguration responds allowed: false, kube-apiserver never reaches phase 4. The object you rejected doesn't end up "stored and marked as invalid" anywhere — it simply never existed. There's no record of "this Pod was rejected" in etcd, because etcd never knew it existed. The only evidence the attempt happened is the error message kubectl returned in your own terminal, at the exact moment of the attempt — you're going to see that message, literal, in lesson 4.
Why this is preventive, not detective
cloud-security-and-guardrails-guide, in its Module 7, left this distinction with a formal name: "A preventive guardrail couldn't be bypassed. It acts before the event exists (...) A detective guardrail leaves a record. It acts afterward: it didn't stop anything, but it leaves queryable evidence that something happened." The diagram above is the proof that an admission controller falls, unambiguously, into the first category: the rejection happens in phase 3, before phase 4 (persistence in etcd). There's no moment where the rejected object "briefly existed" before being deleted — the rejection prevents it from existing, period.
Contrast this with what a detective guardrail would do with the same scenario: if instead of a ValidatingWebhookConfiguration you only had Kubernetes' audit logs (kube-apiserver can log every request, accepted or not, to a file — outside this module's scope, analogous to what CloudTrail does on the AWS side), that Pod with no resource limits would exist, running, consuming memory with no ceiling, and the audit log would only let you answer, after the fact, "who created this Pod, and when?" — never "prevent" it from being created. The same guide that already built that vocabulary with CloudTrail gives you, here, the exact mirror example: CloudTrail is the camera; this module's admission controller is the gatekeeper. Both exist in the same ecosystem, solving the same general problem — "what happened, or what's about to happen?" — on completely different layers.
| Admission control (this module) | Kubernetes audit logs (out of scope) | CloudTrail (cloud-security-and-guardrails-guide, M7) | |
|---|---|---|---|
| Category | Preventive | Detective | Detective |
| When it acts | Before phase 4 (persistence in etcd) | After any phase, logging what happened | After any AWS API call |
| What it can do | Reject the object — it never gets to exist | Nothing — it only leaves a record | Nothing — it only leaves a record |
| Question it answers | "Is it allowed to exist?" | "What exactly happened?" | "What exactly happened?" |
Common mistakes
Thinking an object rejected by admission control "existed for an instant" before getting deleted (an incorrect mental model). What happens: someone, seeing the admission webhook denied the request message, imagines Kubernetes created the object and then deleted it. How to spot it: if your explanation of what happened includes the word "deleted" or "removed." How to fix it: look at this lesson's diagram — the admission control phase (3) happens before the persistence phase (4). A rejected object never reaches phase 4; there's nothing to delete because it was never written. kubectl get pods is never going to show it, not even for a fraction of a second — there's no intermediate state at all.
Confusing authorization (RBAC) with admission control (mixed layers). What happens: someone assumes that if kubectl lets you run the command (RBAC authorizes you), the object necessarily gets created. How to spot it: if your reasoning is "I have cluster-admin permission, so nothing can stop me." How to fix it: RBAC (phase 2) answers a different question from admission control (phase 3) — RBAC decides whether you, as an identity, can attempt to create a Pod in general; admission control decides whether that specific Pod, with that exact content, is allowed to exist. Being cluster-admin guarantees you always pass phase 2 — it doesn't exempt you from phase 3. Lesson 4 confirms it live: you're going to try to create a Pod with the cluster's full admin credentials, and Gatekeeper is going to reject it anyway.
Assuming admission control is exclusive to Gatekeeper/Kyverno (incomplete scope). What happens: someone concludes that without installing one of the two engines, phase 3 of the lifecycle simply doesn't exist. How to spot it: if your summary of this lesson is "admission control = Gatekeeper or Kyverno." How to fix it: Kubernetes ships internal admission controllers, compiled into kube-apiserver's own binary, active by default in any cluster — including kind — from day one (NamespaceLifecycle, which prevents creating objects in a namespace being deleted; LimitRanger, which applies default limits declared with a LimitRange object, if one existed). Gatekeeper and Kyverno don't create phase 3 — they connect to it, via ValidatingWebhookConfiguration, to add their own rules on top of the ones already there by default.
Exercises
Exercise 1 — Order the four phases without looking at the diagram. Without looking back at this lesson's diagram, order these four phases in the correct sequence kube-apiserver executes them: (a) persistence in etcd; (b) admission control; (c) authorization (RBAC); (d) authentication.
See solution
The correct order is: (d) authentication → (c) authorization (RBAC) → (b) admission control → (a) persistence in etcd. The mnemonic: first confirm who you are (authentication), then whether you have general permission for the action (authorization), then whether that specific object is allowed to exist (admission control), and only at the end does it get stored (persistence). If you flipped (b) and (c), remember: RBAC never looks at the content of the object you're creating, only the verb (create, get, delete) and the resource type (pods, deployments) — admission control is the only phase that inspects the full YAML content.
Exercise 2 — Explain, without using the word "preventive," why admission control isn't the same as an audit log. A colleague, after reading about Kubernetes audit logs, asks: "isn't it the same thing? Both check every request." What would you answer, without using the word "preventive" or "detective"?
See solution
A reasonable explanation: "Both do see every request, that's true, but at different points in the process and with different capabilities. An audit log activates after kube-apiserver already decided what to do with the request — it only describes what happened, it can't change the outcome. An admission controller activates before that final decision, and its response (allowed: true or false) is what determines whether the object gets to exist. If the admission controller says no, there's nothing to audit about 'a Pod that got created,' because that Pod never got created." If your explanation distinguishes "can change the outcome" from "only describes the outcome," you have the right criterion without needing the formal vocabulary.
Exercise 3 — Predict what would happen if Gatekeeper itself went down. If the gatekeeper-controller-manager Deployment (which you're going to install in lesson 4) stopped responding — for example, if its three replicas all went down at once — what would happen to a kubectl apply for a new Pod against andes-cargo-cluster? Think about a ValidatingWebhookConfiguration's failurePolicy field before answering.
See solution
It depends exactly on the value of failurePolicy in Gatekeeper's ValidatingWebhookConfiguration. The official v3.23.0 manifest (the same one you're going to apply in lesson 4) declares the main webhook — validation.gatekeeper.sh, the one that evaluates every Constraint — with failurePolicy: Ignore: if Gatekeeper doesn't respond in time, kube-apiserver proceeds as if the webhook had said "yes," and the Pod gets created anyway, with no Constraint having evaluated it. It's a deliberate availability-over-security decision: a whole cluster blocked because the policy engine is down would, for most teams, be an operational risk worse than temporarily letting an unreviewed object through. (The same manifest does ship a second webhook, check-ignore-label.gatekeeper.sh — one that only checks an administrative label, not your Constraints — that one with failurePolicy: Fail; don't confuse it with the one that evaluates this module's policies.) This is precisely why Gatekeeper gets installed with three replicas of its controller (you're going to confirm this yourself in lesson 4): to minimize the windows where the guardrail is, even briefly, open.
Summary and next step
This lesson unpacked, in slow motion, the sentence Module 1 left open: kube-apiserver processes every request in four strict phases — authentication, authorization, admission control, persistence in etcd — and an admission controller stands exactly at the third one, before any object gets to actually exist. You saw the exact names of the two pieces Gatekeeper and Kyverno register (MutatingWebhookConfiguration and ValidatingWebhookConfiguration), and why that makes this layer preventive, in the exact same sense cloud-security-and-guardrails-guide already used that word to distinguish SCPs from CloudTrail.
Before moving on you should be able to: order the request lifecycle's four phases from memory; explain the difference between MutatingWebhookConfiguration and ValidatingWebhookConfiguration; and predict what would happen if a policy engine's Deployment went down, based on its failurePolicy.
Next lesson: OPA Gatekeeper, ConstraintTemplate, and Constraint. There you're going to see the exact syntax through which Rego — the same declarative language cloud-security-and-guardrails-guide used with conftest — gets packaged inside a Kubernetes CRD, and the precise technical difference between evaluating a static file outside the cluster and evaluating a live object inside it.
Resources
kubernetes-and-eks-in-production-guide(NIEVA), Module 1, lesson 6 — the original citation aboutkube-apiserveras the single entry door, this lesson's foundation.cloud-security-and-guardrails-guide(NIEVA), Module 7, lesson 1 — the preventive/detective vocabulary, picked back up here with a new central example.- Kubernetes — Controlling Access to the Kubernetes API — the complete lifecycle of a request, official documentation.
- Kubernetes — Dynamic Admission Control — official reference for
MutatingWebhookConfigurationandValidatingWebhookConfiguration, the two pieces Gatekeeper and Kyverno register. - Kubernetes — Admission Controllers Reference — the complete list of internal admission controllers
kindalready ships enabled by default.