BlogKubernetes

Sign and verify images with Cosign and Kyverno

Keyless image signing with cosign in CI and signature verification at the admission controller, so unsigned or tampered images simply stop scheduling.

Feb 24, 2026·10 min readAdvanced

Scanning tells you an image was clean when it left CI. It says nothing about whether the image running in your cluster is the one CI actually built. Signing closes that gap: Cosign signs the digest in the pipeline, Kyverno verifies the signature at admission, and an unsigned or tampered image simply never schedules.

Modern Cosign is keyless — there's no private key to store or rotate. It exchanges your CI's OIDC token for a short-lived certificate from Fulcio and records the signature in the Rekor transparency log:

bash — cosign keylesslive
cosign sign --yes registry.io/myapp@sha256:9f2a1c
Generating ephemeral keys ...
Retrieving signed certificate from Fulcio ...
Requesting OIDC token (github-actions) ...
tlog entry created: rekor.sigstore.dev index 74839201
 
cosign verify --certificate-oidc-issuer=github registry.io/myapp@sha256:9f2a1c
Verified OK — cert identity ci@myorg, issuer GitHub Actions
Signature lifecycle
1
CI builds
gets a digest
2
Cosign signs
keyless via OIDC
3
Rekor logs
public transparency
4
Admission
Kyverno verifies

Sign in the pipeline

Sign the digest, never a tag — tags are mutable and a signature on :latest guarantees nothing. Cosign reads the CI's OIDC identity automatically, so there are no secrets in this step.

.github/workflows/release.yml
- name: Sign image (keyless)
env:
COSIGN_EXPERIMENTAL: "1"
run: |
DIGEST=$(cosign triangulate "$REGISTRY/$IMAGE" 2>/dev/null || echo "$IMAGE_DIGEST")
cosign sign --yes "$REGISTRY/$IMAGE@$DIGEST"

Verify at admission with Kyverno

A ClusterPolicy with verifyImages intercepts every pod, checks the signature against the expected OIDC identity, and rejects anything that doesn't match. Set validationFailureAction: Enforce — in Audit it only logs.

verify-images.yaml
apiVersion: kyverno.io/v1
kind: ClusterPolicy
metadata:
name: verify-images
spec:
validationFailureAction: Enforce
rules:
- name: require-signature
match:
any:
- resources:
kinds: ["Pod"]
verifyImages:
- imageReferences: ["registry.io/*"]
attestors:
- entries:
- keyless:
issuer: "https://token.actions.githubusercontent.com"
subject: "*@myorg"

Deploy an image CI never signed and admission stops it cold — the pod object is never created:

bash — unsigned deploylive
kubectl run demo --image=registry.io/unsigned:latest
kyverno admission webhook: verify-images
 
Error from server: admission webhook "mutate.kyverno.svc" denied the request:
verify-images: failed to verify image registry.io/unsigned:latest:
no matching signatures found
 
images signed by CI schedule normally
Signatures prove origin, not safety
A signed image is one you built — not one free of CVEs. Keep scanning in the pipeline; signing and scanning are complementary gates, not substitutes.

Where this goes next

The same keyless flow signs attestations — attach an SBOM or a provenance statement and verify those at admission too. That's the jump from 'this image is ours' to 'this image was built from this source by this pipeline', which is what SLSA is really about.

Go deeper in a courseSoftware supply chain securitySigning, SBOMs, provenance and SLSA — commit to cluster.View course

Related posts