Container images move through many hands before they reach your cluster, including a developer laptop, a CI runner, a registry, and finally a Kubernetes node. At any of these steps, an image can be swapped, tampered with, or replaced by something malicious. The fix is to sign container images with Cosign right after you build them, and then tell Kubernetes to reject anything that is not signed. This guide shows you the whole process.
Enforce Signed Container Images in Kubernetes with Cosign and Kyverno
Table of Contents
- Why You Should Sign Container Images with Cosign
- What You Will Need
- Step 1: Install Cosign
- Step 2: Build and Push a Test Container Image
- Step 3: Generate a Cosign Key Pair
- Step 4: Sign the Image with Your Cosign Key
- Step 5: Verify the Signature Manually
- Step 6: Add an SBOM Attestation (Optional)
- Step 7: Install Kyverno
- Step 8: Store the Cosign Public Key as a Kubernetes Secret
- Step 9: Create the ImageValidatingPolicy
- Step 10: Verify Signatures Are Enforced
- Step 11: Prove Unsigned Images Are Rejected
- Step 12: Check Attestations (Optional)
- Signing with Keyless Identities in Cosign
- Troubleshooting Common Cosign Errors
- Conclusion

Why You Should Sign Container Images with Cosign
Image signing proves who built the image, and whether it was changed after that. When you sign container images with Cosign, it adds a signature next to the image in the registry. Anyone with the public key can check that the image is untouched and really came from the signer.
This matters because Kubernetes will run any image it can pull, with no built-in check on who made it or whether it was modified.
Cosign and Kyverno fix this together. Cosign proves trust when the image is built, and Kyverno checks that trust when the image is deployed. No valid signature means the Pod never gets created.
What You Will Need
Before you start, make sure you have these things ready:
- A Kubernetes cluster. A managed cluster, K3s, or a self-hosted cluster on a dedicated server all work.
kubectlconfigured to talk to that cluster, with cluster-admin rights.- Docker or another OCI-compatible builder installed on your machine.
- A container registry you can push to, such as Docker Hub, GitHub Container Registry, or a private registry.
- Kyverno already installed on your machine.
If you already set up Kyverno 1.19 from our Kyverno CEL policy guide, with the policy-lab and app-production namespaces, you can reuse that same cluster and the kyverno.perlod.com/security-tier=production label. This guide just adds a new policy type, ImageValidatingPolicy, on top of that. It does not replace your existing ValidatingPolicy rules for privileged containers, non-root users, or resource limits.
Step 1: Install Cosign
Cosign is the command-line tool from the Sigstore project that you use to sign container images with Cosign and verify signatures.
On a Linux machine (amd64), download and install the Cosign binary directly:
Check that Cosign installed correctly:
You should see a version line similar to GitVersion: v3.1.3. If the command is not found, confirm /usr/local/bin is in your $PATH.
Step 2: Build and Push a Test Container Image
You need an image in a registry before you can sign it. This example uses a small Nginx-based image and GitHub Container Registry, but any registry works the same way.
Create a simple project folder and a minimal Dockerfile:
Paste this into the file:
Create a small index.html file next to it:
Log in to your registry, replace ghcr.io and the username with your own:
Build and push the image:
Cosign signs the exact image digest, not just the tag, so get the digest now with:
You will use $IMAGE_DIGEST in the next steps. Signing by digest is the safer way to sign container images with Cosign because a tag can later point to a different image.
Step 3: Generate a Cosign Key Pair
For this guide, we will use key-based signing, which is the simplest way to get started. Set a password for the private key and generate the pair:
This creates two files in your current folder:
cosign.key: The private key. Keep this secret. Never commit it to Git.cosign.pub: The public key. This is safe to share and is what Kyverno will use to check signatures.
Store cosign.key somewhere safe, such as a password manager or your CI/CD secret store. You will need cosign.pub in Step 8 to build the Kyverno policy.
Step 4: Sign the Image with Your Cosign Key
At this point, use the private key to sign container images with Cosign and upload the signature next to your image in the registry:
The --yes flag skips the confirmation prompt. Cosign will ask for the private key password unless COSIGN_PASSWORD is already set in your shell.
Once the command finishes, Cosign pushes the signature to the same registry repo as your image. There's no separate signing server or database needed; the registry itself holds the proof.
Step 5: Verify the Signature Manually
Before trusting Kubernetes to do this check, you must confirm the signature works from the command line:
If the signature is valid, Cosign prints the verified claims as JSON and exits with status 0. If you run this on an image you never signed, Cosign returns an error and a non-zero exit code. This is the same check Kyverno relies on inside the cluster.
Step 6: Add an SBOM Attestation (Optional)
Signing proves who published the image. An attestation goes one step further and attaches signed facts about the image, such as a software bill of materials (SBOM) or a vulnerability scan result. Kyverno can check attestations the same way it checks signatures.
You can create a simple SBOM-style predicate file. In a real pipeline, tools like Syft or Trivy generate this for you:
Attach it as a signed attestation to the image:
Verify the attestation locally:
Now you have both a signature and an attestation attached to the same image digest in the registry.
Step 7: Install Kyverno
If you already have Kyverno 1.19 running, skip this step. Otherwise, you can install it with Helm:
Wait for the admission controller to be ready:
Confirm the ImageValidatingPolicy custom resource exists in your cluster:
Step 8: Store the Cosign Public Key as a Kubernetes Secret
You can paste the public key directly inside the policy YAML, but keeping it in a Secret makes rotation easier and keeps large PEM blocks out of your policy files. Create the secret from the cosign.pub file you generated in Step 3:
This guide keeps the key inline in the policy, which matches Kyverno's own examples and is simpler to follow. The Secret above becomes useful later, once you manage keys through GitOps.
Step 9: Create the ImageValidatingPolicy
Now you must write the policy that tells Kyverno how to check that you did sign container images with Cosign before letting them run. Print the public key so you can paste it into the YAML:
Create the policy file:
Paste the following content. Replace the placeholder key with your own cosign.pub contents, and replace the registry glob with your image path:
Once you are done, apply the policy:
Note: If your cluster already uses the kyverno.perlod.com/security-tier=production label, add a namespaceSelector under matchConstraints. This keeps the image-signing rule scoped to the same namespaces as your other ValidatingPolicy rules.
Step 10: Verify Signatures Are Enforced
Now you can create a Pod manifest that points to the image you already signed:
Once you are done, apply it with:
Because you already used Cosign to sign this image and pushed the signature to the registry, Kubernetes should accept the Pod and start it running normally.
Step 11: Prove Unsigned Images Are Rejected
At this point, you can try an image that was never signed. Any public image you have not run through Cosign will do:
If your matchImageReferences glob only covers your own registry, either change it to "*" for this test, or point the test Pod at an unsigned image inside your own registry instead. Once the policy matches that image, Kubernetes rejects the request with an error like this:
This confirms that when you sign container images with Cosign, they pass; when you skip that step, Kyverno blocks them. Clean up the test Pods when you are done:
Step 12: Check Attestations (Optional)
If you added an SBOM attestation in Step 6, you can require it too. Just add a second check using verifyAttestationSignatures, pointing to an attestations entry in the policy.
The exact fields depend on your attestation format, so check the official Kyverno reference before using this in production, because Kyverno's CEL API usually changes.
Signing with Keyless Identities in Cosign
The steps above use a long-lived key pair, which is the easiest way to learn to sign with Cosign. In a GitHub Actions pipeline, you can skip key management entirely and use keyless signing.
In this way, Cosign uses the pipeline's own OIDC identity instead of a stored private key:
To verify a keyless-signed image, Cosign and Kyverno need the signer's identity and OIDC issuer instead of a public key. It requires a more advanced setup.
Troubleshooting Common Cosign Errors
Even with the right steps, small mistakes can break the setup. Here are the most common errors and how to fix them:
"no matching signatures" during cosign verify: Double-check you are verifying the same digest you signed, not a newer build with the same tag.- Kyverno accepts everything, even unsigned images: Confirm
validationActionsis set to[Deny], not[Audit], and thatmatchImageReferencesmatches the image path you are testing. - Pod is stuck instead of rejected: Check
kubectl describe pod <name>for the real admission error, and check Kyverno logs withkubectl logs -n kyverno deployment/kyverno-admission-controller --tail=200. - Policy applies but does nothing: Run
kubectl get imagevalidatingpolicy verify-cosign-signature -o yamland look at the status field for parsing or CEL expression errors. - Password prompt blocks CI scripts: Always set
COSIGN_PASSWORDas an environment variable in automated pipelines, and pass--yestocosign signandcosign attest.
Conclusion
At this point, you have a working image trust pipeline. You sign container images with Cosign right after building, store the signature in the registry, and use Kyverno's ImageValidatingPolicy to enforce it at admission time.
Next, you can try keyless signing in CI and SBOM attestations in production. Also, you can run this whole setup on your dedicated server hosting for full control over the registry, cluster, and policies.