From aeb64f30f5a8d8919ded3f3790cd8b79b1a27161 Mon Sep 17 00:00:00 2001 From: Eike Haller <58111764+eksrha@users.noreply.github.com> Date: Thu, 1 Oct 2026 13:58:10 +0200 Subject: [PATCH] fix: sign container images by digest in release workflows Sign the pushed images with keyless cosign (GitHub OIDC) after push, and document how to verify them. Closes #5 Claude-Session: https://claude.ai/code/session_01F31tj4MqKr67gzt3awyM4m --- .github/workflows/release-models.yml | 19 +++++++++++++++++++ .github/workflows/release.yml | 13 +++++++++++++ README.md | 23 +++++++++++++++++++++++ scripts/sign-image.sh | 17 +++++++++++++++++ 4 files changed, 72 insertions(+) create mode 100755 scripts/sign-image.sh diff --git a/.github/workflows/release-models.yml b/.github/workflows/release-models.yml index 2649292..8e2df95 100644 --- a/.github/workflows/release-models.yml +++ b/.github/workflows/release-models.yml @@ -19,6 +19,7 @@ on: permissions: contents: read packages: write + id-token: write # cosign keyless OIDC signing env: REGISTRY: ghcr.io/layer87-labs @@ -54,6 +55,12 @@ jobs: docker push ${{ env.REGISTRY }}/tei-model-init:${{ env.MODEL_TAG }} docker push ${{ env.REGISTRY }}/tei-model-init:latest + - name: Install cosign + uses: sigstore/cosign-installer@v3 + + - name: Sign image (keyless via OIDC, by digest) + run: scripts/sign-image.sh "${{ env.REGISTRY }}/tei-model-init:${{ env.MODEL_TAG }}" + reranker-model: if: > github.event_name == 'push' || @@ -85,6 +92,12 @@ jobs: docker push ${{ env.REGISTRY }}/tei-reranker-model-init:${{ env.MODEL_TAG }} docker push ${{ env.REGISTRY }}/tei-reranker-model-init:latest + - name: Install cosign + uses: sigstore/cosign-installer@v3 + + - name: Sign image (keyless via OIDC, by digest) + run: scripts/sign-image.sh "${{ env.REGISTRY }}/tei-reranker-model-init:${{ env.MODEL_TAG }}" + whisper-model: if: > github.event_name == 'push' || @@ -116,3 +129,9 @@ jobs: . docker push ${{ env.REGISTRY }}/whisper:${{ env.MODEL_TAG }} docker push ${{ env.REGISTRY }}/whisper:latest + + - name: Install cosign + uses: sigstore/cosign-installer@v3 + + - name: Sign image (keyless via OIDC, by digest) + run: scripts/sign-image.sh "${{ env.REGISTRY }}/whisper:${{ env.MODEL_TAG }}" diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 636b28b..b628568 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -9,6 +9,7 @@ on: permissions: contents: write packages: write + id-token: write # cosign keyless OIDC signing env: REGISTRY: ghcr.io/layer87-labs @@ -65,6 +66,18 @@ jobs: make docker/tei-runtime VERSION=${{ env.VERSION }} docker push ${{ env.REGISTRY }}/tei-runtime:${{ env.VERSION }} + # ── Sign images (keyless cosign via GitHub OIDC, by digest) ───────── + + - name: Install cosign + uses: sigstore/cosign-installer@v3 + + - name: Sign code images + run: | + scripts/sign-image.sh \ + "${{ env.REGISTRY }}/inference-router:${{ env.VERSION }}" \ + "${{ env.REGISTRY }}/tei-base:${{ env.VERSION }}" \ + "${{ env.REGISTRY }}/tei-runtime:${{ env.VERSION }}" + # ── Helm chart (OCI → ghcr.io) ─────────────────────────────────────── - name: Package and push Helm chart diff --git a/README.md b/README.md index 9361de0..f855456 100644 --- a/README.md +++ b/README.md @@ -101,6 +101,29 @@ All images are published to `ghcr.io/layer87-labs/`: All images run as non-root with no privilege escalation. +### Verify image signatures + +Images built by the release workflows are signed with +[cosign](https://github.com/sigstore/cosign) keyless via GitHub OIDC. Verify by +digest against the exact workflow identity: + +```bash +IMAGE=ghcr.io/layer87-labs/inference-router +DIGEST=$(docker buildx imagetools inspect "$IMAGE:" --format '{{json .Manifest}}' | jq -r .digest) + +cosign verify \ + --certificate-identity 'https://github.com/layer87-labs/inference-stack/.github/workflows/release.yml@refs/heads/main' \ + --certificate-oidc-issuer 'https://token.actions.githubusercontent.com' \ + "$IMAGE@$DIGEST" +``` + +`release.yml` signs `inference-router`, `tei-base` and `tei-runtime`. +`release-models.yml` signs `tei-model-init`, `tei-reranker-model-init` and +`whisper`; for those, use the identity +`https://github.com/layer87-labs/inference-stack/.github/workflows/release-models.yml@refs/heads/main` +(for manual runs, the ref is the branch the workflow was started from). +Images published before signing was added are unsigned. + ## Build ```bash diff --git a/scripts/sign-image.sh b/scripts/sign-image.sh new file mode 100755 index 0000000..bdb5deb --- /dev/null +++ b/scripts/sign-image.sh @@ -0,0 +1,17 @@ +#!/usr/bin/env bash +# Sign pushed container images by digest with keyless cosign (GitHub OIDC). +# Usage: scripts/sign-image.sh ... +# Requires: cosign, docker, a prior `docker push`, and `id-token: write`. +set -euo pipefail + +for ref in "$@"; do + repo="${ref%:*}" + digest="$(docker inspect --format '{{range .RepoDigests}}{{println .}}{{end}}' "${ref}" \ + | grep "^${repo}@sha256:" | head -n1)" + if [ -z "${digest}" ]; then + echo "no pushed digest found for ${ref}" >&2 + exit 1 + fi + cosign sign --yes "${digest}" + echo "Signed ${digest}" >> "${GITHUB_STEP_SUMMARY:-/dev/null}" +done