Kubernetes admission control for container images: what to check at the door.
A validating admission webhook is the one place every pod passes through before it runs, whoever applied it. This guide is about what that gate should ask of a container image, what to do when it has no answer yet, and how to check that it is really standing.
Why the door, not the postmortem
Runtime security tells you a container started behaving oddly at three in the morning. Admission control decides whether it starts at all. The two answer different questions, and the second one is cheaper to get right: at admission the API server is already waiting for a yes or no, the image reference is right there in the pod spec, and a refusal costs a failed deploy rather than an incident.
Because the check sits in the API server's admission chain, it does not matter who does the applying — kubectl, Argo CD, Flux, a Job, an operator reconciling its own CRD. Every pod CREATE and UPDATE outside a short list of bypass namespaces passes the same webhook, and a GitOps sync that fails policy fails with the denial message as its error, which is exactly where you want to read it.
The five checks
These are the questions an image gate can answer from the pod spec and a scan cache, in the order they are worth turning on. The first three need no scanner at all.
-
Tag hygiene. A mutable tag can change what runs without anyone deploying.
Refusing
:latestis the smallest possible rule and the one most worth having on day one. - Registry allow-list. Anything not from your registries is refused outright. An empty list allows any registry; a non-empty one refuses all others.
- Digest pinning. Three levels: allow tags, prefer a digest, require one. Requiring a digest is the strongest claim about what ran and the strictest rollout, so most estates start at "prefer" and tighten per namespace.
- Vulnerability thresholds. Refuse on any critical finding, on any high finding, or past a ceiling of mediums and lows. This is the check that needs a scanner behind it, and it is where the next section matters.
- Signatures and attestations. Require a verified cosign signature, an SBOM attestation, a vulnerability-scan attestation, and refuse attestations older than a maximum age. Turn these on once your build pipeline actually produces them; before that they refuse everything.
On top of the image itself, the same review can look at the workload: privileged containers, hostPath mounts and their relatives are hardening findings, and a policy decides whether they refuse, warn, or pass.
An image nobody has scanned yet
Scanning takes minutes; the API server waits seconds. So a gate that scans inline is a gate that eventually times out and takes deployments with it. The workable design keeps the two apart: a scanner fills a cache in the background, and admission reads the cache. A slow registry then cannot slow down a deployment.
The consequence you have to decide on before you install: a digest nobody has seen before has no
scan result yet. There are two honest answers, and in Attestkeep the policy field
admissionTiming picks one:
- Allow and scan. Admit the unknown image, queue the scan immediately, and decide on the finished report at every later admission of that digest. Adoption-friendly, and the default: nothing you deploy on day one is refused for being new.
- Deny until scanned. Refuse the unknown image with "a scan has been queued — retry shortly", and admit it once the report exists. Every deploy of a new digest costs one retry. Choose it for the namespaces that have earned it.
Either way the first scan of a new digest runs in the background. On a fresh cluster it can take a few minutes — the scanner pulls the image and the vulnerability database is cold — and until it finishes the image is unknown, never compliant. Both settings are available in every Attestkeep edition, Community included.
Fail open or fail closed
The other posture decision is not about the image; it is about the gate itself. What should the
API server do when your webhook does not answer? Kubernetes calls this the webhook's
failurePolicy, and neither value is universally right.
- Ignore (fail open) keeps the cluster deployable during an outage of the webhook, and leaves a gap: for that window nothing was reviewed. A control worth trusting records that window rather than quietly showing a clean quarter.
- Fail (fail closed) gives you a control that provably never had a gap, and it means an outage of the webhook stops deployments. That is a real cost, and one to choose on purpose with the replica count and monitoring to back it.
Whatever crossed during a gap is still running after it closes. A periodic reconciliation sweep is
what finds it: compare every running container's observed image digest with the admission ledger,
and report — or stop and re-admit — the ones with no decision behind them. Attestkeep ships
Ignore by default, records every outage window for the evidence document, and runs that
sweep hourly.
Prove it is standing
An admission webhook that is silently not running looks exactly like a compliant cluster. So the
first thing to do after installing any gate, and after every upgrade, is to watch it refuse something.
With Attestkeep's default policy the smallest violation is the :latest tag:
kubectl run enforcement-check --image=nginx:latest
Error from server: admission webhook "image-security.attestkeep.com" denied the request:
image security policy: nginx:latest: the :latest tag is blocked by policy
kubectl get pod enforcement-check
Error from server (NotFound): pods "enforcement-check" not found
Two things are worth noticing. The error comes from Error from server: the deny happened inside the API server's admission chain, not in an agent that might have been asleep. And the reason names the rule, so a developer reading it in a CI log knows what to fix. If the pod was created instead, check the usual suspects in order: a bypassed namespace, a policy in audit mode, a webhook that is failing open, an installation that was never activated.
Keep the record
A denial that leaves no record is enforcement without evidence. Record the allows as well as the denies — evidence that only lists refusals cannot show a control was operating — and record each decision under the hash of the policy that made it, so "what rule was in force when this was admitted" is a lookup rather than archaeology through git history.
That is the part free tools do not give you and the part an auditor actually reads. Attestkeep content-hashes every decision at write time, seals the ledger under signed, chained checkpoints, and turns a period of decisions into a signed evidence package mapped to the control set you select. How that ledger is made tamper-evident, and exactly where its guarantees end, is on the evidence trust model page.
Every setting named here is documented: Policies for the field reference and ready-to-apply examples, Admission for the two posture settings, and Prove it is enforcing for the full runbook. The Community edition is free and carries every check on this page.