diff --git a/.github/template-sync-state.json b/.github/template-sync-state.json index e6c843c..a38a593 100644 --- a/.github/template-sync-state.json +++ b/.github/template-sync-state.json @@ -1,4 +1,4 @@ { "template_repo": "craftainer/template-docker", - "synced_tag": "v1.8.1" + "synced_tag": "v1.8.2" } diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 16e9b7b..1691b62 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -89,7 +89,9 @@ jobs: # is the separate `publish-packages` job below. `packages: write` lets # `make publish` push to this repository's own GHCR packages # (ghcr.io//...) with `GITHUB_TOKEN` (actor - # `GITHUB_ACTOR`) when the OCI_REGISTRY_* secrets are unset. The + # `GITHUB_ACTOR`, a default Actions env var) when the OCI_REGISTRY_* + # secrets are unset and OCI_REGISTRY is ghcr.io (the Makefile's + # REGISTRY_CREDS; any other registry needs the secrets). The # `release` job has it too, for `make finalize`'s manifest lists. permissions: contents: write diff --git a/Makefile b/Makefile index 01de2cb..261860e 100644 --- a/Makefile +++ b/Makefile @@ -124,6 +124,24 @@ sbom: check-arch repo-check release-assets: check-arch repo-check image-smoke test -f $(ASSET).tar && test -f $(ASSET).spdx.json +# Registry credentials for publish and finalize, as shell that sets $user and +# $pass. The OCI_REGISTRY_USERNAME/OCI_REGISTRY_PASSWORD secrets win when +# set. Otherwise, on ghcr.io only (a GITHUB_TOKEN is valid nowhere else), the +# workflow's own GITHUB_ACTOR/GITHUB_TOKEN are used (release.yml grants +# `packages: write`). Anywhere else the missing secret is an error. +REGISTRY_CREDS = user="$$OCI_REGISTRY_USERNAME"; pass="$$OCI_REGISTRY_PASSWORD"; \ + if [ -z "$$user" ]; then \ + if [ "$(OCI_REGISTRY)" != ghcr.io ]; then \ + echo "error: OCI_REGISTRY_USERNAME is not set; it and OCI_REGISTRY_PASSWORD are required for registry $(OCI_REGISTRY) (GITHUB_TOKEN works only for ghcr.io)" >&2; exit 1; \ + fi; \ + user="$$GITHUB_ACTOR"; pass="$$GITHUB_TOKEN"; \ + if [ -z "$$user" ] || [ -z "$$pass" ]; then \ + echo "error: OCI_REGISTRY_USERNAME is not set and GITHUB_ACTOR/GITHUB_TOKEN are empty; set the OCI_REGISTRY_* secrets or run in GitHub Actions" >&2; exit 1; \ + fi; \ + elif [ -z "$$pass" ]; then \ + echo "error: OCI_REGISTRY_USERNAME is set but OCI_REGISTRY_PASSWORD is empty" >&2; exit 1; \ + fi + # Copies the tested tar from dist/ to the registry (skopeo copy --all # oci-archive:... docker://...), records the pushed index digest in # dist/.digest for finalize, and logs in through a throwaway authfile @@ -136,9 +154,10 @@ ifeq ($(OCI_REGISTRY),) else @$(REPO_GUARD) @test -f $(ASSET).tar || { echo "$(ASSET).tar not found; run 'make build' first." >&2; exit 1; } - @auth="$$(mktemp -d)" && chmod 0777 "$$auth" && trap 'rm -rf "$$auth"' EXIT && \ - printf '%s' "$$OCI_REGISTRY_PASSWORD" | bash scripts/skopeo.sh -i -v "$$auth:/auth" -- \ - login --authfile /auth/auth.json -u "$$OCI_REGISTRY_USERNAME" --password-stdin "$(OCI_REGISTRY)" && \ + @$(REGISTRY_CREDS); \ + auth="$$(mktemp -d)" && chmod 0777 "$$auth" && trap 'rm -rf "$$auth"' EXIT && \ + printf '%s' "$$pass" | bash scripts/skopeo.sh -i -v "$$auth:/auth" -- \ + login --authfile /auth/auth.json -u "$$user" --password-stdin "$(OCI_REGISTRY)" && \ bash scripts/skopeo.sh -v "$$auth:/auth:ro" -- \ copy --all --authfile /auth/auth.json --digestfile /$(ASSET).digest \ oci-archive:/$(ASSET).tar docker://$(OCI_REGISTRY)/$(OCI_NAME):$(RELEASE_VERSION)-$(RELEASE_ARCH) @@ -190,9 +209,10 @@ ifeq ($(OCI_REGISTRY),) @echo "OCI_REGISTRY not set -- no manifest list to create." else @$(REPO_GUARD) - @orig="$${DOCKER_CONFIG:-$$HOME/.docker}" && export DOCKER_CONFIG="$$(mktemp -d)" && trap 'rm -rf "$$DOCKER_CONFIG"' EXIT && \ + @$(REGISTRY_CREDS); \ + orig="$${DOCKER_CONFIG:-$$HOME/.docker}" && export DOCKER_CONFIG="$$(mktemp -d)" && trap 'rm -rf "$$DOCKER_CONFIG"' EXIT && \ if [ -d "$$orig/cli-plugins" ]; then ln -s "$$orig/cli-plugins" "$$DOCKER_CONFIG/cli-plugins"; fi && \ - printf '%s' "$$OCI_REGISTRY_PASSWORD" | docker login "$(OCI_REGISTRY)" -u "$$OCI_REGISTRY_USERNAME" --password-stdin && \ + printf '%s' "$$pass" | docker login "$(OCI_REGISTRY)" -u "$$user" --password-stdin && \ srcs="" && for a in $(ARCHES); do \ d="$$(cat dist/$(IMAGE)-$(RELEASE_VERSION)-$$a.digest)" || exit 1; \ srcs="$$srcs $(OCI_REGISTRY)/$(OCI_NAME)@$$d"; \ diff --git a/docs/TEMPLATE.md b/docs/TEMPLATE.md index d3ff227..764d6a2 100644 --- a/docs/TEMPLATE.md +++ b/docs/TEMPLATE.md @@ -240,7 +240,11 @@ The explicit secrets remain the way to reach any other registry, or a package outside that repository's reach, and an instance that sets them should prefer them. Base's own `publish` and `finalize` are no-ops and log in nowhere, so each instance implements this fallback in its own -`Makefile`. Trade-off: the token is in the job env, so every per-arch +`Makefile`; template-docker's does: `publish` and `finalize` use the +`OCI_REGISTRY_USERNAME`/`OCI_REGISTRY_PASSWORD` secrets when +`OCI_REGISTRY_USERNAME` is set, else `GITHUB_ACTOR`/`GITHUB_TOKEN` when +`OCI_REGISTRY` is `ghcr.io` (a `GITHUB_TOKEN` is valid nowhere else), else +fail naming the missing secret. Trade-off: the token is in the job env, so every per-arch target (`build`, `sbom`, `release-assets`), not only `publish`, can write those packages. diff --git a/scripts/README.md b/scripts/README.md index b162bf1..a2e6d9e 100644 --- a/scripts/README.md +++ b/scripts/README.md @@ -32,7 +32,9 @@ (passive) in its network namespace, with rule levels from the instance-owned `.zap/rules.tsv`; reports go to `dist/dast/`. - `test-image-scripts.sh` — the `image-scripts-test` hook: offline tests of - the Makefile's repo derivation and these scripts. + the Makefile's repo derivation, the `publish`/`finalize` registry + credentials (secrets, the ghcr.io `GITHUB_TOKEN` fallback, the non-ghcr + failure, no password in an argv; docker stubbed) and these scripts. - `image-smoke.sh` — `make image-smoke` (a `release-check` prerequisite): converts the arch's image in the release OCI tar from `dist/` to a docker-archive with the pinned skopeo (`docker load` can't read a buildx diff --git a/scripts/test-image-scripts.sh b/scripts/test-image-scripts.sh index fac0572..37cf6a2 100755 --- a/scripts/test-image-scripts.sh +++ b/scripts/test-image-scripts.sh @@ -114,6 +114,57 @@ out="$(sk -- copy a b)" || fail "skopeo.sh: no docker args should work" out="$(sk -i -- login)" || fail "skopeo.sh: docker args should work" [[ "$out" == *" -i img login" ]] || fail "skopeo.sh: unexpected command: $out" +# Makefile publish/finalize credentials, with docker stubbed (it records its +# argv and the stdin of any --password-stdin login): the OCI_REGISTRY_* secrets +# win; with them unset ghcr.io falls back to GITHUB_ACTOR/GITHUB_TOKEN; any +# other registry fails naming the missing secret; the password is only ever +# on stdin, never in an argv. +cr="$tmp/creds" +mkdir -p "$cr/bin" "$cr/dist" +ln -s "$root/scripts" "$cr/scripts" +: >"$cr/dist/r-1.0.0-amd64.tar" +cat >"$cr/bin/docker" <<'EOF_STUB' +#!/usr/bin/env bash +printf '%s\n' "$*" >>"$STUB_LOG" +case "$*" in *--password-stdin*) cat >"$STUB_STDIN" ;; esac +EOF_STUB +chmod +x "$cr/bin/docker" +# creds_run TARGET REGISTRY [VAR=value ...]: prints make's output (call it in +# $(...), then read the exit code with creds_rc); the stub's argv log is +# left in $cr/log and its stdin in $cr/stdin. +creds_run() { + local target="$1" registry="$2" + shift 2 + printf 'sha256:abc' >"$cr/dist/r-1.0.0-amd64.digest" # finalize deletes it + rm -f "$cr/log" "$cr/stdin" + : >"$cr/log" + local rc=0 + (cd "$cr" && env -u OCI_REGISTRY_USERNAME -u OCI_REGISTRY_PASSWORD -u GITHUB_ACTOR -u GITHUB_TOKEN \ + PATH="$cr/bin:$PATH" STUB_LOG="$cr/log" STUB_STDIN="$cr/stdin" GITHUB_REPOSITORY=o/r \ + RELEASE_VERSION=1.0.0 RELEASE_ARCH=amd64 ARCHES=amd64 "$@" \ + make -s -f "$root/Makefile" "$target" OCI_REGISTRY="$registry" 2>&1) || rc=$? + printf '%s' "$rc" >"$cr/rc" +} +creds_rc() { cat "$cr/rc"; } +for target in publish finalize; do + out="$(creds_run "$target" ghcr.io OCI_REGISTRY_USERNAME=u OCI_REGISTRY_PASSWORD=sekret GITHUB_ACTOR=a GITHUB_TOKEN=tok)" + expect_eq "$target secrets: exit" 0 "$(creds_rc)" + expect_eq "$target secrets: stdin" sekret "$(cat "$cr/stdin")" + [[ "$(cat "$cr/log")" == *" -u u "* ]] || fail "$target secrets: username not u: $(cat "$cr/log")" + out="$(creds_run "$target" ghcr.io GITHUB_ACTOR=a GITHUB_TOKEN=tok)" + expect_eq "$target ghcr fallback: exit" 0 "$(creds_rc)" + expect_eq "$target ghcr fallback: stdin" tok "$(cat "$cr/stdin")" + [[ "$(cat "$cr/log")" == *" -u a "* ]] || fail "$target ghcr fallback: username not a: $(cat "$cr/log")" + [[ "$(cat "$cr/log")" != *tok* && "$(cat "$cr/log")" != *sekret* ]] || fail "$target: password in argv: $(cat "$cr/log")" + out="$(creds_run "$target" quay.io GITHUB_ACTOR=a GITHUB_TOKEN=tok)" + [[ "$(creds_rc)" != 0 && "$out" == *OCI_REGISTRY_USERNAME* ]] || fail "$target non-ghcr without secrets should fail naming OCI_REGISTRY_USERNAME (rc=$(creds_rc)): $out" + [[ ! -s "$cr/log" ]] || fail "$target non-ghcr: docker ran: $(cat "$cr/log")" + out="$(creds_run "$target" ghcr.io)" + [[ "$(creds_rc)" != 0 && "$out" == *GITHUB_TOKEN* ]] || fail "$target ghcr without any credential should fail (rc=$(creds_rc)): $out" + out="$(creds_run "$target" ghcr.io OCI_REGISTRY_USERNAME=u GITHUB_ACTOR=a GITHUB_TOKEN=tok)" + [[ "$(creds_rc)" != 0 && "$out" == *OCI_REGISTRY_PASSWORD* ]] || fail "$target username without password should fail naming OCI_REGISTRY_PASSWORD (rc=$(creds_rc)): $out" +done + # image-dast.sh in a scratch copy of the repo (it works relative to its own # repo root), with docker and curl stubbed: skips without a tar, wires ZAP to # the app container's network and the rules file, and maps zap-baseline.py's