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.
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:
cosign sign --yes registry.io/myapp@sha256:9f2a1cGenerating 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:9f2a1cVerified OK — cert identity ci@myorg, issuer GitHub ActionsSign 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.
- 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.
apiVersion: kyverno.io/v1kind: ClusterPolicymetadata:name: verify-imagesspec:validationFailureAction: Enforcerules:- name: require-signaturematch: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:
kubectl run demo --image=registry.io/unsigned:latestkyverno 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 normallyWhere 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