#!/bin/sh # Blocking pre-flight gate: fail if any deploy/*.Dockerfile pulls an # external base image without an immutable `@sha256:` digest pin. # # Required by docs/operations/DEPLOYMENT_UNRAID.md §6 ("immutable # release/image digests recorded") and # docs/architecture/SECURITY_THREAT_MODEL.md §5 ("immutable image digest in # production record"). See deploy/IMAGE_DIGESTS.md for the current ledger # and the exact commands to resolve a real digest. # # This script intentionally does NOT contain any digest value itself. It # only checks that Dockerfiles reference one. It must be run (and must pass) # before any production image build/release; wire it into the release # pipeline (e.g. `make build`, `make compose-up`, or CI) as a blocking step, # alongside DEPLOYMENT_UNRAID.md §8 step 1 ("validate clean build and # images"). # # Usage: deploy/verify-image-digests.sh # Exit status: 0 if every external base image is digest-pinned, 1 otherwise. set -eu script_dir=$(CDPATH= cd -- "$(dirname -- "$0")" && pwd) status=0 for dockerfile in "$script_dir"/*.Dockerfile; do [ -f "$dockerfile" ] || continue # Stage names declared with `AS ` in this file: a later `FROM # ` refers to a previous build stage, not a registry image, and # must not be treated as something that needs a digest. stage_names=$(grep -Eio '^FROM[[:space:]]+.*[[:space:]]AS[[:space:]]+[a-zA-Z0-9_.-]+' "$dockerfile" \ | awk '{print tolower($NF)}' || true) grep -Ein '^FROM[[:space:]]' "$dockerfile" | while IFS= read -r line; do lineno=${line%%:*} rest=${line#*:} # First token after FROM, ignoring an optional --platform=... flag. image=$(printf '%s\n' "$rest" | awk '{ for (i = 1; i <= NF; i++) { if (tolower($i) == "from") continue if ($i ~ /^--platform=/) continue print $i break } }') image_lc=$(printf '%s' "$image" | tr 'A-Z' 'a-z') is_stage=0 for s in $stage_names; do if [ "$s" = "$image_lc" ]; then is_stage=1 break fi done [ "$is_stage" -eq 1 ] && continue case "$image_lc" in scratch) : ;; # Docker's built-in empty rootfs; no registry manifest or digest exists. *@sha256:*) : ;; # pinned, OK *) echo "UNPINNED: $(basename "$dockerfile"):$lineno: FROM $image" >&2 echo " action: resolve a real digest (see deploy/IMAGE_DIGESTS.md) and" >&2 echo " change this line to 'FROM $image@sha256:'." >&2 echo "unpinned" >> "$script_dir/.verify-image-digests.tmp" ;; esac done done if [ -f "$script_dir/.verify-image-digests.tmp" ]; then rm -f "$script_dir/.verify-image-digests.tmp" status=1 fi if [ "$status" -ne 0 ]; then echo >&2 echo "deploy/verify-image-digests.sh: FAILED - one or more base images are not pinned by digest." >&2 echo "This blocks release per DEPLOYMENT_UNRAID.md §6 / SECURITY_THREAT_MODEL.md §5." >&2 else echo "deploy/verify-image-digests.sh: OK - all external base images are digest-pinned." fi exit "$status"