cosign/go-containerregistry resolve registry auth by HOST only, while skopeo/buildah longest-prefix-match path-scoped entries. The shared codeberg-pusher Secret has a host-level auth (op1st-gitops, no goern package-write) plus a path-scoped codeberg.org/goern auth (write:package). push/promote use skopeo and pick goern by prefix; cosign host-matched op1st-gitops and got "401 Unauthorized: reqPackageAccess" when signing and attaching the SBOM for codeberg.org/goern/forgejo-mcp. Add a credential-resolution shim to both cosign tasks: fetch jq (static binary, curl + sha256 — step runs non-root, cannot microdnf-install), pick the longest auths key that path-prefixes the image repo, and re-key it under the bare host so cosign's host-only lookup finds the right token. No-op for operate-first/release-tools (its longest match IS the host key), so the release-tools publish pipeline is unaffected. Validated against the pushed v2.27.0-alpha.1 digest: both `cosign sign` and `cosign attach sbom` succeeded with the goern token.
156 lines
7.1 KiB
YAML
156 lines
7.1 KiB
YAML
apiVersion: tekton.dev/v1
|
|
kind: Task
|
|
metadata:
|
|
name: release-tools-cosign-sign-image-by-digest
|
|
annotations:
|
|
tekton.dev/displayName: "cosign sign release-tools image manifest by digest"
|
|
spec:
|
|
description: |
|
|
Signs the image manifest identified by digest using cosign. Pushes the
|
|
signature object to the registry alongside the image.
|
|
|
|
A3 asymmetry (intentional, per spec):
|
|
- This task mounts cosign-signing-key-images with optional: false (fail-closed).
|
|
A missing key causes a Failed PipelineRun; the unsigned image is NOT
|
|
promoted to a human-readable tag.
|
|
- The forgejo-mcp release pipeline uses optional: true (fail-open).
|
|
The asymmetry is intentional: an unsigned release-tools image is a
|
|
supply-chain regression; an unsigned checksums.txt is a UX regression only.
|
|
|
|
Image choice: uses ubi9-minimal + curl-installed cosign rather than the
|
|
release-tools image itself. Reasons:
|
|
1. Avoids circular reference at v1.0.0 (the image being built does not
|
|
yet exist when this task runs the first time).
|
|
2. ubi9-minimal has /bin/sh so Tekton script: blocks execute.
|
|
3. Single-step install pattern (proven in
|
|
.tekton/tasks/goreleaser-release.yaml after the v2.24.x lessons).
|
|
|
|
cosign needs registry write auth to push the signature object. Mounts the
|
|
same registry-credentials workspace as push-image-by-digest /
|
|
promote-image-tag.
|
|
params:
|
|
- name: IMAGE_REPO_DIGEST
|
|
description: Full digest reference (e.g. codeberg.org/operate-first/release-tools@sha256:abc123).
|
|
type: string
|
|
- name: COSIGN_VERSION
|
|
description: cosign CLI version pinned for this Task.
|
|
type: string
|
|
default: v2.4.1
|
|
workspaces:
|
|
- name: registry-credentials
|
|
mountPath: /workspace/registry-creds
|
|
optional: false
|
|
description: |
|
|
Dockerconfigjson Secret with write auth on the release-tools registry
|
|
path. Reuses codeberg-pusher Secret in op1st-pipelines (Path A
|
|
decision — see docs/design.md D4).
|
|
volumes:
|
|
- name: cosign-signing-key-images
|
|
secret:
|
|
# A3 fail-closed: optional: false ensures a missing key fails the PipelineRun
|
|
# rather than silently promoting an unsigned image.
|
|
secretName: cosign-signing-key-images
|
|
optional: false
|
|
steps:
|
|
- name: sign
|
|
image: registry.access.redhat.com/ubi9/ubi-minimal:latest
|
|
volumeMounts:
|
|
- name: cosign-signing-key-images
|
|
mountPath: /workspace/cosign-key
|
|
readOnly: true
|
|
env:
|
|
- name: COSIGN_KEY_PATH
|
|
value: /workspace/cosign-key/cosign.key
|
|
- name: COSIGN_PASSWORD
|
|
valueFrom:
|
|
secretKeyRef:
|
|
name: cosign-signing-key-images
|
|
# Real Secret has keys: cosign.key, cosign.password, cosign.pub
|
|
# (verified by inspection on nostromo op1st-pipelines namespace).
|
|
key: cosign.password
|
|
optional: false
|
|
- name: REGISTRY_AUTH_FILE
|
|
value: /workspace/registry-creds/.dockerconfigjson
|
|
script: |
|
|
#!/usr/bin/env sh
|
|
set -eu
|
|
|
|
# curl-minimal is pre-installed in ubi9-minimal; nothing extra needed.
|
|
curl -sSfLo /tmp/cosign-linux-amd64 \
|
|
"https://github.com/sigstore/cosign/releases/download/$(params.COSIGN_VERSION)/cosign-linux-amd64"
|
|
curl -sSfLo /tmp/cosign-checksums.txt \
|
|
"https://github.com/sigstore/cosign/releases/download/$(params.COSIGN_VERSION)/cosign_checksums.txt"
|
|
cd /tmp && grep " cosign-linux-amd64$" cosign-checksums.txt | sha256sum -c -
|
|
mv /tmp/cosign-linux-amd64 /usr/local/bin/cosign
|
|
rm /tmp/cosign-checksums.txt
|
|
chmod +x /usr/local/bin/cosign
|
|
cosign version
|
|
|
|
# Some registries publish config.json instead of .dockerconfigjson.
|
|
if [ ! -f "${REGISTRY_AUTH_FILE}" ]; then
|
|
REGISTRY_AUTH_FILE=/workspace/registry-creds/config.json
|
|
fi
|
|
if [ ! -f "${REGISTRY_AUTH_FILE}" ]; then
|
|
echo "ERROR: no registry auth file found in /workspace/registry-creds/" >&2
|
|
ls -la /workspace/registry-creds/ >&2
|
|
exit 1
|
|
fi
|
|
export REGISTRY_AUTH_FILE
|
|
|
|
IMAGE_DIGEST_REF="$(params.IMAGE_REPO_DIGEST)"
|
|
echo "Signing: ${IMAGE_DIGEST_REF}"
|
|
|
|
# --- credential resolution shim ------------------------------------
|
|
# cosign / go-containerregistry resolve registry auth by HOST only
|
|
# (e.g. "codeberg.org"), whereas skopeo/buildah longest-prefix-match
|
|
# path-scoped entries (e.g. "codeberg.org/goern"). The codeberg-pusher
|
|
# Secret carries BOTH: a host-level auth (op1st-gitops, no goern
|
|
# package-write) and a path-scoped goern auth (write:package on
|
|
# goern/*). push-image-by-digest (skopeo) picks goern by prefix and
|
|
# succeeds; cosign picks the host entry and gets
|
|
# "401 Unauthorized: reqPackageAccess".
|
|
#
|
|
# Replicate skopeo's behaviour: pick the longest auths key that is a
|
|
# path-prefix of this image's repo, then re-key it under the bare host
|
|
# so cosign's host-only lookup finds the right token. No-op for
|
|
# operate-first/release-tools, whose longest match IS the host key.
|
|
# jq is fetched as a static binary (curl + sha256, same supply-chain
|
|
# pattern as cosign/syft/goreleaser) because the step runs non-root and
|
|
# cannot microdnf-install.
|
|
JQ_VERSION=jq-1.7.1
|
|
JQ_SHA256=5942c9b0934e510ee61eb3e30273f1b3fe2590df93933a93d7c58b81d19c8ff5
|
|
curl -sSfLo /tmp/jq \
|
|
"https://github.com/jqlang/jq/releases/download/${JQ_VERSION}/jq-linux-amd64"
|
|
echo "${JQ_SHA256} /tmp/jq" | sha256sum -c -
|
|
chmod +x /tmp/jq
|
|
|
|
REPO="${IMAGE_DIGEST_REF%@*}" # strip @sha256:... -> codeberg.org/ns/img
|
|
HOST="${REPO%%/*}" # codeberg.org
|
|
# Longest auths key that equals REPO or is a "<key>/" path-prefix of it.
|
|
# Done in shell (not jq) to avoid jq's "." rebinding inside a pipe.
|
|
BEST_KEY=""
|
|
for k in $(/tmp/jq -r '.auths | keys[]' "${REGISTRY_AUTH_FILE}"); do
|
|
if [ "${REPO}" = "${k}" ] || [ "${REPO#"${k}"/}" != "${REPO}" ]; then
|
|
[ ${#k} -gt ${#BEST_KEY} ] && BEST_KEY="${k}"
|
|
fi
|
|
done
|
|
if [ -z "${BEST_KEY}" ]; then
|
|
echo "ERROR: no auth entry in ${REGISTRY_AUTH_FILE} matches ${REPO}" >&2
|
|
/tmp/jq -r '.auths | keys[]' "${REGISTRY_AUTH_FILE}" >&2 || true
|
|
exit 1
|
|
fi
|
|
echo "Auth entry '${BEST_KEY}' re-keyed under host '${HOST}' for cosign"
|
|
mkdir -p /tmp/cosign-docker
|
|
/tmp/jq -c --arg key "${BEST_KEY}" --arg host "${HOST}" \
|
|
'{auths: {($host): .auths[$key]}}' "${REGISTRY_AUTH_FILE}" \
|
|
> /tmp/cosign-docker/config.json
|
|
export DOCKER_CONFIG=/tmp/cosign-docker
|
|
export REGISTRY_AUTH_FILE=/tmp/cosign-docker/config.json
|
|
# -------------------------------------------------------------------
|
|
|
|
cosign sign \
|
|
--key="${COSIGN_KEY_PATH}" \
|
|
--yes \
|
|
"${IMAGE_DIGEST_REF}"
|
|
|
|
echo "cosign sign completed for ${IMAGE_DIGEST_REF}"
|