Skip to content

Commit 5cb83cb

Browse files
committed
feat(ci): add sboms and cosign verification for official build artifacts
1 parent bba0b76 commit 5cb83cb

File tree

4 files changed

+137
-11
lines changed

4 files changed

+137
-11
lines changed

.github/workflows/ci-container.yml

Lines changed: 98 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -19,41 +19,58 @@ on:
1919

2020
permissions:
2121
contents: read
22+
id-token: write
23+
attestations: write
2224

2325
jobs:
24-
# Builds Container only if a new push to main branch, a tag or a pull request from a source
25-
# branch within the repository
26+
# Builds and pushes the container image for branch pushes, PRs, and release tags.
27+
# On tag events, also signs the image and attests a signed SBOM to DockerHub via cosign.
2628
ci-build-container:
2729
name: ci-build-container
2830
runs-on: ubuntu-latest
2931
steps:
32+
# Check out the repository so the Dockerfile and source are available to the build.
3033
- name: Checkout
3134
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
3235

36+
# Register QEMU emulators so Docker Buildx can build non-native architectures (arm, s390x, etc).
3337
- name: Set up QEMU
3438
uses: docker/setup-qemu-action@ce360397dd3f832beb865e1373c09c0e9f86d70a # v4.0.0
3539

40+
# Create a multi-platform-capable Buildx builder instance on the runner.
3641
- name: Set up Docker Buildx
3742
uses: docker/setup-buildx-action@4d04d5d9486b7bd6fa91e7baf45bbb4f8b9deedd # v4.0.0
3843

44+
# Authenticate to DockerHub so subsequent push steps and cosign attestation pushes succeed.
3945
- name: Login to DockerHub
4046
uses: docker/login-action@b45d80f862d83dbcd57f89517bcf500b2ab88fb2 # v4.0.0
4147
with:
4248
username: ${{ secrets.DOCKERHUB_USERNAME }}
4349
password: ${{ secrets.DOCKERHUB_TOKEN }}
4450

51+
# Install cosign once for all tag-based signing and attestation steps below.
52+
# Uses keyless signing — no private key required; identity is proven via the GitHub Actions
53+
# OIDC token issued to this workflow, rooted in Sigstore Fulcio and logged in Rekor.
54+
- name: Install cosign
55+
if: ${{ startsWith(github.ref, 'refs/tags/v') }}
56+
uses: sigstore/cosign-installer@ba7bc0a3fef59531c69a25acd34668d6d3fe6f22 # v4.1.0
57+
58+
# Parse the branch name from GITHUB_REF for use as the image tag on non-tag branch pushes.
4559
- name: Extract branch from github ref - New Push
4660
if: ${{ startsWith(github.ref, 'refs/tags/v') != true && github.event_name != 'pull_request' }}
4761
shell: bash
4862
run: echo "branch=${GITHUB_REF#refs/heads/}" >> "$GITHUB_OUTPUT"
4963
id: extract_branch
5064

65+
# Parse the version tag from GITHUB_REF for use as the image tag on tag pushes.
5166
- name: Extract tag from github ref - New Release
5267
if: ${{ startsWith(github.ref, 'refs/tags/v') }}
5368
shell: bash
5469
run: echo "tag=${GITHUB_REF#refs/tags/}" >> "$GITHUB_OUTPUT"
5570
id: extract_tag
5671

72+
# Build and push a multi-arch image tagged with the branch name on direct pushes to tracked
73+
# branches (master, v*, prep-v*). Not run on PRs or tag pushes.
5774
- name: Build and push - New Push
5875
uses: docker/build-push-action@d08e5c354a6adb9ed34480a06d141179aa583294 # v7.0.0
5976
if: ${{ startsWith(github.ref, 'refs/tags/v') != true && github.event_name != 'pull_request' }}
@@ -71,22 +88,30 @@ jobs:
7188
RUNTIME_BASE=${{ inputs.runtime-base }}
7289
tags: cloudnativelabs/kube-router-git:${{ steps.extract_branch.outputs.branch }}
7390

