diff options
| author | agent <agent@local> | 2026-11-07 09:00:55 +0100 |
|---|---|---|
| committer | agent <agent@local> | 2026-11-07 09:00:55 +0100 |
| commit | 54541fa8357cf2af616effbaeb7d5b8d76f80ea2 (patch) | |
| tree | 0be5059280b10545912e01081f7d95aaf9055623 /scripts/verify-mirror.sh | |
Public read-only distroless OCI mirror (zot + edge vhost + auditor)
Diffstat (limited to 'scripts/verify-mirror.sh')
| -rwxr-xr-x | scripts/verify-mirror.sh | 191 |
1 files changed, 191 insertions, 0 deletions
diff --git a/scripts/verify-mirror.sh b/scripts/verify-mirror.sh new file mode 100755 index 0000000..42f10bf --- /dev/null +++ b/scripts/verify-mirror.sh @@ -0,0 +1,191 @@ +#!/bin/bash +# verify-mirror.sh — prove that the public mirror serves exactly what upstream +# serves, and mark every verified ref with a git tag. +# +# For each repo/tag pair it compares the *index* digest advertised by gcr.io +# with the digest advertised by the mirror. Only when both agree does it pull +# the image by digest through the public hostname (the real client path) and +# create the marker tag `synced-<repo>-<tag>-<digest12>`. +# +# Marker tags are created locally; the workflow pushes them. Writing the marker +# last is the whole point: its existence means "verified pullable by digest", +# not merely "upstream has a newer tag". +# +# Exit status: +# 0 every ref resolved; new refs were verified and marked (or none changed) +# 1 the mirror answered with an error, or upstream lost a ref we mirror +# 2 usage / configuration error +# +# Environment: +# MIRROR public registry host (default img.dieschoenewolke.de) +# UPSTREAM upstream registry host (default gcr.io) +# REPOS space separated repo names under distroless/ (default the four) +# TAGS space separated tags (default the four upstream variants) +# VERIFY_PULL new (default) | always | never +# PLATFORM platform used for the verification pull (default linux/amd64) + +set -uo pipefail + +MIRROR="${MIRROR:-img.dieschoenewolke.de}" +UPSTREAM="${UPSTREAM:-gcr.io}" +REPOS="${REPOS:-static base cc python3}" +TAGS="${TAGS:-latest nonroot debug debug-nonroot}" +VERIFY_PULL="${VERIFY_PULL:-new}" +PLATFORM="${PLATFORM:-linux/amd64}" + +ACCEPT='application/vnd.oci.image.index.v1+json,application/vnd.docker.distribution.manifest.list.v2+json,application/vnd.oci.image.manifest.v1+json,application/vnd.docker.distribution.manifest.v2+json' + +say() { printf '%s\n' "$*"; } +warn() { printf '%s\n' "$*" >&2; } + +case "$VERIFY_PULL" in + new | always | never) ;; + *) + warn "VERIFY_PULL must be new, always or never" + exit 2 + ;; +esac + +command -v curl >/dev/null || { + warn "curl is required" + exit 2 +} +command -v jq >/dev/null || { + warn "jq is required" + exit 2 +} +if [ "$VERIFY_PULL" != "never" ]; then + command -v docker >/dev/null || { + warn "docker is required unless VERIFY_PULL=never" + exit 2 + } +fi + +# Ask <url> for its manifest. Sets PROBE_STATUS (HTTP code, 000 on transport +# error) and PROBE_DIGEST (empty when the registry sent no digest header). +probe() { # <url> [authorization-header] + local url=$1 auth=${2:-} headers + headers=$(mktemp) + if [ -n "$auth" ]; then + PROBE_STATUS=$(curl -sS --max-time 120 -H "$auth" -H "Accept: $ACCEPT" \ + -D "$headers" -o /dev/null -w '%{http_code}' "$url") || PROBE_STATUS=000 + else + PROBE_STATUS=$(curl -sS --max-time 120 -H "Accept: $ACCEPT" \ + -D "$headers" -o /dev/null -w '%{http_code}' "$url") || PROBE_STATUS=000 + fi + PROBE_DIGEST=$(tr -d '\r' <"$headers" \ + | awk 'tolower($1) == "docker-content-digest:" { print $2; exit }') + rm -f "$headers" +} + +upstream_token() { # <repo> + curl -fsS --max-time 60 \ + "https://$UPSTREAM/v2/token?scope=repository:distroless/$1:pull&service=$UPSTREAM" \ + | jq -r '.token // empty' +} + +new_markers="" +failures=0 +pending=0 +unchanged=0 +verified=0 + +for repo in $REPOS; do + token=$(upstream_token "$repo") || token="" + if [ -z "$token" ]; then + # Without a token we cannot compare anything, so this is a real failure. + warn "FAIL distroless/$repo: no anonymous gcr.io token" + failures=$((failures + 1)) + continue + fi + + for tag in $TAGS; do + ref="distroless/$repo:$tag" + + probe "https://$UPSTREAM/v2/distroless/$repo/manifests/$tag" "Authorization: Bearer $token" + up=$PROBE_DIGEST + if [ "$PROBE_STATUS" != "200" ] || [ -z "$up" ]; then + warn "FAIL $ref: upstream answered $PROBE_STATUS without a digest" + failures=$((failures + 1)) + continue + fi + + probe "https://$MIRROR/v2/distroless/$repo/manifests/$tag" + mirror=$PROBE_DIGEST + case "$PROBE_STATUS" in + 200) ;; + 404) + # zot has not mirrored this tag yet (new upstream tag, or a + # pull-through that failed). Nothing to mark; the next run + # retries after zot's 2h sync. + say "PENDING $ref upstream=${up#sha256:} (not mirrored yet)" + pending=$((pending + 1)) + continue + ;; + *) + warn "FAIL $ref: mirror answered $PROBE_STATUS" + failures=$((failures + 1)) + continue + ;; + esac + if [ -z "$mirror" ]; then + warn "FAIL $ref: mirror answered 200 without a digest header" + failures=$((failures + 1)) + continue + fi + + if [ "$mirror" != "$up" ]; then + # The tag moved upstream and zot's periodic sync has not caught up. + say "PENDING $ref upstream=${up#sha256:} mirror=${mirror#sha256:}" + pending=$((pending + 1)) + continue + fi + + short=${up#sha256:} + short=${short:0:12} + marker="synced-$repo-$tag-$short" + marked=0 + git rev-parse -q --verify "refs/tags/$marker" >/dev/null 2>&1 && marked=1 + + if [ "$marked" -eq 1 ] && [ "$VERIFY_PULL" != "always" ]; then + say "OK $ref ${up#sha256:}" + unchanged=$((unchanged + 1)) + continue + fi + + if [ "$VERIFY_PULL" = "never" ]; then + say "NEW $ref ${up#sha256:} (pull verification disabled)" + else + if ! docker pull --quiet --platform "$PLATFORM" "$MIRROR/$ref@$up" >/dev/null; then + warn "FAIL $ref: digest matches but the image does not pull by digest" + failures=$((failures + 1)) + continue + fi + docker image rm "$MIRROR/$ref@$up" >/dev/null 2>&1 || true + say "OK $ref ${up#sha256:} (pulled by digest)" + fi + + if [ "$marked" -eq 1 ]; then + unchanged=$((unchanged + 1)) + continue + fi + verified=$((verified + 1)) + + if ! git tag "$marker"; then + warn "FAIL $ref: could not create marker tag $marker" + failures=$((failures + 1)) + continue + fi + new_markers="$new_markers $marker" + done +done + +say "" +say "verified=$verified unchanged=$unchanged pending=$pending failures=$failures" + +if [ -n "$new_markers" ]; then + printf '%s\n' "${new_markers# }" >"${MARKERS_FILE:-/tmp/mirror-markers}" +fi + +[ "$failures" -eq 0 ] || exit 1 +exit 0 |
