ci: sign every published runner image by digest (zegit#262)
zegit's arc-runner-pin.sh pins a runner image only when cosign verifies its digest against the runner-image public key (zegit#143), but nothing signed the images, so the pin refused every day. Sign the digest with the RUNNER_IMAGE_COSIGN_KEY repository secret after the publish and verify the signature in the same run. sign_only re-signs an image published before signing existed. Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]> Claude-Session: https://claude.ai/code/session_0147GvtJgsTjDGcrfvJsx5jS
This commit is contained in:
1 parent
96ba41dfbf
commit
f9809550c8
1 file changed
+56
@@ -24,6 +24,11 @@ on:
|
||||
description: 'Image tag, e.g. 12.11.1-ci6 (upstream runner version + ci revision)'
|
||||
required: true
|
||||
type: string
|
||||
sign_only:
|
||||
description: 'Sign the already-published tag instead of building it (zegit#262: signs an image published before signing existed)'
|
||||
required: false
|
||||
default: false
|
||||
type: boolean
|
||||
push:
|
||||
branches: [master]
|
||||
paths:
|
||||
@@ -35,6 +40,7 @@ env:
|
||||
BUILDKIT_VERSION: v0.32.2
|
||||
REGISTRY: gitea.langenoja.se
|
||||
IMAGE_PATH: zegit-zoo/zegit-forgejo-runner
|
||||
COSIGN_VERSION: v3.1.3
|
||||
|
||||
jobs:
|
||||
build:
|
||||
@@ -78,6 +84,7 @@ jobs:
|
||||
if [ "${{ github.event_name }}" = "workflow_dispatch" ]; then
|
||||
echo "push=true" >> "$GITHUB_OUTPUT"
|
||||
echo "tag=${{ inputs.tag }}" >> "$GITHUB_OUTPUT"
|
||||
echo "sign_only=${{ inputs.sign_only }}" >> "$GITHUB_OUTPUT"
|
||||
else
|
||||
echo "push=false" >> "$GITHUB_OUTPUT"
|
||||
echo "tag=ci-validation" >> "$GITHUB_OUTPUT"
|
||||
@@ -103,6 +110,7 @@ jobs:
|
||||
> "$HOME/.docker/config.json"
|
||||
|
||||
- name: Build multi-arch
|
||||
if: ${{ steps.meta.outputs.sign_only != 'true' }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
REF="${REGISTRY}/${IMAGE_PATH}:${{ steps.meta.outputs.tag }}"
|
||||
@@ -165,6 +173,54 @@ jobs:
|
||||
--opt platform=linux/amd64,linux/arm64
|
||||
echo "✓ toolchain intact on linux/amd64 and linux/arm64"
|
||||
|
||||
# zegit#143/#262: a -ci<N> tag is only a name, and ZEGIT_CI_PACKAGE_TOKEN
|
||||
# (readable by every zegit-zoo branch run) can push one. Consumers pin
|
||||
# this image only when `cosign verify` accepts its digest with the
|
||||
# runner-image public key, so every published digest is signed here.
|
||||
# The private key is a REPOSITORY secret of the runner repos only, never
|
||||
# an org secret. Classic .sig tag, no transparency log: the registry is
|
||||
# private and verifiers use --insecure-ignore-tlog with a pinned key.
|
||||
- name: Sign the published image by digest
|
||||
if: ${{ steps.meta.outputs.push == 'true' }}
|
||||
env:
|
||||
COSIGN_KEY_PEM: ${{ secrets.RUNNER_IMAGE_COSIGN_KEY }}
|
||||
COSIGN_PASSWORD: ${{ secrets.RUNNER_IMAGE_COSIGN_PASSWORD }}
|
||||
TOKEN: ${{ secrets.ZEGIT_CI_PACKAGE_TOKEN }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
if [ -z "${COSIGN_KEY_PEM:-}" ] || [ -z "${COSIGN_PASSWORD:-}" ]; then
|
||||
echo "::error::RUNNER_IMAGE_COSIGN_KEY / RUNNER_IMAGE_COSIGN_PASSWORD are not visible to this run (repository secrets, zegit#262). An unsigned runner image is never pinned."
|
||||
exit 1
|
||||
fi
|
||||
case "$(uname -m)" in
|
||||
x86_64) arch=amd64; sum=4629c757b7618056f8ddd7e2625ae9fdd94c0372a65049520bc7d9df9efc7f71;;
|
||||
aarch64) arch=arm64; sum=c5d324e091826b0d7a78eb16fef316450b4eb9aaec045611c08ba06f5e73220a;;
|
||||
*) echo "::error::unsupported arch $(uname -m)"; exit 1;;
|
||||
esac
|
||||
bin="$GITHUB_WORKSPACE/.bin/cosign"
|
||||
curl -fsSL -o "$bin" "https://github.com/sigstore/cosign/releases/download/${COSIGN_VERSION}/cosign-linux-${arch}"
|
||||
echo "${sum} ${bin}" | sha256sum -c -
|
||||
chmod +x "$bin"
|
||||
tag="${{ steps.meta.outputs.tag }}"
|
||||
tmp=$(mktemp -d); trap 'rm -rf "$tmp"' EXIT
|
||||
# Credentials go through 0600 curl configs, never argv (zegit#139).
|
||||
( umask 077; printf 'user = "zegit-zoo:%s"\n' "$TOKEN" > "$tmp/basic.cfg" )
|
||||
tok=$(curl -fsS -K "$tmp/basic.cfg" "https://${REGISTRY}/v2/token?scope=repository:${IMAGE_PATH}:pull&service=container_registry" |
|
||||
python3 -c 'import json,sys; print(json.load(sys.stdin)["token"])')
|
||||
( umask 077; printf 'header = "Authorization: Bearer %s"\n' "$tok" > "$tmp/bearer.cfg" )
|
||||
digest=$(curl -fsS -I -K "$tmp/bearer.cfg" \
|
||||
-H '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' \
|
||||
"https://${REGISTRY}/v2/${IMAGE_PATH}/manifests/${tag}" | tr -d '\r' |
|
||||
awk -F': ' 'tolower($1)=="docker-content-digest"{print $2}')
|
||||
[[ "$digest" =~ ^sha256:[0-9a-f]{64}$ ]] || { echo "::error::no digest for ${IMAGE_PATH}:${tag}"; exit 1; }
|
||||
ref="${REGISTRY}/${IMAGE_PATH}@${digest}"
|
||||
printf '%s\n' "$COSIGN_KEY_PEM" > "$tmp/cosign.key"; chmod 600 "$tmp/cosign.key"
|
||||
"$bin" sign --yes --key "$tmp/cosign.key" --tlog-upload=false \
|
||||
--new-bundle-format=false --use-signing-config=false "$ref"
|
||||
"$bin" public-key --key "$tmp/cosign.key" > "$tmp/cosign.pub"
|
||||
"$bin" verify --key "$tmp/cosign.pub" --insecure-ignore-tlog "${REGISTRY}/${IMAGE_PATH}:${tag}@${digest}" >/dev/null
|
||||
echo "✓ signed and verified ${REGISTRY}/${IMAGE_PATH}:${tag}@${digest}"
|
||||
|
||||
- name: Summary
|
||||
run: |
|
||||
if [ "${{ steps.meta.outputs.push }}" = "true" ]; then
|
||||
|
||||
Reference in new issue
Block a user