91+
# Build and push a single-arch (amd64) image tagged with the PR number for pull requests.
92+
# Multi-arch is skipped here as it adds 30+ minutes to PR feedback time.
7493
- name: Build and push - New PR
7594
uses: docker/build-push-action@d08e5c354a6adb9ed34480a06d141179aa583294 # v7.0.0
7695
if: github.event_name == 'pull_request'
7796
with:
7897
context: .
79-
# Don't build multi arch images for PR as they take more than 30 min to build
8098
platforms: linux/amd64
8199
push: true
82100
build-args: |
83101
BUILDTIME_BASE=${{ inputs.buildtime-base }}
84102
RUNTIME_BASE=${{ inputs.runtime-base }}
85103
tags: cloudnativelabs/kube-router-git:PR-${{ github.event.pull_request.number }}
86104

87-
# Tagging a release candidate, don't update latest
105+
# -----------------------------------------------------------------------------------------
106+
# Release Candidate tag (e.g. v2.8.0-rc1): build, sign, generate SBOM, and attest to registry.
107+
# The `latest` tag is NOT updated for release candidates.
108+
# -----------------------------------------------------------------------------------------
109+
110+
# Build and push the multi-arch release candidate image. The step id captures the digest
111+
# so subsequent signing and attestation steps can reference the exact immutable image.
88112
- name: Build and push - New Tag (Release Candidate)
89113
uses: docker/build-push-action@d08e5c354a6adb9ed34480a06d141179aa583294 # v7.0.0
114+
id: push_rc
90115
if: ${{ startsWith(github.ref, 'refs/tags/v') && contains(github.ref, '-rc') }}
91116
with:
92117
context: .
@@ -103,9 +128,51 @@ jobs:
103128
tags: |
104129
cloudnativelabs/kube-router:${{ steps.extract_tag.outputs.tag }}
105130
106-
# Tagging a proper release, update latest
131+
# Sign the RC image digest with cosign (keyless). The signature is pushed to DockerHub as a
132+
# sibling OCI artifact and the signing event is recorded in the Rekor transparency log.
133+
# Verify with: cosign verify --certificate-identity-regexp "https://github.com/cloudnativelabs/.*"
134+
# --certificate-oidc-issuer "https://token.actions.githubusercontent.com"
135+
# cloudnativelabs/kube-router:<rc-tag>
136+
- name: Sign container image - New Tag (Release Candidate)
137+
if: ${{ startsWith(github.ref, 'refs/tags/v') && contains(github.ref, '-rc') }}
138+
run: cosign sign --yes cloudnativelabs/kube-router@${{ steps.push_rc.outputs.digest }}
139+
140+
# Generate an SPDX-JSON SBOM for the RC image using Syft. The SBOM is written to a local
141+
# file for the attestation step and also uploaded as a workflow artifact.
142+
- name: Generate SBOM for container image - New Tag (Release Candidate)
143+
uses: anchore/sbom-action@e22c389904149dbc22b58101806040fa8d37a610 # v0.24.0
144+
id: sbom_rc
145+
if: ${{ startsWith(github.ref, 'refs/tags/v') && contains(github.ref, '-rc') }}
146+
with:
147+
image: cloudnativelabs/kube-router@${{ steps.push_rc.outputs.digest }}
148+
format: spdx-json
149+
artifact-name: kube-router-${{ github.ref_name }}-image-sbom.spdx.json
150+
output-file: ./container-sbom-rc.spdx.json
151+
152+
# Wrap the SBOM in a signed in-toto attestation and push it to DockerHub alongside the image.
153+
# Users can retrieve and verify it with:
154+
# cosign verify-attestation --type spdxjson
155+
# --certificate-identity-regexp "https://github.com/cloudnativelabs/.*"
156+
# --certificate-oidc-issuer "https://token.actions.githubusercontent.com"
157+
# cloudnativelabs/kube-router:<rc-tag> | jq -r '.payload' | base64 -d | jq '.predicate'
158+
- name: Attest SBOM to container image - New Tag (Release Candidate)
159+
if: ${{ startsWith(github.ref, 'refs/tags/v') && contains(github.ref, '-rc') }}
160+
run: |
161+
cosign attest --yes \
162+
--predicate ./container-sbom-rc.spdx.json \
163+
--type spdxjson \
164+
cloudnativelabs/kube-router@${{ steps.push_rc.outputs.digest }}
165+
166+
# -----------------------------------------------------------------------------------------
167+
# Production release tag (e.g. v2.8.0): build, sign, generate SBOM, and attest to registry.
168+
# Both the versioned tag and `latest` are updated.
169+
# -----------------------------------------------------------------------------------------
170+
171+
# Build and push the multi-arch production release image. Updates both the versioned tag and
172+
# `latest`. The step id captures the digest for signing and attestation.
107173
- name: Build and push - New Tag
108174
uses: docker/build-push-action@d08e5c354a6adb9ed34480a06d141179aa583294 # v7.0.0
175+
id: push_release
109176
if: ${{ startsWith(github.ref, 'refs/tags/v') && ! contains(github.ref, '-rc') }}
110177
with:
111178
context: .
@@ -122,3 +189,29 @@ jobs:
122189
tags: |
123190
cloudnativelabs/kube-router:${{ steps.extract_tag.outputs.tag }}
124191
cloudnativelabs/kube-router:latest
192+
193+
# Sign the production release image digest with cosign (keyless). Same verification command
194+
# as the RC step above, using the production release tag.
195+
- name: Sign container image - New Tag
196+
if: ${{ startsWith(github.ref, 'refs/tags/v') && ! contains(github.ref, '-rc') }}
197+
run: cosign sign --yes cloudnativelabs/kube-router@${{ steps.push_release.outputs.digest }}
198+
199+
# Generate an SPDX-JSON SBOM for the production release image using Syft.
200+
- name: Generate SBOM for container image - New Tag
201+
uses: anchore/sbom-action@e22c389904149dbc22b58101806040fa8d37a610 # v0.24.0
202+
id: sbom_release
203+
if: ${{ startsWith(github.ref, 'refs/tags/v') && ! contains(github.ref, '-rc') }}
204+
with:
205+
image: cloudnativelabs/kube-router@${{ steps.push_release.outputs.digest }}
206+
format: spdx-json
207+
artifact-name: kube-router-${{ github.ref_name }}-image-sbom.spdx.json
208+
output-file: ./container-sbom-release.spdx.json
209+
210+
# Wrap the SBOM in a signed in-toto attestation and push it to DockerHub alongside the image.
211+
- name: Attest SBOM to container image - New Tag
212+
if: ${{ startsWith(github.ref, 'refs/tags/v') && ! contains(github.ref, '-rc') }}
213+
run: |
214+
cosign attest --yes \
215+
--predicate ./container-sbom-release.spdx.json \
216+
--type spdxjson \
217+
cloudnativelabs/kube-router@${{ steps.push_release.outputs.digest }}

