This commit is contained in:
@@ -0,0 +1,84 @@
|
||||
#!/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 <name>` in this file: a later `FROM
|
||||
# <name>` 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:<digest>'." >&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"
|
||||
Reference in New Issue
Block a user