Kyverno Image Scanning: Kubernetes Admission Control
Published August 16, 2026 · 9 min read
A Kubernetes admission controller is the last checkpoint before an image actually runs — the one CI cannot bypass and no one can skip with a quick kubectl run. This guide builds a working policy in Kyverno — the most widely deployed Kubernetes admission controller and policy engine — that blocks any Pod whose image lacks a passing, signed ScanRook scan attestation, and explains the tradeoffs of enforcing it.

Why a CI gate alone is not enough
A CI gate like the one in our GitHub Actions scanning guide only runs for Pods that came through that specific pipeline. It does nothing to stop a different team's deployment, a manual kubectl apply, a rollback to a tag that predates the scanning policy, or a Helm chart that references a base image no one scanned. Admission control closes that gap by checking every Pod creation request against the cluster's API server, regardless of what produced it.
The pattern below does not scan images inside the admission webhook itself — that would make Pod scheduling as slow as a full scan. Instead, CI produces a signed attestation of the scan result (following the same scan step used elsewhere) and the admission webhook verifies that attestation exists, is signed by a trusted key, and reports zero critical findings.
Step one: publish a signed scan attestation in CI
After scanning, attach the result to the image in the registry as a cosign attestation, signed with a key your cluster policy will trust:
scanrook scan --file image.tar --format json --out report.json
# Build a minimal predicate from the scan summary
jq '{summary: .summary, scannedAt: now | todate}' report.json > predicate.json
cosign attest \
--predicate predicate.json \
--type https://scanrook.io/attestations/vuln-scan/v1 \
--key cosign.key \
myregistry.example.com/myapp@$(docker inspect --format='{{index .RepoDigests 0}}' myapp:ci | cut -d@ -f2)The attestation is pushed alongside the image in the registry, keyed to the image's digest — not its tag — so it stays correct even if the tag is later overwritten or reused.
Step two: the Kyverno ClusterPolicy
Kyverno is a CNCF policy engine that runs as a Kubernetes admission controller, which is what makes it a leading choice for enforcing image policy at deploy time. Its verifyImages rule can both verify an image's cosign signature and evaluate signed attestations against conditions (recent Kyverno versions also support CEL expressions for richer checks), so a single policy can require that a passing scan attestation exists for the exact image digest being admitted. Apply this policy to the cluster — it verifies the attestation's signature and enforces that the attested critical-finding count is zero before allowing the Pod:
apiVersion: kyverno.io/v1
kind: ClusterPolicy
metadata:
name: require-scanrook-attestation
spec:
validationFailureAction: Enforce
webhookTimeoutSeconds: 30
rules:
- name: verify-scan-attestation
match:
any:
- resources:
kinds:
- Pod
verifyImages:
- imageReferences:
- "myregistry.example.com/*"
attestors:
- entries:
- keys:
publicKeys: |-
-----BEGIN PUBLIC KEY-----
MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAE...
-----END PUBLIC KEY-----
attestations:
- type: https://scanrook.io/attestations/vuln-scan/v1
conditions:
- all:
- key: "{{ summary.critical }}"
operator: Equals
value: 0What each part does
- validationFailureAction: Enforce. Blocks matching Pods outright. The alternative,
Audit, logs a policy violation without blocking — the right starting point while you build confidence that the policy is not producing false rejections. - imageReferences.Scopes the policy to images from your own registry, so third-party images (a public Helm chart's sidecar, for example) are not blocked for lacking an attestation they were never going to have.
- attestors. The public key here must match the private key used by
cosign attestin CI. Kyverno rejects any attestation it cannot verify against a listed key, so an attacker cannot forge a passing result without that private key. - attestations.type. Must match the
--typevalue used when the attestation was created. Kyverno fetches the attestation payload matching this type and makes its fields available to the condition below. - conditions.
{{ summary.critical }}reads directly from the predicate JSON built in CI. Because the check runs against the signed attestation content, not a live scan, a Pod is only admitted if the exact attested result says zero critical findings.
Rolling this out without an outage
Enforcing this policy cluster-wide on day one, before every image has an attestation, will block deployments that have nothing to do with security — just missing metadata. Roll out in stages:
Start in Audit mode. Set validationFailureAction: Audit and review Kyverno's PolicyReport resources for a week to see which workloads would be blocked, before switching to Enforce.
Scope by namespace first. Use a match.any.resources.namespaces selector to enforce in one low-risk namespace, confirm CI is reliably producing attestations for everything deployed there, then widen namespace by namespace.
Decide on failurePolicy deliberately.A webhook configured to fail closed blocks all matching Pod creation if Kyverno itself is unreachable — correct for a security-critical gate, but make sure Kyverno's own availability is monitored at least as closely as the policy it enforces, or an unrelated Kyverno outage becomes a cluster-wide deployment outage.
Where this fits with CI scanning
Admission control is a backstop, not a replacement for the scanning covered in our GitHub Actions guide and container scanning best practices. CI scanning gives developers fast feedback before merge, when a fix is cheapest. Admission control guarantees the policy holds even when something bypasses CI entirely — the two layers check different failure modes and both are worth running.
Frequently asked questions
What is admission control for images?
A validating webhook Kubernetes calls before creating a Pod, which can reject the request based on the Pod's image — blocking vulnerable or unattested images from ever running.
Why not just rely on CI scanning?
CI only covers pipelines that actually run it. Admission control catches manual deploys, rollbacks, and anything else that bypasses CI.
Does this scan images at admission time?
No. The scan happens in CI; admission control verifies a signed attestation of that result, which is fast because no scan runs during Pod scheduling.
What if the webhook goes down?
Depends on failurePolicy. Fail closed blocks matching Pods until it recovers; fail open lets them through during the outage.
Attach ScanRook attestations to your images
ScanRook produces JSON reports you can attach as cosign attestations yourself, so the same scan step your CI already runs becomes the source of truth an admission policy can enforce.