.github/workflows/ci-release.yml

Lines changed: 23 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,12 +12,14 @@ permissions:
1212
contents: read
1313

1414
jobs:
15-
# Runs GoReleaser to publish binaries and create a GitHub release
15+
# Runs GoReleaser to publish binaries, generate a binary SBOM, and produce SLSA provenance
1616
ci-goreleaser-tag:
1717
name: ci-goreleaser-tag
1818
runs-on: ubuntu-latest
1919
permissions:
2020
contents: write
21+
id-token: write
22+
attestations: write
2123
steps:
2224
# Check out the repository so GoReleaser has access to the full git history and source.
2325
- name: Check out code
@@ -29,6 +31,8 @@ jobs:
2931
with:
3032
go-version: ${{ inputs.go-version }}
3133

34+
# Build multi-arch binaries, create archives, generate checksums, and publish a GitHub release.
35+
# The release is created as a draft (see .goreleaser.yml) pending manual review before publish.
3236
- name: Run GoReleaser
3337
uses: goreleaser/goreleaser-action@ec59f474b9834571250b370d4735c50f8e2d1e29 # v7.0.0
3438
with:
@@ -37,3 +41,21 @@ jobs:
3741
args: release --clean
3842
env:
3943
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
44+
45+
# Generate a CycloneDX SBOM covering all release binaries in dist/ and attach it
46+
# to the GitHub release as a downloadable asset for binary consumers.
47+
- name: Generate SBOM for release binaries
48+
uses: anchore/sbom-action@e22c389904149dbc22b58101806040fa8d37a610 # v0.24.0
49+
with:
50+
path: ./dist
51+
format: cyclonedx-json
52+
artifact-name: kube-router-${{ github.ref_name }}-sbom.cyclonedx.json
53+
upload-release-assets: true
54+
55+
# Generate SLSA Build Level 2 provenance for the release binaries.
56+
# Uses GitHub's native attestation (first-party action, GA since June 2024).
57+
# Consumers can verify with: gh attestation verify <binary> --repo cloudnativelabs/kube-router
58+
- name: Generate SLSA provenance for release binaries
59+
uses: actions/attest@59d89421af93a897026c735860bf21b6eb4f7b26 # v4.1.0
60+
with:
61+
subject-checksums: ./dist/checksums.txt

