Skip to main content
RunBook Academy

← All checklists in Docker & Containers

Before deploymentImages

Checklist: Image supply chain verification

25 items ·18 critical ·7 warn ·0 info

When to run this

Once per release, against the exact digest that is about to be deployed. Not against the tag, not against last night’s build, and not against the branch. If the digest changes after this checklist runs, the checklist has to run again — that is the whole point of pinning.

Its companion, docker-checklist-registry-pull-readiness, checks that the deploy host can reach and pull the artefact. This one checks what the artefact contains and where it came from.

Setting up

IMAGE='registry.example.com/platform/api@sha256:REPLACE_ME'
BASE_IMAGE='docker.io/library/debian:12-slim'
EXPECTED_IDENTITY='https://github.com/example-org/api/.github/workflows/release.yml@refs/heads/main'
EXPECTED_ISSUER='https://token.actions.githubusercontent.com'

Run from a checkout at the commit the release was built from, so the Dockerfile items examine the same source the image came from.

The three items that are usually theatre

Three of the controls on this list are commonly implemented in a way that produces artefacts without producing security. They are worth separating out because each looks green in a pipeline dashboard.

A scan is not coverage. A scanner reports the packages it can identify: entries in the distro package database, and dependencies declared in a lockfile it recognises. A Go binary with its dependencies compiled in, a JAR with a shaded library inside it, a library vendored into third_party/, an npm tree installed and then flattened, anything pulled in by a curl in a RUN step — none of that is in the report, and none of it is reported as missing either. scan-blind-spots-named exists because a clean report on an image that is mostly one static binary is approximately no information, and the honest response is to write down what the tool cannot see and cover it another way.

A signature is not enforcement. Signing produces a signature. Security comes from something refusing to run an artefact that does not verify. Between those two there is usually a gap that nobody notices, because signing succeeds visibly and verification fails silently by not existing. signature-negative-test is the only item that closes it.

An SBOM is not an inventory. An SBOM attached to an image is a document. The control is the ability to answer, in under an hour, which of your running images contain a given package at a given version. If that query has no answer today, the SBOM will not help on the day it is asked.

Evidence and sign-off

Attach to the release record: the scan output, the verification output for the exact digest, the SBOM diff, and the written list of components the scanner could not see. That last one is the document that ages best — it is the only place your actual coverage gap is recorded, and it is what the next person needs when a static dependency turns out to matter.

  • Release: _________________ Digest: ___________
  • Operator: _________________ Date: ___________
  • Reviewer: ________________ Date: ___________

Critical18 items

  1. docker compose config --images | grep -v '@sha256:'
  2. grep -iE '^[[:space:]]*FROM' Dockerfile | grep -v '@sha256:'
  3. docker manifest inspect "$IMAGE" >/dev/null
  4. grep -iE '^[[:space:]]*FROM' Dockerfile; docker buildx imagetools inspect "$BASE_IMAGE" --format '{{.Manifest.Digest}}'
  5. docker scout cves --only-severity critical,high --exit-code "$IMAGE"
  6. docker scout cves --only-fixed --format markdown --output scout-report.md "$IMAGE"
  7. docker scout sbom --format list "$IMAGE" | wc -l; docker history --no-trunc --format '{{.CreatedBy}}' "$IMAGE" | grep -ciE 'curl |wget |go build|COPY --from'
  8. cosign verify --certificate-identity-regexp "$EXPECTED_IDENTITY" --certificate-oidc-issuer "$EXPECTED_ISSUER" "$IMAGE"
  9. docker buildx imagetools inspect "$IMAGE" --format '{{ json .SBOM.SPDX }}' | jq -e '.packages | length > 0'
  10. docker buildx imagetools inspect "$IMAGE" --format '{{ json .Provenance.SLSA }}' | jq '.buildDefinition.externalParameters // .invocation.configSource'
  11. docker history --no-trunc --format '{{.CreatedBy}}' "$IMAGE" | grep -iE 'token|password|passwd|secret|api[_-]?key|BEGIN [A-Z ]*PRIVATE KEY'
  12. docker image inspect "$IMAGE" --format '{{json .Config.Env}} {{json .Config.Labels}}'
  13. grep -n 'mount=type=secret' Dockerfile

Warning7 items

  1. docker image inspect "$BASE_IMAGE" --format '{{.Created}}'
  2. docker scout version
  3. docker buildx imagetools inspect "$IMAGE" --format '{{ json .SBOM.SPDX }}' | jq -r '.packages[].name' | sort > sbom-new.txt; diff sbom-previous.txt sbom-new.txt
  4. docker buildx imagetools inspect "$IMAGE" --format '{{ json .Provenance.SLSA }}'
  5. docker image inspect "$IMAGE" --format '{{index .Config.Labels "org.opencontainers.image.source"}} {{index .Config.Labels "org.opencontainers.image.vendor"}}'