If you have been signing your container images with Cosign but you are not actually verifying those signatures at deploy time, an attacker who pushes a malicious tag to your registry still wins. This tutorial walks through installing Sigstore policy-controller on a Kubernetes cluster, writing a ClusterImagePolicy that enforces keyless Cosign signatures on images from a specific Fulcio identity, and testing the policy with both a signed and an unsigned image. You will finish with admission-level blocking, a dry-run mode for gradual rollout, and a kubectl workflow to debug rejections.
Prerequisites: A Kubernetes 1.27+ cluster, kubectl access with cluster-admin, helm 3.12+, cosign 2.2.0 or newer, and at least one signed image in a registry.
Time to complete: About 45 minutes.
What is policy-controller and why use it for admission?
policy-controller is the Sigstore-maintained admission webhook that verifies Cosign signatures before pods start. Unlike writing Kyverno or OPA policies by hand, it understands Cosign's verification semantics natively, including Fulcio certificate identities, Rekor transparency log inclusion, and annotations on signatures. This means your admission rules stay close to the Cosign commands your build pipeline already runs.
Install it with Helm:
helm repo add sigstore https://sigstore.github.io/helm-charts
helm repo update
helm install policy-controller -n cosign-system --create-namespace \
sigstore/policy-controller --version 0.8.2
Verify the webhook is healthy:
kubectl get pods -n cosign-system
kubectl get validatingwebhookconfiguration policy-controller.sigstore.dev
You should see the policy-controller-webhook pod in Running state and the webhook configuration listed. Until you label a namespace, nothing is enforced.
How do I write a ClusterImagePolicy for keyless signing?
A ClusterImagePolicy declares which images must be signed and by whom. For a keyless flow signed by GitHub Actions, you match on Fulcio OIDC issuer and subject. Create policy.yaml:
apiVersion: policy.sigstore.dev/v1beta1
kind: ClusterImagePolicy
metadata:
name: require-signed-images
spec:
images:
- glob: "ghcr.io/acme/**"
authorities:
- name: keyless-github
keyless:
url: https://fulcio.sigstore.dev
identities:
- issuer: https://token.actions.githubusercontent.com
subjectRegExp: "^https://github.com/acme/.+/.github/workflows/release.yml@refs/tags/v.*$"
ctlog:
url: https://rekor.sigstore.dev
Apply it and opt a namespace into enforcement with the policy.sigstore.dev/include=true label:
kubectl apply -f policy.yaml
kubectl label namespace prod policy.sigstore.dev/include=true
The subjectRegExp matches only releases cut from tags on the release.yml workflow. That pins the trusted builder identity rather than the image digest, which is what you want for a keyless flow.
How do I test enforcement with a signed and unsigned image?
Deploy two pods, one with a signed image and one without, and watch what happens. First, confirm the signed image verifies manually:
cosign verify ghcr.io/acme/api:v1.4.2 \
--certificate-identity-regexp="github.com/acme/.+/release.yml@refs/tags/v.*" \
--certificate-oidc-issuer=https://token.actions.githubusercontent.com
A successful verification prints the signature claims JSON. Now apply a pod manifest:
kubectl -n prod run signed --image=ghcr.io/acme/api:v1.4.2
kubectl -n prod run unsigned --image=docker.io/library/nginx:1.25
The first pod starts. The second is rejected:
Error from server (BadRequest): admission webhook "policy.sigstore.dev"
denied the request: validation failed: no matching policies:
spec.containers[0].image: docker.io/library/nginx:1.25
That rejection is exactly what you want. Any image outside your declared glob is denied by default once the namespace label is applied.
How do I roll this out without breaking production?
Use the warn mode and per-namespace labels to stage the rollout. Add mode: warn to the policy spec and redeploy:
spec:
mode: warn
images:
- glob: "ghcr.io/acme/**"
In warn mode, rejections are logged but pods still start. Tail the webhook logs:
kubectl logs -n cosign-system -l app.kubernetes.io/name=policy-controller -f \
| grep "policy violation"
Spend one or two weeks collecting violations, fix signing gaps in your pipelines, then remove mode: warn or set it to enforce. Start with low-risk namespaces like staging, then expand to prod after signed coverage reaches 100 percent.
How do I handle images that legitimately cannot be signed?
Write a second ClusterImagePolicy that allow-lists specific third-party images by digest. This keeps the default deny posture intact while making exceptions explicit:
apiVersion: policy.sigstore.dev/v1beta1
kind: ClusterImagePolicy
metadata:
name: allow-known-third-party
spec:
images:
- glob: "docker.io/library/redis@sha256:*"
authorities:
- name: static-digest
static:
action: pass
The static.action: pass tells policy-controller to accept without signature checking. Pin to a digest rather than a tag so an attacker cannot substitute the image behind the allow-list. Review this list quarterly and remove entries as upstream projects adopt signing.
How do I debug a surprise rejection?
Use kubectl describe on the failed pod and inspect the webhook log. Most rejections come from one of three causes: a missing signature, a subject mismatch, or a Rekor lookup failure. Check the signature inventory on the registry directly:
cosign tree ghcr.io/acme/api:v1.4.2
If the signature exists but verification fails, widen the webhook log level temporarily:
kubectl -n cosign-system set env deployment/policy-controller-webhook \
LOG_LEVEL=debug
Look for the exact subject the signature carries and compare it to your subjectRegExp. Nine times out of ten, a branch-based build produced a subject like .../release.yml@refs/heads/main, which your regex intentionally rejects.
How Safeguard Helps
Safeguard closes the loop between signing, admission, and reachability. Every image Safeguard scans carries its Cosign attestation chain alongside its SBOM, so you can query which workloads in production run images whose signatures have expired or whose Fulcio identity no longer matches policy. Griffin AI surfaces reachable vulnerabilities in those same images, letting you prioritize fixes for CVEs that ship in signed-but-still-vulnerable layers. Policy gates then block promotion of any image that lacks a valid signature or has an unresolved critical finding, turning Cosign from a build-time habit into an enforced supply chain control.