Image signing proves that an image is exactly what you think it is β
which is a sentence that sounds much stronger than what a signature
actually says. Start with the precise version, because every mistake in
this area comes from the imprecise one.
What a signature attests
A Cosign signature over an image asserts exactly this:
Some holder of this signing key β or, for keyless signing, some
holder of a certificate binding a public key to this OIDC identity β
performed a signing operation over this manifest digest.
That is the whole statement. Everything else is inference from
surrounding context, and the inferences people make are:
People assume the signature says
It actually says
The image is safe to run
Nothing about the contents
The code was reviewed
Nothing about the source
The build was not tampered with
Nothing about the build (that is provenance)
The publisher endorses it for production
Only that a signing operation happened
The image is current
Nothing; a signature does not expire with the image
The last row deserves emphasis. A signature over a two-year-old image
riddled with known vulnerabilities verifies perfectly. Signing and
scanning are orthogonal controls and neither substitutes for the other.
What signing does give you is enormous and specific: the registry
stops being a trusted party. Without a signature, βthis image came
from our registryβ is a claim the registry makes about itself. With
one, a consumer verifies against a publisher identity, and an attacker
who fully controls the registry still cannot make you accept an image
your publisher did not sign.
Signing with a key
Configuration changegenerate a keypair and signβ Sign the digest, never the tag. A tag is a pointer somebody else can move; the digest is the artefact.
Cosign also accepts a KMS reference in place of a file β
--key <provider>://<key> for AWS KMS, GCP KMS, Azure Key Vault,
HashiCorp Vault, OpenBao and Kubernetes secrets. That is the right
answer when you keep long-lived keys: the private key never leaves the
KMS, and every signing operation is logged by an auditable service you
already operate.
Keyless signing
For CI, the right answer is usually to have no key at all.
Configuration changekeyless signing in CIβ No --key means keyless. Cosign obtains an OIDC token from the CI environment and requests a short-lived certificate.
In current Cosign releases, keyless is simply the path taken when no
--key is supplied. Older material wraps this in
COSIGN_EXPERIMENTAL=1; that was required by Cosign 1.x and is not part
of the current documented invocation.
Verification, and the one that accepts anything
This is the part that goes wrong.
Read-only / Safekeyless verification, constrainedβ The identity and issuer constraints are the control. Without them there is no policy, only a cryptographic formality.
For keyless flows, either --certificate-identity or
--certificate-identity-regexp must be set β Cosign will not run
without one. There is an equivalent pair for the issuer:
--certificate-oidc-issuer and --certificate-oidc-issuer-regexp.
Enforcement
A verification nobody enforces is a log line. Where you enforce depends
on what runs your containers, and the honest summary is uneven:
Kubernetes. An admission controller is the strong answer, because
it sits in the path of every workload creation and cannot be bypassed
by a person with kubectl.
Configuration changeKyverno keyless verification policyβ verifyImages is the mechanism. Checking for an annotation is not β annotations are attacker-controlled and prove nothing.
The verifyImages rule also offers mutateDigest, which rewrites a
tag reference to the digest that was verified, and verifyDigest,
which refuses tag references outright. Both close the
verify-then-deploy window described above, at the cluster level.
Plain Docker hosts. There is no daemon-level signature enforcement
to rely on. Docker Content Trust is being retired β the Notary v1
service at notary.docker.io shuts down on 8 December 2026 β and it
was never a Cosign mechanism anyway.
So on a standalone host, enforcement is your deploy tooling. Make it a
gate that fails closed:
Configuration changedeploy gate on a standalone hostβ Verification runs before the pull, and the script exits non-zero if it fails. set -e is doing real work here.
verified: registry.example.com/myorg/myapp@sha256:4f2a...c19d
sha256:4f2a...c19d: Pulling from myorg/myapp
Illustrative output
BuildKit and Compose. Neither verifies signatures. Do not assume a
docker compose pull checks anything; it does not.
Rotation
Sign new releases with the new key while the old public key is still in every trust store.
Optionally re-sign current releases with the new key β the registry holds multiple signatures for one digest happily, and this lets verifiers move independently.
Distribute the new public key to every verifier: CI, admission policies, deploy scripts, other teams.
Confirm every verifier accepts the new key before removing anything.
Remove the old public key from the trust stores. This is the step that actually revokes it.
Step 5 is the whole rotation. Signatures do not expire, so a compromised
key keeps producing valid signatures until every verifier stops
trusting it. βWe rotatedβ means nothing until you can enumerate the
trust stores you updated β which is a strong practical argument for
keyless, where there is no long-lived key to rotate and revocation is
expressed by narrowing the identity policy instead.
Knowledge check
Knowledge check Β· 5 questions
Q1. A Cosign signature over an image digest asserts which of the following?
Q2. In keyless signing, the certificate Fulcio issues is valid for only minutes. How can a verifier check the signature a year later?
Q3. Which of these verification setups provide no real assurance? Select all that apply.
Q4. Running `cosign verify` against a tag and then deploying that same tag is equivalent to verifying and deploying the digest.
Q5. Removing a compromised public key from every verifier trust store is what revokes it, because signatures made with it do not expire.
Passing score: 75%. Answers are checked in this browser.