.github/workflows/ci.yml

Lines changed: 14 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -18,8 +18,8 @@ permissions:
1818
contents: read
1919

2020
env:
21-
BUILDTIME_BASE: &buildtime_base "golang:1.25.7-alpine3.23"
22-
RUNTIME_BASE: &runtime_base "alpine:3.23"
21+
BUILDTIME_BASE: &buildtime_base "golang:1.25.7-alpine3.23@sha256:f6751d823c26342f9506c03797d2527668d095b0a15f1862cddb4d927a7a4ced"
22+
RUNTIME_BASE: &runtime_base "alpine:3.23@sha256:25109184c71bdad752c8312a8623239686a9a2071e8825f20acb8f2198c3f659"
2323
GO_VERSION: &go_version "~1.25.7"
2424

2525
jobs:
@@ -34,9 +34,15 @@ jobs:
3434
# Phase 2: Build and push the container image.
3535
# Only runs for non-fork PRs and direct pushes — skipped for dependabot and external fork PRs
3636
# to prevent secret exposure to untrusted code.
37+
# id-token: write — required for keyless cosign signing via Sigstore OIDC
38+
# attestations: write — required for pushing SBOM attestations to DockerHub
3739
container:
3840
needs: checks
3941
if: ${{ github.event_name != 'pull_request' || (github.event.pull_request.head.repo.full_name == github.repository && github.actor != 'dependabot[bot]') }}
42+
permissions:
43+
contents: read
44+
id-token: write
45+
attestations: write
4046
uses: ./.github/workflows/ci-container.yml
4147
with:
4248
buildtime-base: *buildtime_base
@@ -47,12 +53,17 @@ jobs:
4753

4854
# Phase 3: Publish a versioned release via GoReleaser.
4955
# Only runs on tag pushes (v*) after the container build succeeds.
50-
# contents: write is required here in the caller — called workflows cannot self-elevate permissions.
56+
# contents: write — required for GoReleaser to create the GitHub release
57+
# id-token: write — required for keyless cosign signing via Sigstore OIDC
58+
# attestations: write — required for SLSA provenance and SBOM attestation on release binaries
59+
# Called workflows cannot self-elevate permissions; all must be granted here in the caller.
5160
release:
5261
needs: container
5362
if: ${{ startsWith(github.ref, 'refs/tags/v') }}
5463
permissions:
5564
contents: write
65+
id-token: write
66+
attestations: write
5667
uses: ./.github/workflows/ci-release.yml
5768
with:
5869
go-version: *go_version

Makefile

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,14 +18,14 @@ MAKEFILE_DIR=$(dir $(realpath $(firstword $(MAKEFILE_LIST))))
1818
UPSTREAM_IMPORT_PATH=$(GOPATH)/src/github.com/cloudnativelabs/kube-router/
1919
BUILD_IN_DOCKER?=true
2020
# See Versions: https://hub.docker.com/_/golang
21-
DOCKER_BUILD_IMAGE?=golang:1.25.7-alpine3.23
21+
DOCKER_BUILD_IMAGE?=golang:1.25.7-alpine3.23@sha256:f6751d823c26342f9506c03797d2527668d095b0a15f1862cddb4d927a7a4ced
2222
## These variables are used by the Dockerfile as the bases for building and creating the runtime container
2323
## During CI these come from .github/workflows/ci.yaml below we define for local builds as well
2424
GO_CACHE?=$(shell go env GOCACHE)
2525
GO_MOD_CACHE?=$(shell go env GOMODCACHE)
2626
BUILDTIME_BASE?=$(DOCKER_BUILD_IMAGE)
2727
# See Versions: https://hub.docker.com/_/alpine
28-
RUNTIME_BASE?=alpine:3.23
28+
RUNTIME_BASE?=alpine:3.23@sha256:25109184c71bdad752c8312a8623239686a9a2071e8825f20acb8f2198c3f659
2929
# See Versions: https://hub.docker.com/r/golangci/golangci-lint/tags
3030
DOCKER_LINT_IMAGE?=golangci/golangci-lint:v2.8.0
3131
# See Versions: https://hub.docker.com/r/tmknom/markdownlint/tags

0 commit comments

Comments
 (0)