From 2f641d7c3f1d472e58e3a9e26718c97aa990873b Mon Sep 17 00:00:00 2001 From: Alex Luong Date: Sat, 15 Aug 2026 00:12:11 +0700 Subject: [PATCH 1/2] chore(release): publish signed images with SBOM and provenance Turns on the two attestations dockers_v2 can emit and signs the published images with cosign, keyless via the release workflow's OIDC token. - sbom: true and buildx's default provenance, reversing the suppressions added while migrating to dockers_v2 to hold the artifact shape steady. - docker_signs with --key omitted, which is what selects keyless signing. - id-token: write on the job so cosign can exchange the OIDC token for a short-lived certificate. Declaring permissions replaces the defaults, so contents: write is listed explicitly for the GitHub release. Signing runs in the publish phase, so local `--snapshot` builds never invoke cosign. Verified: a full snapshot succeeds on a machine with no cosign at all. Co-Authored-By: Claude Opus 5 (1M context) --- .github/workflows/release.yml | 8 ++++++++ build/.goreleaser.yaml | 18 ++++++++++++++---- 2 files changed, 22 insertions(+), 4 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 674a4f7b..89799bb1 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -8,6 +8,12 @@ on: jobs: release: runs-on: ubuntu-latest + # Declaring permissions replaces the defaults entirely, so contents: write + # has to be listed for the GitHub release. id-token: write mints the OIDC + # token cosign exchanges for a short-lived signing certificate. + permissions: + contents: write + id-token: write steps: - name: Code checkout uses: actions/checkout@v5 @@ -36,6 +42,8 @@ jobs: uses: actions/setup-go@v5 with: go-version: 1.26.5 + - name: Install cosign + uses: sigstore/cosign-installer@v3 - name: Run GoReleaser uses: goreleaser/goreleaser-action@v6 with: diff --git a/build/.goreleaser.yaml b/build/.goreleaser.yaml index 937ab128..16d6719d 100644 --- a/build/.goreleaser.yaml +++ b/build/.goreleaser.yaml @@ -112,12 +112,14 @@ dockers_v2: platforms: - linux/amd64 - linux/arm64 - # No attestations, so the manifest keeps the same two platform entries as - # today. Takes both switches — buildx adds provenance on its own. - sbom: false + # Publish SBOM and provenance attestations. Both are on by default here: + # sbom: true adds --attest=type=sbom, and buildx attaches provenance itself + # whenever it pushes. They ride along as extra manifest entries reported as + # "unknown/unknown" platforms, which is how attestations are always carried — + # Docker Hub's UI filters them out of the tag listing. + sbom: true flags: - "--pull" - - "--provenance=false" labels: org.opencontainers.image.created: "{{ .Date }}" org.opencontainers.image.name: "{{ .ProjectName }}" @@ -125,3 +127,11 @@ dockers_v2: org.opencontainers.image.version: "{{ .Version }}" repository: "https://github.com/hookdeck/outpost" homepage: "https://hookdeck.com" + +# Sign published images with cosign, keyless via the workflow's OIDC token. The +# default args sign with a local cosign.key; dropping --key is what selects +# keyless. Runs in the publish phase, so --snapshot builds never reach it and +# local builds need neither cosign nor credentials. +docker_signs: + - artifacts: images + args: ["sign", "${artifact}@${digest}", "--yes"] From e961f7032471027d804b4714d062f420c105436a Mon Sep 17 00:00:00 2001 From: Alex Luong Date: Sat, 15 Aug 2026 00:27:16 +0700 Subject: [PATCH 2/2] chore(release): sign checksums.txt MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signs the checksums file with cosign, which covers every archive transitively. This is the artifact form supply-chain tooling looks for — OpenSSF Scorecard's Signed-Releases check inspects release assets for .sigstore.json and similar, and does not see container signatures. Unlike docker_signs, this pipe runs before publish, so it executes during snapshot builds too and fails when cosign is absent. The two Makefile targets that build snapshots locally now pass --skip=sign; verified a full snapshot still succeeds with no cosign installed. Co-Authored-By: Claude Opus 5 (1M context) --- Makefile | 4 ++-- build/.goreleaser.yaml | 15 +++++++++++---- 2 files changed, 13 insertions(+), 6 deletions(-) diff --git a/Makefile b/Makefile index 337767da..126ebb6e 100644 --- a/Makefile +++ b/Makefile @@ -17,7 +17,7 @@ build: @echo "Binaries built in ./bin/" build/goreleaser: - goreleaser release -f ./build/.goreleaser.yaml --snapshot --clean + goreleaser release -f ./build/.goreleaser.yaml --snapshot --clean --skip=sign build/outpost: go build -tags nomsgpack -o bin/outpost ./cmd/outpost @@ -144,7 +144,7 @@ logs: # Use docker/push to push to Docker Hub: DOCKER_USER= make docker/push TAG=v0.13.3-beta docker/build: @if [ -z "$(TAG)" ]; then echo "Usage: make docker/build TAG=v0.13.3-beta"; exit 1; fi - GORELEASER_CURRENT_TAG=$(TAG) goreleaser release -f ./build/.goreleaser.yaml --snapshot --clean + GORELEASER_CURRENT_TAG=$(TAG) goreleaser release -f ./build/.goreleaser.yaml --snapshot --clean --skip=sign # Tag and push image to Docker Hub under DOCKER_USER (e.g. make docker/push DOCKER_USER=alexbouchard TAG=v0.13.3-beta). # Requires: docker login first. diff --git a/build/.goreleaser.yaml b/build/.goreleaser.yaml index 16d6719d..109eb612 100644 --- a/build/.goreleaser.yaml +++ b/build/.goreleaser.yaml @@ -128,10 +128,17 @@ dockers_v2: repository: "https://github.com/hookdeck/outpost" homepage: "https://hookdeck.com" -# Sign published images with cosign, keyless via the workflow's OIDC token. The -# default args sign with a local cosign.key; dropping --key is what selects -# keyless. Runs in the publish phase, so --snapshot builds never reach it and -# local builds need neither cosign nor credentials. +# Signs checksums.txt, which covers every archive transitively. This one runs +# before publish, so local snapshot builds pass --skip=sign to avoid needing +# cosign installed. +signs: + - cmd: cosign + signature: "${artifact}.sigstore.json" + artifacts: checksum + args: ["sign-blob", "--bundle=${signature}", "${artifact}", "--yes"] + +# Keyless cosign signing via the workflow's OIDC token. Omitting --key is what +# selects keyless; publish-phase only, so --snapshot never invokes cosign. docker_signs: - artifacts: images args: ["sign", "${artifact}@${digest}", "--yes"]