Image Signing and Provenance
A pulled image's digest proves the bytes were not corrupted in transit — Chapter 2 topic 08 established that content-addressing guarantees integrity. What a digest does not tell you is who built those bytes or whether they were tampered with at the source. Signing closes that gap: it binds an image's digest to a key the publisher controls, so a verifier can confirm driftwood/web@sha256:… was published by Driftwood's CI and not slipped in by someone who gained push access to the registry.
Provenance goes one step further — a signed attestation of how and from what source the image was built, which buildx can attach automatically. Together, signing and provenance turn "this is the right digest" into "this digest came from our build of our source," which is a meaningfully stronger claim and the one a deploy gate should actually check.
Digest Integrity Is Not Authenticity
Content-addressing guarantees the layers match their digests — corruption in transit fails the hash check. But anyone who can push to the registry can publish a fresh image under any tag, with a perfectly valid digest of their own malicious bytes. The digest proves the bytes are intact; it says nothing about whether they are yours. Signing is what proves the digest came from a trusted publisher rather than an attacker with push access.
Docker Content Trust, the Legacy Path
Docker Content Trust (DCT), built on Notary v1, was Docker's original signing layer, enabled by setting DOCKER_CONTENT_TRUST=1. It works, but it is largely superseded by Sigstore and carries the operational weight of running Notary. Recognize it in older pipelines; do not reach for it when starting fresh.
Sigstore and cosign, the Modern Path
cosign signs the image digest and stores the signature in the registry alongside the image. Its defining feature is keyless signing: instead of a long-lived private key, the signature is tied to a CI identity — the driftwood-io/app GitHub OIDC token — and recorded in a public transparency log. There is no standing key to leak, because the signing identity is the ephemeral workload identity of the build itself.
driftwood-io/app pipeline# .github/workflows/release.yml — sign step, runs after build-and-push
- name: Sign the published image # keyless: cosign 2.x signs via the
run: | # CI OIDC identity, no key or env flag
cosign sign --yes \
registry.driftwood.example/driftwood/web@${{ steps.build.outputs.digest }}
# the signature is pushed to the registry next to the image, keyed to the digest
The signed reference is the @sha256:… digest, never the tag. The GitHub Actions runner's OIDC token is the identity Sigstore records, so the resulting signature says "the driftwood-io/app release workflow signed exactly these bytes" without any key Driftwood has to store or rotate.
cosign sign the digestSLSA Provenance and SBOM Attestations
docker buildx build --provenance=true --sbom=true attaches two signed records to the image: SLSA provenance — the source repo, commit, and builder that produced it — and a software bill of materials listing every OS and language package inside. Both travel with the image as referrers in the registry, so a verifier can later ask the registry "how was this built and what is in it" and get a signed answer rather than a guess.
Verify on Pull, Not Just on Push
Signing only pays off if something checks the signature before the image runs. An unverified signature is decorative. The verification has to live at the point of use: a cosign verify step in CI before deploy, or an admission policy in the orchestrator that rejects any digest without a valid signature from the expected identity. Without that gate, a tampered or unsigned driftwood/web pulls and runs exactly like a good one.
$ cosign verify \
--certificate-identity-regexp 'https://github.com/driftwood-io/app/.+' \
--certificate-oidc-issuer https://token.actions.githubusercontent.com \
registry.driftwood.example/driftwood/web@sha256:7e9a…c204
# exits non-zero — failing the deploy — if no signature from that identity covers the digest
The verify step pins both the digest and the identity that must have signed it. A signature from any other identity, or none at all, fails the check and the deploy stops — which is the whole point: the gate, not the signature, is what keeps a bad image off a host.
- Signing the tag instead of the digest — a signature bound to a movable tag means nothing once the tag is re-pointed; cosign signs the immutable
@sha256:…so the signature covers exact content, not a name that can drift. - Signing in CI but never verifying before deploy — an unverified signature is decorative; without a
cosign verifygate or admission policy, a tampered image with no valid signature pulls and runs anyway. - Using long-lived signing keys checked into CI secrets — a leaked key lets an attacker sign malicious images as you; keyless signing tied to the CI OIDC identity removes the standing key entirely.
- Assuming an image digest alone proves it is safe — the digest proves integrity, not provenance; "it's the right digest" says nothing about whether that digest is the one your build produced versus one an attacker pushed.
- Sign the image digest with cosign in the
driftwood-io/apppipeline using keyless OIDC signing, so there is no standing key to compromise. - Attach SLSA provenance and an SBOM at build time (
--provenance,--sbom) so every publisheddriftwood/webcarries a verifiable record of how and from what source it was built. - Enforce verification at the point of use — gate deploys on
cosign verifyor an admission policy that rejects unsigned digests — so signing actually blocks bad images rather than merely labeling good ones. - Prefer Sigstore/cosign over Docker Content Trust for new pipelines, treating DCT as something to recognize and migrate off rather than build on.
Knowledge Check
Why does an image digest prove integrity but not authenticity?
- It guarantees the bytes match the hash, but anyone with push access can publish their own valid-digest bytes — only a signature ties it to a trusted publisher
- It cannot reliably detect corruption that happens in transit, so the bytes you pull might silently not match what was originally pushed
- The digest is only weakly encrypted and can therefore be forged by a determined attacker to match an entirely different, malicious set of bytes than the original
- It only identifies the image approximately rather than exactly, so two genuinely different images can end up sharing one digest
Why does cosign sign the digest rather than the tag?
- A tag is movable, so a signature on it stops meaning anything once the tag is re-pointed; the digest is immutable and names exact bytes
- Signing the digest produces a noticeably smaller signature object that transfers faster across the network than a tag signature would
- cosign cannot read or resolve human-readable tags at all, so it is technically forced to fall back to the underlying digest as its only addressable target
- Registries reject signatures attached to tags as a protocol rule
What do SLSA provenance and an SBOM attestation each record about an image?
- Provenance records how and from what source it was built — repo, commit, builder; the SBOM lists the packages inside the image
- Provenance lists every installed OS and language package, while the SBOM records the build's source repo and the exact commit
- Both are simply duplicate copies of the cosign signature itself, stored under two different referrer names in the registry
- Provenance records the running container's live runtime behavior and the SBOM records every network call it makes in production
Why must verification happen on pull rather than only signing on push?
- Without a verify gate at the point of use, an unsigned or tampered image pulls and runs exactly like a signed one — the signature is decorative
- Signing on push is fundamentally unreliable, so the real, trustworthy signature is only actually created later during the pull itself
- Verifying on pull is faster overall because it lets the daemon skip re-downloading the layers that were already signed and uploaded on the push side a second time
- The registry already automatically blocks every unsigned pull on its own, so the verify step is really only a redundant backup
You got correct