From 2a2efb847e1847908e2f3ab9d0c38da01f36bd0b Mon Sep 17 00:00:00 2001 From: Anso Date: Fri, 10 Apr 2026 13:46:27 -0400 Subject: [PATCH] feat(release): sign and attest published docker images (#480) Every published Sencho Docker image is now signed with Sigstore cosign using GitHub Actions keyless OIDC, and ships with an embedded SBOM plus SLSA provenance attestation attached as OCI referrers on the image. Users can verify the signature and inspect the attestations themselves before pulling an image into production. cosign verify saelix/sencho: \ --certificate-identity-regexp "https://github.com/AnsoCode/Sencho/.*" \ --certificate-oidc-issuer https://token.actions.githubusercontent.com The release pipeline now also publishes a new moving minor tag (saelix/sencho:X.Y, e.g. 0.42) alongside the existing latest and immutable X.Y.Z tags, so operators who want the latest patch on a given minor line can pin without chasing every release. Since every 0.x minor is potentially breaking, the {{major}}-only tag is deferred until 1.0. cosign is installed via the sigstore/cosign-installer action pinned to a full commit SHA. The signing step batches every tag produced by metadata-action into a single cosign invocation pinned to the built digest, so all tags resolve to the same manifest and we save N-1 Rekor/Fulcio round trips. The job declares id-token: write so the ambient OIDC token can be minted. A new docs page (docs/reference/verifying-images.mdx) walks users through installing cosign, verifying an image, and inspecting the SBOM and provenance with docker buildx imagetools. A note calls out that signatures start shipping with v0.43.0, so older images failing verification is expected behavior and not tampering. --- .github/workflows/docker-publish.yml | 42 +++++++++++++++ CHANGELOG.md | 8 +++ docs/docs.json | 1 + docs/reference/verifying-images.mdx | 81 ++++++++++++++++++++++++++++ 4 files changed, 132 insertions(+) create mode 100644 docs/reference/verifying-images.mdx diff --git a/.github/workflows/docker-publish.yml b/.github/workflows/docker-publish.yml index 917e6ac3..ac1d669d 100644 --- a/.github/workflows/docker-publish.yml +++ b/.github/workflows/docker-publish.yml @@ -17,6 +17,8 @@ jobs: timeout-minutes: 30 permissions: contents: read + # Required for cosign keyless signing via GitHub OIDC. + id-token: write steps: - name: Check out the repo uses: actions/checkout@v6 @@ -35,16 +37,27 @@ jobs: username: ${{ secrets.DOCKERHUB_USERNAME }} password: ${{ secrets.DOCKERHUB_TOKEN }} + - name: Install cosign + uses: sigstore/cosign-installer@cad07c2e89fa2edd6e2d7bab4c1aa38e53f76003 # v4.1.1 + - name: Extract metadata (tags, labels) for Docker id: meta uses: docker/metadata-action@v6 with: images: saelix/sencho + # On a v-tag push we publish: + # latest always points at the newest release + # X.Y.Z the immutable semver tag + # X.Y moving minor tag for users who want latest patch + # The pre-1.0 constraint hides the {{major}}-only tag until we ship 1.0, + # since on 0.x every minor is potentially breaking. tags: | type=raw,value=latest,enable=${{ startsWith(github.ref, 'refs/tags/v') }} type=semver,pattern={{version}},enable=${{ startsWith(github.ref, 'refs/tags/v') }} + type=semver,pattern={{major}}.{{minor}},enable=${{ startsWith(github.ref, 'refs/tags/v') }} - name: Build and push Docker image + id: build uses: docker/build-push-action@v7 with: context: . @@ -54,3 +67,32 @@ jobs: labels: ${{ steps.meta.outputs.labels }} cache-from: type=registry,ref=saelix/sencho:buildcache cache-to: type=registry,ref=saelix/sencho:buildcache,mode=max + # SBOM + provenance attestations are embedded as OCI referrers on the + # published image. Inspect with: docker buildx imagetools inspect + sbom: true + provenance: mode=max + + - name: Sign published image with cosign (keyless) + # Signs every tag produced by metadata-action using the ambient GitHub + # OIDC token. No private keys, no secrets. Users verify with: + # cosign verify saelix/sencho: \ + # --certificate-identity-regexp "https://github.com/AnsoCode/Sencho/.*" \ + # --certificate-oidc-issuer https://token.actions.githubusercontent.com + env: + TAGS: ${{ steps.meta.outputs.tags }} + DIGEST: ${{ steps.build.outputs.digest }} + run: | + # Build a single cosign invocation with every tag pinned to the digest. + # All tags resolve to the same manifest, so one call signs them all and + # saves N-1 Rekor/Fulcio round trips. On workflow_dispatch $TAGS is empty + # and the refs array stays empty, so we no-op cleanly. + refs=() + while IFS= read -r tag; do + [ -n "$tag" ] || continue + refs+=("${tag}@${DIGEST}") + done <<< "$TAGS" + if [ ${#refs[@]} -gt 0 ]; then + cosign sign --yes "${refs[@]}" + else + echo "No tags to sign (likely a workflow_dispatch run on a non-tag ref)." + fi diff --git a/CHANGELOG.md b/CHANGELOG.md index 1726385e..efa7403a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -48,6 +48,14 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 Large payloads like `/api/templates` shrink roughly 5x on the wire, with Server-Sent Event streams explicitly excluded so real-time log tails and system metrics are not buffered. +* **release:** sign every published Docker image with Sigstore cosign keyless + signing, and embed an SBOM plus SLSA provenance attestation as OCI referrers on + the image. Users can now independently verify that a pulled image came from the + official GitHub Actions build pipeline (see the new "Verifying Docker Images" + reference page in the docs). The release pipeline also publishes a new moving + minor tag `saelix/sencho:X.Y` alongside the existing `latest` and immutable + `X.Y.Z` tags, so operators who want the latest patch on a given minor line can + pin without chasing every release. ### Fixed diff --git a/docs/docs.json b/docs/docs.json index 66b0a1c1..a7381e94 100644 --- a/docs/docs.json +++ b/docs/docs.json @@ -125,6 +125,7 @@ "pages": [ "features/node-compatibility", "reference/settings", + "reference/verifying-images", "reference/contact", "reference/security-advisories" ] diff --git a/docs/reference/verifying-images.mdx b/docs/reference/verifying-images.mdx new file mode 100644 index 00000000..ab000e52 --- /dev/null +++ b/docs/reference/verifying-images.mdx @@ -0,0 +1,81 @@ +--- +title: Verifying Docker Images +description: Verify that the Sencho image you pulled was built and signed by the official GitHub Actions pipeline. +--- + +Every published Sencho Docker image is signed by the official GitHub Actions pipeline using [Sigstore cosign](https://docs.sigstore.dev/) keyless signing. The signature is backed by a short-lived certificate issued by Sigstore's public Fulcio certificate authority, tied to the GitHub Actions workflow run that produced the image. You can verify this signature yourself before pulling the image into production. + +Every image also ships with an embedded SBOM (Software Bill of Materials) and [SLSA](https://slsa.dev) (Supply-chain Levels for Software Artifacts) provenance attestation, so you can inspect exactly what went into the image and which workflow run built it. + + + Image signing is available starting with Sencho **v0.43.0**. Earlier releases do not ship a cosign signature; running `cosign verify` against them will fail, which is expected and does not indicate tampering. If you are on an older version, the first step is to upgrade. + + +## Why verify + +Verification guarantees: + +1. The image was built from the [AnsoCode/Sencho](https://github.com/AnsoCode/Sencho) repository on GitHub. +2. The build ran inside a GitHub Actions workflow, not on someone's laptop. +3. The image has not been tampered with between publish and pull. + + + Verification is optional. If you skip it, Sencho still works exactly the same; you are just trusting Docker Hub to deliver the right bytes. Verification adds a second independent check. + + +## Install cosign + +[cosign](https://docs.sigstore.dev/system_config/installation/) is a single static binary. Install it once on any machine you use to pull Sencho. + + + +```bash macOS (Homebrew) +brew install cosign +``` + +```bash Linux (binary) +curl -Lo cosign https://github.com/sigstore/cosign/releases/latest/download/cosign-linux-amd64 +chmod +x cosign +sudo mv cosign /usr/local/bin/ +``` + +```bash Windows (Scoop) +scoop install cosign +``` + + + +## Verify a released image + +Replace `` with the version you intend to pull, for example `0.42.7`, `0.42`, or `latest`. + +```bash +cosign verify saelix/sencho: \ + --certificate-identity-regexp "https://github.com/AnsoCode/Sencho/.*" \ + --certificate-oidc-issuer https://token.actions.githubusercontent.com +``` + +A successful verification prints the signature, the Rekor transparency log entry, and the certificate that signed it. If the image is not signed by the official Sencho pipeline, the command exits with a non-zero status and you should not run that image. + +## Inspect the SBOM and provenance + +Both the SBOM and SLSA provenance are attached to the published image as [OCI referrers](https://docs.docker.com/build/attestations/attestation-storage/). You can inspect them with `docker buildx imagetools`: + +```bash +docker buildx imagetools inspect saelix/sencho: --format "{{json .SBOM}}" +docker buildx imagetools inspect saelix/sencho: --format "{{json .Provenance}}" +``` + +The SBOM lists every package baked into the image with versions and licenses. The provenance records the exact GitHub Actions workflow run, commit SHA, and builder that produced it. + +## Available tags + +Each release publishes two moving tags plus one immutable tag, so you can choose how aggressively you want to track updates: + +| Tag | Example | Updates on | +|---|---|---| +| `latest` | `saelix/sencho:latest` | Every release | +| `X.Y` | `saelix/sencho:0.42` | Every patch release in the `0.42.x` line | +| `X.Y.Z` | `saelix/sencho:0.42.7` | Never (immutable) | + +For production, pin to `X.Y.Z` or `X.Y` and verify the signature on every pull. For staging or development, `latest` is fine.