Sign, verify & allow registries

Keyless signing and verification at admission.

Advanced14 min · lesson 21 of 24

Scanning proves an image was clean at the moment you looked; signing proves the image running now is the same one you scanned; and verification at admission is the only step that actually enforces any of it. The chain is worth stating plainly because teams routinely do the first two and skip the third — and an unverified signature stops nothing, since anyone can still deploy an unsigned image.

The chain, and where it is enforced
1buildpin base, minimize2scanTrivy, fail on critical3signCosign keyless4verifyat admission — Enforce
Build, scan, and sign produce evidence; only the admission verify step refuses to run an image that lacks it. Skip it and the rest is theatre.

Cosign keyless signing removes the private key you would otherwise have to protect: it binds the signature to your CI’s OIDC identity via a short-lived Fulcio certificate, logs it in the public Rekor transparency log, and needs no long-lived key on disk. The pipeline signs the exact digest it just built and scanned, not a mutable tag.

terminal
$ cosign sign --yes "$CI_REGISTRY_IMAGE@$(cat image.digest)"
# a Fulcio cert is issued for the CI OIDC identity, the signature is pushed,
# and a tamper-evident entry is written to Rekor — nothing durable to steal

Verify and restrict registries at admission

A policy engine intercepts every pod create and refuses images that are unsigned, signed by an unexpected identity, or pulled from a registry you do not trust. Verifying the signature and pinning the registry are two separate rules — you need both, because a signature check alone lets an attacker sign their own malicious image with their own identity and pass. (The older ImagePolicyWebhook is the pre-Kyverno way to do the same admission gate.)

verify-images.yaml
apiVersion: kyverno.io/v1
kind: ClusterPolicy
metadata: { name: verify-and-restrict }
spec:
validationFailureAction: Enforce
rules:
- name: only-internal-registry
match: { any: [{ resources: { kinds: [Pod] } }] }
validate:
message: "images must come from registry.internal"
pattern: { spec: { containers: [{ image: "registry.internal/*" }] } }
- name: verify-signature
match: { any: [{ resources: { kinds: [Pod] } }] }
verifyImages:
- imageReferences: ["registry.internal/*"]
attestors:
- entries:
- keyless: { issuer: "https://gitlab.acme.internal", subject: "*@acme.internal" }

You can verify by hand too, asserting the identity you expect — a good habit for spot checks and for understanding what the admission policy is really asserting on your behalf.

terminal
$ cosign verify \
--certificate-identity-regexp ".*@acme.internal" \
--certificate-oidc-issuer "https://gitlab.acme.internal" \
registry.internal/payments-api@sha256:9f2a... # non-zero exit = reject
Enforce, not Audit
Set validationFailureAction to Enforce or the policy only warns while unsigned images keep scheduling. And always pair verification with the registry allowlist: “is it signed” is not the question — “is it signed by us and pulled from our registry” is.