Admission-Time Image Verification
Admission-time image verification is the enforcement point where a Kubernetes cluster refuses to run a container image unless that image carries a cryptographic proof — a signature, and often a provenance/attestation — that satisfies a written policy. It is the last preventive gate before runtime: everything upstream (signing at build, generating provenance, scanning for CVEs) produces evidence; this gate is where a machine finally checks that evidence and either admits or denies the pod. The check runs inside the Kubernetes admission chain as a webhook, so it fires on every
Pod-creating request before any container is pulled or started. The animating rule is a short sentence with heavy consequences: “only signed images, from a trusted builder, meeting a required SLSA level, may run here.” The tools that implement it — Sigstore’s policy-controller, Kyverno’sverifyImages, Connaisseur, and OPA Gatekeeper — differ in surface but share one mechanism: intercept the request, resolve the image to an immutable digest, verify the attached cryptographic material against configured trust roots, and turn the result into an admit/deny decision.
This note owns the supply-chain practice of verifying images at the admission gate. It deliberately does not re-teach the Kubernetes admission machinery — how a ValidatingWebhookConfiguration is wired, how the API server calls out to a webhook, the mutating-vs-validating ordering — that mechanism lives in Kubernetes MOC (Admission Controllers, ValidatingAdmissionWebhook, MutatingAdmissionWebhook). Nor does it re-teach how signatures and provenance are produced; that is Sigstore Signing and Keyless Verification, Build Provenance and Verifiable Builds, and The SLSA Framework. Here the job is: what does the gate check, how does it check it, and what does a real policy look like.
Mental Model — the gate that consumes the proof
Think of the whole supply chain as a relay of claims and this gate as the referee who finally validates them. A build pipeline signs the image and attaches a provenance attestation (claims: “I, GitHub Actions workflow X, built this from source repo Y at commit Z”). Those claims travel with the image in the registry as extra OCI artifacts. When someone applies a Deployment, the admission gate is the first component with both the authority to say no and the policy that says what “good” means.
flowchart LR subgraph Build["Upstream (produces proof)"] SIGN["cosign sign<br/>+ attest provenance"] end subgraph Reg["Registry"] IMG["image@sha256:...<br/>+ .sig + .att artifacts"] end subgraph Cluster["Kubernetes API server"] MUT["Mutating webhook<br/>resolve tag → digest"] VAL["Validating webhook<br/>verify sig + attestation<br/>vs ClusterImagePolicy"] end SIGN --> IMG IMG --> MUT --> VAL VAL -->|"proof valid"| ADMIT["Pod admitted<br/>(pinned to digest)"] VAL -->|"missing / bad proof"| DENY["Pod denied<br/>(or warn)"]
What it shows and the insight to take: the gate does not trust the image because it came from your registry — a registry is just storage an attacker can push to. It trusts the image because the cryptographic material verifies against an identity you configured. The registry is where the proof is stored; the admission gate is where the proof is believed or rejected. Note the two-phase shape: a mutating pass rewrites the mutable tag (nginx:1.27) to an immutable digest (nginx@sha256:...) so the thing verified is the exact thing that runs, and a validating pass makes the admit/deny call. This ordering matters — verifying a tag would be meaningless, because the tag can be re-pointed at a different image between check and pull (a time-of-check-to-time-of-use gap).
Why the digest pin is load-bearing
The single most important property of admission-time verification is that it ends on a digest, not a tag. A tag is a mutable pointer; myapp:latest today and myapp:latest tomorrow can be different bytes. A signature is made over a specific digest. So a naive check — “does myapp:latest have a valid signature?” — is exploitable: an attacker who can re-point the tag serves a signed digest to the verifier and an unsigned (or differently-signed) digest to the kubelet’s image pull. Every serious verifier closes this gap the same way: it resolves the tag to a digest, verifies the signature for that digest, and mutates the pod spec to reference the digest directly, so the kubelet pulls exactly what was verified.
- Sigstore policy-controller “automatically resolves image tags to their digests, ensuring the running image matches what was admitted” (policy-controller overview).
- Kyverno’s
mutateDigestfield (defaulttrue) “mutates matching images to add the image digest,” and itsverifyDigestfield (defaulttrue) additionally requires that images already use digests; theverifyImagesrule runs “during the mutation webhook first, then during validation” (Kyverno verifyImages). - Connaisseur translates tags to their SHA-256 digests so that only “trusted digests (signed by a trusted entity) are passed to the container runtime” (Connaisseur docs).
This is why image verification is inherently both a mutating and a validating admission operation, and why you cannot implement it correctly with a pure validating policy that only reads the incoming spec.
Mechanical walk-through — a pod create, step by step
The sequence below traces a kubectl apply of a Deployment through the gate using the Sigstore policy-controller as the concrete example; Kyverno and Connaisseur differ in field names but follow the identical shape.
sequenceDiagram participant U as kubectl / CI participant API as kube-apiserver participant MW as policy-controller<br/>(mutating) participant Reg as OCI Registry participant Rekor as Fulcio / Rekor participant VW as policy-controller<br/>(validating) U->>API: create Pod (image: app:v1) API->>MW: AdmissionReview (mutating) MW->>Reg: resolve app:v1 → app@sha256:abc MW-->>API: patch: image = app@sha256:abc API->>VW: AdmissionReview (validating) VW->>Reg: fetch signature + attestation for sha256:abc VW->>Rekor: verify cert chain (Fulcio root)<br/>+ inclusion proof (Rekor) VW->>VW: match identity (issuer/subject)<br/>+ evaluate CUE/Rego on predicate alt all authorities satisfied VW-->>API: allowed = true API-->>U: Deployment created else proof missing or identity mismatch VW-->>API: allowed = false, message API-->>U: admission denied end
What it shows and the insight to take: verification is not one comparison but a chain of them. First the image identity is pinned (digest). Then the signature is checked against a trust root — for keyless Sigstore that means validating the short-lived signing certificate against the Fulcio certificate-authority root and confirming the signature is recorded in the Rekor transparency log. Then the signer identity is matched — the certificate must have been issued to the OIDC identity you expect (e.g. issuer https://token.actions.githubusercontent.com, subject your build workflow). Only then, optionally, is the content of an attestation evaluated with a policy language. A failure at any link denies the pod. The takeaway: “is it signed?” is the weakest possible question; “is it signed by the identity I trust, and does its provenance say what I require?” is the real one.
The AND/OR trust algebra
A subtle but critical semantic: within one policy, multiple authorities combine with OR (any one satisfying authority admits), but across multiple policies that match the same image, the combination is AND (every matching policy must pass) (policy-controller overview). This lets you express “signed by our CI or by our break-glass key” (two authorities, OR) while also layering an independent org-wide policy that every image must additionally satisfy (AND). Getting this backwards is a common misconfiguration that either over-blocks legitimate images or silently admits ones a second policy was meant to catch.
Configuration — three real policies
Sigstore policy-controller — ClusterImagePolicy
The policy-controller is installed with a namespace opt-in: only namespaces labeled policy.sigstore.dev/include=true are subject to verification by default (policy-controller overview). A ClusterImagePolicy then declares which images and which authorities:
apiVersion: policy.sigstore.dev/v1alpha1
kind: ClusterImagePolicy
metadata:
name: require-ci-signature
spec:
images:
- glob: "ghcr.io/acme/**" # which images this policy governs
authorities: # authorities OR together
- keyless:
url: https://fulcio.sigstore.dev # Fulcio CA that issued the signing cert
identities:
- issuer: https://token.actions.githubusercontent.com
subject: https://github.com/acme/app/.github/workflows/release.yml@refs/heads/main
ctlog:
url: https://rekor.sigstore.dev # transparency-log inclusion required
attestations: # optionally require + inspect provenance
- name: must-have-slsa
predicateType: https://slsa.dev/provenance/v1
policy:
type: cue
data: |
predicate: builder: id: "https://github.com/acme/ci"
mode: enforce # or "warn" to alert without blockingLine-by-line: images.glob selects the images (glob, not regex); authorities is the OR-set of acceptable proofs; keyless.identities pins the OIDC issuer and certificate subject so only your build workflow counts as a valid signer; ctlog.url forces a Rekor transparency-log inclusion check; the attestations block requires a SLSA provenance predicate to be present and runs an inline CUE policy asserting the builder ID; mode: enforce denies on failure (set warn to admit-but-alert). Cluster-wide default behavior for images matched by no policy is set separately via the config-policy-controller ConfigMap’s no-match-policy, which takes deny, warn, or allow (policy-controller overview). Air-gapped or custom Sigstore deployments are handled by a TrustRoot CRD (remote or serialized TUF root, or bring-your-own keys).
Kyverno — verifyImages with keyless cosign
Kyverno folds verification into its general policy engine. A keyless verifyImages rule:
apiVersion: kyverno.io/v1
kind: ClusterPolicy
metadata:
name: verify-keyless-images
spec:
validationFailureAction: Enforce # Enforce = deny; Audit = report only
rules:
- name: verify-signature
match:
any:
- resources: {kinds: [Pod]}
verifyImages:
- imageReferences:
- "ghcr.io/acme/*" # static strings only — NO {{ }} interpolation
mutateDigest: true # tag → digest (default true)
verifyDigest: true # require digest form (default true)
required: true # every matching image must be verified
attestors:
- entries:
- keyless:
issuer: https://token.actions.githubusercontent.com
subject: https://github.com/acme/app/.github/workflows/build.yaml@refs/heads/main
rekor:
url: https://rekor.sigstore.devAnd a companion rule that verifies a SLSA provenance attestation, checking a field inside the predicate:
verifyImages:
- imageReferences: ["ghcr.io/acme/*"]
type: SigstoreBundle
attestations:
- predicateType: https://slsa.dev/provenance/v1
attestors:
- entries:
- keyless:
issuer: https://token.actions.githubusercontent.com
subject: https://github.com/acme/app/.github/workflows/build.yaml@refs/heads/main
rekor: {url: https://rekor.sigstore.dev}
conditions:
- all:
- key: "{{ buildDefinition.buildType }}"
operator: Equals
value: https://actions.github.io/buildtypes/workflow/v1Two Kyverno gotchas worth internalizing: imageReferences does not support variable interpolation (only static globs) (Kyverno verifyImages); and Kyverno keeps a TTL cache of verification results (imageVerifyCacheEnabled, default max-size 1000, default TTL 60 minutes), so a policy change or a re-push may not re-verify until the cache entry expires — a real source of “why is the old image still admitted” confusion.
Connaisseur — validator-per-signing-scheme
Connaisseur is purpose-built for signature verification and nothing else. It runs as a mutating webhook, supports three validator families — Notary v1 / Docker Content Trust, Sigstore/Cosign, and Notation (Notary v2) — and maps images to validators via an image policy; on a request it will deny (no trusted digest), modify (resolve tag→digest), or admit, and offers a detection mode that warns instead of blocking (Connaisseur docs). It is maintained by Secure Systems Engineering GmbH and supports Kubernetes v1.16+.
From “signed” to “meets a SLSA level”
The strongest form of this gate does not just verify a signature — it enforces that the image’s provenance attestation proves a required build integrity level. The building block is cosign verify-attestation, which checks a DSSE-enveloped in-toto attestation and can run a CUE or Rego policy over the predicate (cosign attestation verification):
cosign verify-attestation \
--type slsaprovenance \
--certificate-identity=https://github.com/acme/app/.github/workflows/build.yaml@refs/heads/main \
--certificate-oidc-issuer=https://token.actions.githubusercontent.com \
--policy slsa-l3.rego \
ghcr.io/acme/app@sha256:abc...The admission tools embed exactly this check. To assert a SLSA Build track level you write policy that inspects the provenance predicate for the properties that level demands — that the build ran on a trusted hosted builder (L2+), that the builder ID matches, that the source URI and revision are the ones you expect. SLSA v1.2 defines a Build track from L0 to L3 and, as of v1.2, reintroduces a Source track; the exact obligations belong to The SLSA Framework, but the enforcement mechanism is this: at admission, require the attestation to exist, verify it was signed by your builder’s identity, and assert the predicate fields the level requires.
Uncertain
Verify: the release/stability status and API version of each tool as of 2026-07-25 — the Sigstore policy-controller docs still carry the banner “This component is still actively under development!” and its CRD is served at
policy.sigstore.dev/v1alpha1/v1beta1depending on release; Kyverno’sverifyImagesfield names (e.g.SigstoreBundletype,mutateDigestdefault) track the current 1.x line but the exact minor version was not pinned here. Reason: fetched docs are “latest” and did not enumerate a version. To resolve: check the policy-controller and Kyverno release notes and CRDapiVersionfor the version you deploy.#uncertain
Failure modes and common misunderstandings
- Verifying the tag, not the digest. Covered above — the whole point is the digest pin. A policy that admits based on a tag’s signature but does not mutate to the digest is exploitable via tag re-pointing.
- Fail-open on webhook outage. If the admission webhook’s
failurePolicyisIgnore, an attacker who can DoS or delete the webhook makes all images admit unverified. The Kubernetes MOC admission notes coverfailurePolicy; the supply-chain lesson is that a security gate configured fail-open is not a gate. But fail-closed (Fail) risks a cluster-wide outage if the verifier is down — a real availability/security trade-off. - The gate covers only what it can see. Init containers, ephemeral containers, and images referenced indirectly must all be matched by
imageReferences/glob, or they slip through. System and infrastructure images (CNI, CSI, the verifier itself) frequently lack signatures and need explicit static/pass authorities or namespace exclusions — Sigstore provides “static authorities” precisely for this (policy-controller overview). - Identity glob too broad. A
subject: "*"orissuerleft unpinned reduces the check to “is it signed by anyone Sigstore will issue a cert to” — which is nearly everyone. The security is entirely in pinning issuer and subject to your build identity. - Cache staleness (Kyverno). A revoked or newly-required policy may not take effect until the TTL cache expires.
- Admission is not runtime. The gate checks the pod at create time. It does not re-verify a running pod, and it does not stop a compromise that happens after start — that is Falco’s job (runtime detection, mechanism in Linux Security MOC). Admission verification is preventive; it is one layer, paired with detective controls.
Choosing a tool
| Tool | Scope | Signing schemes | Policy language | Best when |
|---|---|---|---|---|
| Sigstore policy-controller | Image verification (purpose-built) | Cosign keyless/key, static | CUE or Rego on attestations | All-in on Sigstore; want native ClusterImagePolicy + TrustRoot |
| Kyverno | General policy engine incl. verifyImages | Cosign, Notary | YAML conditions + CUE/Rego | Already run Kyverno for other policy; want one engine |
| Connaisseur | Image verification (purpose-built) | Notary v1/DCT, Cosign, Notation | Image-policy matching | Need Notary/DCT or Notation, not just Sigstore |
| OPA Gatekeeper | General validating admission | Via external data / Ratify plugin | Rego (ConstraintTemplates) | Standardized on Gatekeeper/Rego; verification via external provider |
The honest trade-off: if you have standardized on one admission-policy engine (Kyverno or OPA Gatekeeper) for pod-security and other rules, adding image verification there avoids running a second controller. If image verification is your primary need and you are all-in on Sigstore, the purpose-built policy-controller is the most direct fit. OPA Gatekeeper verifies images only via an external-data provider (e.g. Ratify) rather than natively, so it is the least self-contained of the four for this specific task.
Production notes
Rollout discipline is what separates a working gate from a self-inflicted outage. The universal pattern is to deploy in warn/audit mode first (mode: warn for policy-controller, validationFailureAction: Audit for Kyverno, detection mode for Connaisseur), watch which real workloads would be denied, add exclusions/static authorities for legitimately-unsigned infrastructure images, and only then flip to enforce. Scope with namespace labels so the blast radius of a bad policy is one namespace, not the cluster. Pin identities tightly (issuer and subject), require Rekor inclusion so a stolen ephemeral key alone is insufficient, and layer an org-wide AND policy that no image can escape. Treat the verifier’s own availability as a reliability concern: a fail-closed gate is a hard dependency in every pod-create path, so it needs the same SLO attention as any other critical control-plane component (Site Reliability Engineering MOC).
See Also
- Sigstore Signing and Keyless Verification — how the signatures this gate checks are produced (cosign + Fulcio + Rekor, keyless model)
- Build Provenance and Verifiable Builds — the provenance attestation the gate inspects to enforce a build-integrity requirement
- The SLSA Framework — the level ladder (Build track L0–L3, v1.2 Source track) this gate enforces via attestation policy
- in-toto Attestations — the signed, typed statement format the attestations ride in
- Container Image Scanning — the complementary build/registry-time CVE check (this gate verifies origin, scanning checks contents)
- Kyverno · OPA Gatekeeper · Admission Controllers · ValidatingAdmissionWebhook · MutatingAdmissionWebhook — the admission mechanics this practice consumes (Kubernetes MOC)
- Image Signing with Sigstore — the existing container-image signing note
- Preventive versus Detective Controls — where admission verification sits (preventive) vs Falco (detective)
- DevSecOps and Supply Chain Security MOC — parent MOC (§7 Container and Runtime Security)