Signing Container Images with cosign
Introduction¶
A registry tag is mutable and says nothing about where an image came from. cosign (part of Sigstore) signs images by digest so you can verify provenance — and block anything unsigned from running in your cluster.
Keyless signing¶
The modern default. No long-lived key to manage or leak:
- cosign gets a short-lived OIDC token (from GitHub Actions, GitLab, or an interactive browser login).
- Fulcio issues a short-lived (~10 min) X.509 cert bound to that identity.
- cosign signs the image digest with the ephemeral key.
- The signature + cert are pushed to the registry (as an OCI artifact) and logged in Rekor, the public transparency log.
- Verification checks the signature, that the cert chains to Fulcio, that the identity matches your expected issuer/subject, and that it's in Rekor.
# In CI (GitHub Actions), with id-token: write permission
cosign sign --yes \
ghcr.io/org/app@sha256:abc123...
# Verify
cosign verify \
--certificate-identity-regexp 'https://github.com/org/.*' \
--certificate-oidc-issuer https://token.actions.githubusercontent.com \
ghcr.io/org/app@sha256:abc123...
Always sign and verify by digest, not tag — a tag can be repointed after signing.
Key-based (if you must)¶
cosign generate-key-pair # or --kms awskms://... / cosign.key in a secret
cosign sign --key cosign.key ghcr.io/org/app@sha256:...
cosign verify --key cosign.pub ghcr.io/org/app@sha256:...
Now you own key rotation and distribution. Keyless avoids all of that.
Attestations and SBOMs¶
Beyond "who signed it", attach signed statements about the image:
# Attach an SPDX SBOM as a signed attestation
syft ghcr.io/org/app@sha256:... -o spdx-json > sbom.spdx.json
cosign attest --yes --predicate sbom.spdx.json --type spdxjson \
ghcr.io/org/app@sha256:...
# SLSA provenance from the build
cosign attest --yes --predicate provenance.json --type slsaprovenance ...
cosign verify-attestation --type spdxjson \
--certificate-identity-regexp '...' --certificate-oidc-issuer '...' \
ghcr.io/org/app@sha256:...
Enforce at admission¶
Signing is pointless if unsigned images still run. Use an admission controller:
Sigstore policy-controller¶
apiVersion: policy.sigstore.dev/v1beta1
kind: ClusterImagePolicy
metadata: { name: require-signed }
spec:
images:
- glob: "ghcr.io/org/**"
authorities:
- keyless:
identities:
- issuer: https://token.actions.githubusercontent.com
subjectRegExp: 'https://github.com/org/.*'
Kyverno¶
apiVersion: kyverno.io/v1
kind: ClusterPolicy
metadata: { name: verify-images }
spec:
validationFailureAction: Enforce
rules:
- name: check-signature
match:
any: [{ resources: { kinds: [Pod] } }]
verifyImages:
- imageReferences: ["ghcr.io/org/*"]
attestors:
- entries:
- keyless:
subject: "https://github.com/org/*"
issuer: "https://token.actions.githubusercontent.com"
Both mutate the pod spec to pin the digest on admission — a nice side effect.
Verification and troubleshooting¶
cosign tree ghcr.io/org/app@sha256:... # what signatures/attestations exist
cosign verify ... --output json | jq
no matching signatures— verifying by tag while signed by digest (or vice versa), wrong identity/issuer regex, or the signature is in a different registry/repo (COSIGN_REPOSITORY).- CI signing fails "getting key from Fulcio" — missing
id-token: writepermission on the job, or not running in a supported OIDC environment. - Admission blocks your own images — the identity regex doesn't match the
actual workflow path (reusable workflows change the
subject), or you're pulling a mutating base tag.kubectl get eventsshows the policy denial reason. - Third-party / base images unsigned — scope the policy to your own repos and either mirror+sign upstream images or allow specific known digests.
- Rekor unreachable in an airgapped cluster — run a private Rekor + Fulcio,
or use key-based signing with
--offlineverification against a pinned transparency-log entry. - Signature verified but the image is still malicious — signing proves origin, not safety. Pair with vulnerability scanning (Trivy/Grype) and SBOM policy.
Related tools and reading¶
- On-site: Base64 decoder, JWT decoder.
- Related posts: SBOM and supply-chain security, Multi-stage Docker builds explained.
Stuck on something this site can't fix?Reach out to Prabath directly on LinkedIn.More in Docker & Kubernetes
StatefulSets Explained
StatefulSets give pods sticky names, sticky storage, and ordered operations — what databases and clustered apps need. Here's how each guarantee works.
October 24, 2026Persistent Volumes and Claims, Explained
PVC is a request, PV is the fulfilled storage, StorageClass is the factory. Most 'Pending PVC' problems are an access-mode or provisioner mismatch.
October 24, 2026Kubernetes Jobs and CronJobs
A Job runs pods until they succeed; a CronJob creates Jobs on a schedule. Here's parallelism, retry limits, the missed-schedule trap, and history cleanup.
October 24, 2026