name: Shared - Sign Container Images
# Signs what shared-build-publish-image.yml pushed, with cosign keyless.## The publish workflow already attaches an SBOM and provenance, but nothing ties# a digest to the pipeline that built it: anyone who can push to the package can# push an image with the same tags. A keyless signature is a certificate from# Sigstore's Fulcio, issued against this workflow run's GitHub OIDC token and# logged in Rekor, so a consumer can verify that a digest came from a given# repository and workflow without anyone holding a key.## Separate from the publish workflow on purpose. Signing needs `id-token: write`,# and a reusable workflow cannot be granted more than its caller has: putting it# in the publish workflow would make every caller that does not grant it fail# before the first step. Here only callers who opt in add the job and the# permission.## Run it after the publish job in the same workflow run. It reads the# `image-digest-*` artifacts that job uploads, so it signs exactly the digests# that were pushed — never a tag, which could have moved in between.
on: workflow_call: inputs: registry: description: 'Container registry the images were pushed to' required: false type: string default: 'ghcr.io' runner: description: 'Runner type' required: false type: string default: 'ubuntu-latest'
permissions: contents: read packages: write id-token: write
jobs: sign: name: Sign images runs-on: ${{ inputs.runner }} timeout-minutes: 15
steps: - name: Download pushed digests uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 with: pattern: image-digest-* path: digests merge-multiple: true
- name: Install cosign uses: sigstore/cosign-installer@6f9f17788090df1f26f669e9d70d6ae9567deba6 # v4.1.2 with: cosign-release: 'v3.1.3'
- name: Login to the registry uses: docker/login-action@dbcb813823bdd20940b903addbd779551569679f # v4.6.0 with: registry: ${{ inputs.registry }} username: ${{ github.actor }} password: ${{ secrets.GITHUB_TOKEN }}
# By digest only. A ref without one is refused rather than signed: a tag # can move between the push and this step, and signing whatever it points # at now would vouch for an image this run never built. - name: Sign run: | set -euo pipefail shopt -s nullglob files=(digests/image-digest-*.txt) if [ ${#files[@]} -eq 0 ]; then echo "::error::No image-digest-* artifacts found. Run this after shared-build-publish-image.yml with push: true, in the same workflow run." exit 1 fi # Validate every ref before signing any, so a bad one fails the run # without leaving half the set signed. refs=() for f in "${files[@]}"; do ref=$(tr -d '[:space:]' < "${f}") case "${ref}" in *@sha256:*) refs+=("${ref}") ;; *) echo "::error::${f} holds '${ref}', not a digest reference; refusing to sign anything."; exit 1 ;; esac done { echo "### Signed images" echo "" echo "| Image |" echo "|-------|" } >> "$GITHUB_STEP_SUMMARY" for ref in "${refs[@]}"; do cosign sign --yes "${ref}" echo "| \`${ref}\` |" >> "$GITHUB_STEP_SUMMARY" doneShared (cross-cutting)·Reusable workflow·on: workflow_call
Shared Sign Images
Shared - Sign Container Images
.github/workflows/shared-sign-images.yml