Image Validation

Validate image signatures for your deployments

Overview

Kyverno is available on the cluster and lets you enforce that only container images signed with a key you control may start in your namespace. This is a self-service guardrail against accidentally deploying an unsigned, tampered, or wrong-registry image.

You can create policies of kind NamespacedImageValidatingPolicy in your own namespace. The cluster-scoped equivalent (ImageValidatingPolicy) is reserved for cluster administrators.

Note: Because the policy lives in your namespace, anyone with write access to that namespace can also modify or delete it. Treat it as a safety net for your own workflows, not as a boundary against privileged users.

Prerequisites

  • A namespace on the cluster (testuser-dev in the examples below)
  • cosign v3.x installed locally
  • Push access to a repository in harbor.uni-muenster.de

Create a signing key pair

cosign generate-key-pair

This writes cosign.key (encrypted with the passphrase you enter) and cosign.pub.

Sign your image

Always sign by digest, not by tag. Tags are mutable; a signature bound to a tag proves nothing after the tag moves.

IMAGE=harbor.uni-muenster.de/testuser/testimage
docker push $IMAGE:v1
DIGEST=$(crane digest $IMAGE:v1)

cosign sign --key cosign.key $IMAGE@$DIGEST

Verify locally before involving the cluster:

cosign verify --key cosign.pub $IMAGE@$DIGEST

Create the policy

Replace the namespace, the image glob, and the public key with your own values:

apiVersion: policies.kyverno.io/v1
kind: NamespacedImageValidatingPolicy
metadata:
  name: verify-images
  namespace: testuser-dev
spec:
  validationActions:
    - Deny
  matchConstraints:
    resourceRules:
      - apiGroups: [""]
        apiVersions: ["v1"]
        operations: [CREATE, UPDATE]
        resources: [pods]
  matchImageReferences:
    - glob: "harbor.uni-muenster.de/testuser/testimage*"
  attestors:
    - name: local
      cosign:
        key:
          data: |
            -----BEGIN PUBLIC KEY-----
            MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEOolTpYvhO5y6Xmyi97WMadLgtJlf
            ETaNiQ5G7OHWFSFoTEP2XPlbiGiGMk1rsiXvCCbfyktnt+AWQm+ERaQUpQ==
            -----END PUBLIC KEY-----
        ctlog:
          insecureIgnoreTlog: true # Skip transparency log verification
  validations:
    - expression: >-
        images.containers.all(img, verifyImageSignatures(img, [attestors.local]) > 0) &&
        images.initContainers.all(img, verifyImageSignatures(img, [attestors.local]) > 0) &&
        images.ephemeralContainers.all(img, verifyImageSignatures(img, [attestors.local]) > 0)
      message: "Image signature verification failed - must be signed with the local cosign key."

Note: we are skipping the Tlog verification since access to the external Sigstore servers is blocked in the clusters. As we are not using keyless signing the cryptographic verification against your public key is unaffected by this.

kubectl apply -f verify-images.yaml
kubectl get namespacedimagevalidatingpolicy -n testuser-dev

Test the policy

A signed image should start normally:

kubectl -n testuser-dev run signed --image=$IMAGE@$DIGEST

An unsigned one should be rejected:

kubectl -n testuser-dev run unsigned --image=harbor.uni-muenster.de/testuser/testimage:unsigned
Error from server: admission webhook "ivpol.validate.kyverno.svc-fail" denied the request:
Image signature verification failed - must be signed with the local cosign key.