forgejo-mcp/.tekton/release-tools/tasks/cosign-attach-sbom.yaml
Christoph Görn 2a0db63027
fix: 🔒️ resolve goern push token for cosign in image sign + SBOM tasks
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.
2026-06-01 10:57:11 +02:00

157 lines
6.8 KiB
YAML

apiVersion: tekton.dev/v1
kind: Task
metadata:
name: release-tools-cosign-attach-sbom
annotations:
tekton.dev/displayName: "syft scan + cosign attach SBOM to release-tools image"
spec:
description: |
Generates a CycloneDX SBOM via syft and attaches it to the published image
manifest using cosign attach sbom (OCI referrers). Consumers can retrieve
the SBOM via `cosign download sbom <image-ref>` or the OCI referrers API.
Registry: codeberg.org/operate-first/release-tools (Path A — reuses
codeberg-pusher Secret in op1st-pipelines for both push and attach).
Image choice: ubi9-minimal + curl-installed syft + cosign (NOT the
release-tools image itself). Same reasoning as cosign-sign-image-by-digest:
avoids circular reference at v1.0.0, has shell, install-then-run pattern.
params:
- name: IMAGE_REPO_DIGEST
description: Full digest reference to attach SBOM to (e.g. codeberg.org/operate-first/release-tools@sha256:abc123).
type: string
- name: SYFT_VERSION
description: syft CLI version pinned for this Task.
type: string
default: v1.18.1
- name: COSIGN_VERSION
description: cosign CLI version pinned for this Task.
type: string
default: v2.4.1
workspaces:
- name: source
mountPath: /workspace/source
- name: registry-credentials
mountPath: /workspace/registry-creds
optional: false
description: |
Dockerconfigjson Secret with write auth on the release-tools registry
path. cosign attach sbom pushes the SBOM as an OCI referrer object;
needs the same write auth as push-image-by-digest.
volumes:
- name: cosign-signing-key-images
secret:
secretName: cosign-signing-key-images
optional: false
results:
- name: SBOM_DIGEST
description: sha256 digest of the attached SBOM artifact.
steps:
- name: generate-and-attach-sbom
image: registry.access.redhat.com/ubi9/ubi-minimal:latest
workingDir: $(workspaces.source.path)
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
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; only tar+gzip need adding.
microdnf install -y tar gzip
# Install syft from the pinned release tag (not main branch).
curl -sSfL "https://raw.githubusercontent.com/anchore/syft/$(params.SYFT_VERSION)/install.sh" \
| sh -s -- -b /usr/local/bin "$(params.SYFT_VERSION)"
syft version
# Install cosign. Verify against release checksum manifest before chmod +x.
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
# Registry auth file fallback (.dockerconfigjson vs config.json).
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_REF="$(params.IMAGE_REPO_DIGEST)"
SBOM_FILE="$(workspaces.source.path)/release-tools-sbom.cdx.json"
# --- credential resolution shim ------------------------------------
# `cosign attach sbom` pushes the SBOM as an OCI referrer, so it needs
# the same registry WRITE auth as the image push. cosign/ggcr resolve
# auth by HOST only, whereas skopeo longest-prefix-matches path-scoped
# entries. The codeberg-pusher Secret carries a host-level auth
# (op1st-gitops, no goern package-write) plus a path-scoped goern auth.
# Without this, attaching to codeberg.org/goern/* gets
# "401 Unauthorized: reqPackageAccess". Re-key the longest-prefix match
# under the bare host so cosign finds the right token. No-op for
# operate-first/release-tools (its longest match IS the host key).
# See cosign-sign-image-by-digest.yaml for the full rationale.
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_REF%@*}"
HOST="${REPO%%/*}"
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
# -------------------------------------------------------------------
echo "Generating CycloneDX SBOM for ${IMAGE_REF}..."
syft "${IMAGE_REF}" --output cyclonedx-json="${SBOM_FILE}"
echo "SBOM written to ${SBOM_FILE}"
echo "Attaching SBOM to ${IMAGE_REF}..."
cosign attach sbom \
--sbom="${SBOM_FILE}" \
--type=cyclonedx \
"${IMAGE_REF}"
SBOM_DIGEST=$(sha256sum "${SBOM_FILE}" | cut -d' ' -f1)
printf '%s' "sha256:${SBOM_DIGEST}" > "$(results.SBOM_DIGEST.path)"
echo "SBOM attached. Local digest: sha256:${SBOM_DIGEST}"