Skip to main content
RunBook Academy

Docker & ContainersXII Β· Supply ChainSigning

Image signing β€” Cosign, Sigstore, and verification

Advanced⏱ ~28 mindockercosign

What you'll learn

  • State precisely what a Cosign signature does and does not assert
  • Sign with a key and keylessly, and explain what Fulcio and Rekor each contribute
  • Write a verification that constrains identity, and recognise one that does not
  • Place enforcement where it cannot be bypassed

Prerequisites

Verified against Docker Engine 29.x Β· Docker Engine 28.x Β· Docker Compose 2.x Β· containerd 2.x Β· runc 1.2.x Β· BuildKit 0.20+ Β· Linux kernel 5.15+ Β· Ubuntu 24.04 LTS Β· Debian 12 (Bookworm) Β· 2026-08-12

Not yet marked complete on this device.

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 saysIt actually says
The image is safe to runNothing about the contents
The code was reviewedNothing about the source
The build was not tampered withNothing about the build (that is provenance)
The publisher endorses it for productionOnly that a signing operation happened
The image is currentNothing; 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
DIGEST=sha256:REPLACE_ME
IMG="registry.example.com/myorg/myapp@${DIGEST}"

cosign generate-key-pair
cosign sign --key cosign.key "$IMG"
Read-only / Safeverify with the public key
DIGEST=sha256:REPLACE_ME
IMG="registry.example.com/myorg/myapp@${DIGEST}"

cosign verify --key cosign.pub "$IMG"

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
DIGEST=sha256:REPLACE_ME
IMG="registry.example.com/myorg/myapp@${DIGEST}"

cosign sign "$IMG"

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
DIGEST=sha256:REPLACE_ME
IMG="registry.example.com/myorg/myapp@${DIGEST}"

cosign verify \
--certificate-identity-regexp '^https://github\.com/myorg/myapp/\.github/workflows/release\.yml@refs/heads/main$' \
--certificate-oidc-issuer 'https://token.actions.githubusercontent.com' \
"$IMG"

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
apiVersion: kyverno.io/v1
kind: ClusterPolicy
metadata:
name: check-image-keyless
spec:
webhookConfiguration:
  timeoutSeconds: 30
rules:
  - name: check-image-keyless
    match:
      any:
        - resources:
            kinds:
              - Pod
    verifyImages:
      - imageReferences:
          - 'registry.example.com/myorg/*'
        failureAction: Enforce
        attestors:
          - entries:
              - keyless:
                  subject: 'https://github.com/myorg/*'
                  issuer: 'https://token.actions.githubusercontent.com'
                  rekor:
                    url: https://rekor.sigstore.dev

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
set -euo pipefail

REPO=registry.example.com/myorg/myapp
TAG=1.4.0
IDENTITY='^https://github\.com/myorg/myapp/\.github/workflows/release\.yml@refs/heads/main$'
ISSUER='https://token.actions.githubusercontent.com'

DIGEST=$(docker buildx imagetools inspect "${REPO}:${TAG}" \
| awk '/^Digest:/ {print $2; exit}')
REF="${REPO}@${DIGEST}"

cosign verify \
--certificate-identity-regexp "$IDENTITY" \
--certificate-oidc-issuer "$ISSUER" \
"$REF" > /dev/null

echo "verified: $REF"
docker pull "$REF"
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

  1. Sign new releases with the new key while the old public key is still in every trust store.
  2. Optionally re-sign current releases with the new key β€” the registry holds multiple signatures for one digest happily, and this lets verifiers move independently.
  3. Distribute the new public key to every verifier: CI, admission policies, deploy scripts, other teams.
  4. Confirm every verifier accepts the new key before removing anything.
  5. 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

  1. Q1. A Cosign signature over an image digest asserts which of the following?

  2. Q2. In keyless signing, the certificate Fulcio issues is valid for only minutes. How can a verifier check the signature a year later?

  3. Q3. Which of these verification setups provide no real assurance? Select all that apply.

  4. Q4. Running `cosign verify` against a tag and then deploying that same tag is equivalent to verifying and deploying the digest.

  5. 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.