Adding cosign sign to a pipeline takes about an hour. The signature lands in the registry, the SBOM lands next to it, a compliance checkbox turns green, and the cluster continues running whatever anyone pushes. You have produced attestations that nothing consumes. The gap between "we sign our images" and "we cannot run an unsigned image" is where the entire security value lives, and it is a gap most organisations have not crossed.
The reason is not ignorance. It is that enforcement can take production down, and the failure mode is total — a misconfigured admission policy rejects every pod during a node replacement and you have an outage caused by a security control that was supposed to be preventive. That risk is real and it is manageable, but only with a rollout sequence designed around it.
Keyless signing removes the reason most teams stopped
Traditional signing required a private key. Which needs storage, rotation, access control, an answer for what happens when it leaks, and a decision about which humans or systems hold it. That key management burden is why image signing sat unadopted for a decade after the capability existed.
Sigstore's keyless flow removes the long-lived key entirely. The signing identity is an OIDC token — a GitHub Actions workload identity, a GitLab CI JWT, a Kubernetes service account token. Fulcio, the certificate authority, exchanges that token for a short-lived X.509 certificate valid for ten minutes, binding the public key to the OIDC identity. The signature and certificate are recorded in Rekor, an append-only transparency log. The ephemeral private key is discarded.
What you verify afterwards is not "someone with the key signed this" but "this artifact was signed by the GitHub Actions workflow at .github/workflows/release.yml on the main branch of this specific repository." That is a materially stronger and more useful claim.
jobs:
build:
permissions:
contents: read
packages: write
id-token: write # required for keyless — the usual first failure
steps:
- uses: actions/checkout@v4
- id: push
uses: docker/build-push-action@v6
with: { push: true, tags: "${{ env.IMG }}:${{ github.sha }}" }
- uses: sigstore/cosign-installer@v3
- name: Sign
run: |
cosign sign --yes \
"${{ env.IMG }}@${{ steps.push.outputs.digest }}"
- name: Generate and attest SBOM
run: |
syft "${{ env.IMG }}@${{ steps.push.outputs.digest }}" \
-o spdx-json > sbom.spdx.json
cosign attest --yes \
--predicate sbom.spdx.json \
--type spdxjson \
"${{ env.IMG }}@${{ steps.push.outputs.digest }}"
Sign the digest, never the tag. A tag is a mutable pointer; signing myapp:v1.2.3 is signing a name that can be repointed at different content five minutes later. Every downstream reference should be by digest too, which has the useful side effect of making your deployments genuinely immutable.
Verification at admission is the actual control
Sigstore's policy-controller runs as a validating admission webhook and checks images against a ClusterImagePolicy before the pod is created.
apiVersion: policy.sigstore.dev/v1beta1
kind: ClusterImagePolicy
metadata:
name: internal-images-must-be-signed
spec:
images:
- glob: "registry.internal/**"
authorities:
- keyless:
url: https://fulcio.sigstore.dev
identities:
- issuer: https://token.actions.githubusercontent.com
subjectRegExp: "^https://github\\.com/acme-corp/[^/]+/\\.github/workflows/release\\.yml@refs/heads/main$"
ctlog:
url: https://rekor.sigstore.dev
attestations:
- name: must-have-sbom
predicateType: spdxjson
policy:
type: cue
data: |
predicateType: "https://spdx.dev/Document"
mode: enforce
The subjectRegExp is doing the heavy lifting and it is the field people get wrong. An unanchored or overly permissive pattern — anything matching github.com/acme-corp/.* without pinning the workflow path and the branch — means any workflow in any repo in your org can produce an image the cluster accepts. That includes a workflow on a pull request branch from a fork, depending on your Actions settings. Anchor both ends, pin the workflow file, pin the ref.
The rollout sequence
This is the part that determines whether you end up enforcing or end up with a policy stuck in audit mode for two years.
Phase 1 — sign everything, verify nothing (2–4 weeks). Add signing to every pipeline. No admission policy. The objective is coverage: find the pipelines nobody remembered, the images built on a laptop in 2022, the base images pulled directly from Docker Hub. Track the percentage of running pods whose image has a valid signature. It will start lower than anyone predicts.
# what fraction of running images are signed?
kubectl get pods -A -o jsonpath='{range .items[*].spec.containers[*]}{.image}{"\n"}{end}' \
| sort -u \
| while read -r img; do
if cosign verify "$img" \
--certificate-identity-regexp='.*' \
--certificate-oidc-issuer-regexp='.*' >/dev/null 2>&1; then
echo "OK $img"
else
echo "FAIL $img"
fi
done
Phase 2 — warn mode, one namespace (2 weeks). Deploy the policy with mode: warn scoped to a single low-risk namespace. Warnings appear in admission responses and in the webhook's logs. This is where you find that your policy regex is subtly wrong, which is far better discovered here than in phase 4.
Phase 3 — enforce in non-production, warn everywhere (4 weeks). Flip dev and staging to enforce. Production stays in warn. You now have a real feedback loop: a developer who breaks the signing requirement finds out in staging, at their own pace, with no customer impact.
Phase 4 — enforce in production, with a documented break-glass. Only after phase 3 has run for a full release cycle including at least one incident and one node pool replacement.
The failure modes worth planning for
The webhook becomes a single point of failure. An admission webhook with failurePolicy: Fail that is itself unavailable blocks all pod creation in scope. During a cluster-wide event — a node pool rotation, an AZ failure — this turns a partial outage into a total one, because nothing can be rescheduled.
Run the webhook with at least three replicas, spread across zones, with a PodDisruptionBudget, and — critically — exclude its own namespace and kube-system from the policy scope. A webhook that cannot start because its own policy rejects it is a deadlock that requires deleting the webhook configuration by hand to escape.
namespaceSelector:
matchExpressions:
- key: kubernetes.io/metadata.name
operator: NotIn
values: [kube-system, cosign-system, sigstore-system]
Third-party images. Most vendor images are unsigned, or signed by an identity you have no policy for. Do not carve out a blanket exemption by registry — that is a hole large enough to drive anything through. Mirror third-party images into your own registry through a review gate, and sign them with an internal identity on the way in. Now the policy is uniform and the exception is a documented, auditable promotion step.
Transparency log dependency. Verification against the public Rekor instance introduces an external dependency in your admission path. For production, either run a private Sigstore deployment (Fulcio, Rekor, and a trust root you manage) or configure the verifier to work from the bundled certificate and inclusion proof without a live log query. The latter is simpler and sufficient for most threat models; the former is what you want if availability of admission is a hard requirement.
What this does and does not buy you
Enforced signature verification gives you an answer to a specific and important question: did this running artifact come from my build system? That closes the class of attacks where someone pushes to your registry with stolen credentials, or where a compromised node pulls a substituted image, or where a developer deploys a locally-built binary to production. Combined with SBOM attestations, it also gives you the ability to answer "which running services contain this library" in minutes rather than days, which is the question every organisation scrambled to answer during Log4Shell and mostly could not.
It does not tell you the image is safe. A signature attests to provenance, not quality — a compromised dependency that entered through your legitimate build pipeline produces a perfectly valid signature. That is a different control (dependency review, build reproducibility, vulnerability gating) and conflating the two is how organisations end up believing they are more covered than they are.
It also says nothing about behaviour after the pod starts. Provenance is a build-time and admission-time property; a signed image that is exploited at runtime through an application vulnerability is still exploited. Detecting that requires runtime visibility at the syscall boundary, which is a genuinely complementary control rather than an alternative one. The pairing — verified provenance at admission, behavioural monitoring at runtime — covers substantially more than either alone.
The practical bar to aim for: every image running in production traceable to a specific commit, built by a specific workflow, with a signature the cluster refuses to run without. That is achievable in a quarter for most organisations, and it is a stronger position than the vast majority of clusters are in today.