From 891a013ce619f3c2e6f513686300dd2838d15c8e Mon Sep 17 00:00:00 2001 From: Joshua Gilman Date: Fri, 9 Jan 2026 18:49:10 -0800 Subject: [PATCH 1/6] feat: add Docker image publishing Add Docker image publishing to GHCR and Docker Hub with: - Multi-architecture support (linux/amd64, linux/arm64) - Hadolint linting and Trivy security scanning in CI - Cosign keyless signing and SBOM attestation - Separate CI and release workflows Base image: dhi.io/debian-base:trixie (Docker Hardened Image) Co-Authored-By: Claude Opus 4.5 --- .dockerignore | 47 ++++++ .github/workflows/docker-ci.yml | 100 +++++++++++++ .github/workflows/docker-release.yml | 214 +++++++++++++++++++++++++++ Dockerfile | 61 ++++++++ 4 files changed, 422 insertions(+) create mode 100644 .dockerignore create mode 100644 .github/workflows/docker-ci.yml create mode 100644 .github/workflows/docker-release.yml create mode 100644 Dockerfile diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..12f0faa --- /dev/null +++ b/.dockerignore @@ -0,0 +1,47 @@ +# Git +.git +.gitignore + +# CI/CD (not needed in image) +.github/ +.goreleaser.yaml +.golangci.yml +justfile +release-please-config.json +.release-please-manifest.json + +# Documentation +docs/ +*.md + +# Build artifacts +dist/ +bin/ +coverage.* +profiles/ + +# Test data +testdata/ + +# Nix +flake.* + +# IDE/Editor +.idea/ +.vscode/ +*.swp +*.swo +*~ + +# OS files +.DS_Store +Thumbs.db + +# Claude/AI +.claude/ +CLAUDE.md +AGENTS.md + +# Demo/examples +demo.tape +install.sh diff --git a/.github/workflows/docker-ci.yml b/.github/workflows/docker-ci.yml new file mode 100644 index 0000000..d08601d --- /dev/null +++ b/.github/workflows/docker-ci.yml @@ -0,0 +1,100 @@ +name: Docker CI + +on: + push: + branches: [master] + paths: + - "Dockerfile" + - ".dockerignore" + - ".github/workflows/docker-ci.yml" + - "**.go" + - "go.mod" + - "go.sum" + pull_request: + branches: [master] + paths: + - "Dockerfile" + - ".dockerignore" + - ".github/workflows/docker-ci.yml" + - "**.go" + - "go.mod" + - "go.sum" + +permissions: + contents: read + security-events: write + +jobs: + hadolint: + name: Dockerfile Lint + runs-on: ubuntu-latest + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Run Hadolint + uses: hadolint/hadolint-action@v3.1.0 + with: + dockerfile: Dockerfile + failure-threshold: warning + + build-and-scan: + name: Build and Scan + runs-on: ubuntu-latest + needs: hadolint + steps: + - name: Checkout code + uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 + + - name: Extract metadata + id: meta + run: | + echo "version=$(git describe --tags --always --dirty)" >> $GITHUB_OUTPUT + echo "commit=$(git rev-parse --short HEAD)" >> $GITHUB_OUTPUT + echo "date=$(date -u +%Y-%m-%dT%H:%M:%SZ)" >> $GITHUB_OUTPUT + + - name: Build image for scanning + uses: docker/build-push-action@v6 + with: + context: . + push: false + load: true + tags: blobber:ci-${{ github.sha }} + build-args: | + VERSION=${{ steps.meta.outputs.version }} + COMMIT=${{ steps.meta.outputs.commit }} + DATE=${{ steps.meta.outputs.date }} + cache-from: type=gha + cache-to: type=gha,mode=max + + - name: Run Trivy vulnerability scanner + uses: aquasecurity/trivy-action@0.28.0 + with: + image-ref: blobber:ci-${{ github.sha }} + format: sarif + output: trivy-results.sarif + severity: CRITICAL,HIGH,MEDIUM + + - name: Upload Trivy scan results to GitHub Security + uses: github/codeql-action/upload-sarif@v3 + if: always() + with: + sarif_file: trivy-results.sarif + + - name: Fail on critical vulnerabilities + uses: aquasecurity/trivy-action@0.28.0 + with: + image-ref: blobber:ci-${{ github.sha }} + format: table + exit-code: "1" + severity: CRITICAL,HIGH + + - name: Test image + run: | + docker run --rm blobber:ci-${{ github.sha }} version + docker run --rm blobber:ci-${{ github.sha }} --help diff --git a/.github/workflows/docker-release.yml b/.github/workflows/docker-release.yml new file mode 100644 index 0000000..cd78121 --- /dev/null +++ b/.github/workflows/docker-release.yml @@ -0,0 +1,214 @@ +name: Docker Release + +on: + push: + tags: + - "v*" + workflow_dispatch: + inputs: + tag: + description: "Tag to release (e.g., v1.0.0)" + required: true + type: string + +permissions: + contents: read + packages: write + id-token: write # Required for keyless Cosign signing + +env: + GHCR_IMAGE: ghcr.io/meigma/blobber + DOCKERHUB_IMAGE: meigma/blobber + +jobs: + build-and-push: + name: Build and Push + runs-on: ubuntu-latest + outputs: + digest: ${{ steps.build.outputs.digest }} + version: ${{ steps.version.outputs.version }} + steps: + - name: Checkout code + uses: actions/checkout@v4 + with: + fetch-depth: 0 + ref: ${{ inputs.tag || github.ref }} + + - name: Set up QEMU + uses: docker/setup-qemu-action@v3 + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 + + - name: Login to GitHub Container Registry + uses: docker/login-action@v3 + with: + registry: ghcr.io + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + + - name: Login to Docker Hub + uses: docker/login-action@v3 + with: + username: ${{ secrets.DOCKERHUB_USERNAME }} + password: ${{ secrets.DOCKERHUB_TOKEN }} + + - name: Determine tag + id: tag + run: | + if [ -n "${{ inputs.tag }}" ]; then + echo "value=${{ inputs.tag }}" >> $GITHUB_OUTPUT + else + echo "value=${GITHUB_REF#refs/tags/}" >> $GITHUB_OUTPUT + fi + + - name: Extract metadata + id: meta + uses: docker/metadata-action@v5 + with: + images: | + ${{ env.GHCR_IMAGE }} + ${{ env.DOCKERHUB_IMAGE }} + tags: | + type=semver,pattern={{version}},value=${{ steps.tag.outputs.value }} + type=semver,pattern={{major}}.{{minor}},value=${{ steps.tag.outputs.value }} + type=semver,pattern={{major}},value=${{ steps.tag.outputs.value }},enable=${{ !startsWith(steps.tag.outputs.value, 'v0.') }} + type=sha,prefix= + type=raw,value=latest + + - name: Extract version info + id: version + run: | + if [ -n "${{ inputs.tag }}" ]; then + VERSION="${{ inputs.tag }}" + else + VERSION="${GITHUB_REF#refs/tags/}" + fi + VERSION="${VERSION#v}" + echo "version=${VERSION}" >> $GITHUB_OUTPUT + echo "commit=$(git rev-parse --short HEAD)" >> $GITHUB_OUTPUT + echo "date=$(date -u +%Y-%m-%dT%H:%M:%SZ)" >> $GITHUB_OUTPUT + + - name: Build and push + id: build + uses: docker/build-push-action@v6 + with: + context: . + platforms: linux/amd64,linux/arm64 + push: true + tags: ${{ steps.meta.outputs.tags }} + labels: ${{ steps.meta.outputs.labels }} + build-args: | + VERSION=${{ steps.version.outputs.version }} + COMMIT=${{ steps.version.outputs.commit }} + DATE=${{ steps.version.outputs.date }} + cache-from: type=gha + cache-to: type=gha,mode=max + provenance: true + sbom: true + + sign-and-attest: + name: Sign and Attest + runs-on: ubuntu-latest + needs: build-and-push + steps: + - name: Login to GitHub Container Registry + uses: docker/login-action@v3 + with: + registry: ghcr.io + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + + - name: Login to Docker Hub + uses: docker/login-action@v3 + with: + username: ${{ secrets.DOCKERHUB_USERNAME }} + password: ${{ secrets.DOCKERHUB_TOKEN }} + + - name: Install Cosign + uses: sigstore/cosign-installer@v3.7.0 + + - name: Install Syft + uses: anchore/sbom-action/download-syft@v0.17.8 + + - name: Sign GHCR image with Cosign (keyless) + env: + DIGEST: ${{ needs.build-and-push.outputs.digest }} + run: | + cosign sign --yes "${GHCR_IMAGE}@${DIGEST}" + + - name: Sign Docker Hub image with Cosign (keyless) + env: + DIGEST: ${{ needs.build-and-push.outputs.digest }} + run: | + cosign sign --yes "${DOCKERHUB_IMAGE}@${DIGEST}" + + - name: Generate SBOM for GHCR + env: + DIGEST: ${{ needs.build-and-push.outputs.digest }} + run: | + syft "${GHCR_IMAGE}@${DIGEST}" -o spdx-json > sbom-ghcr.spdx.json + + - name: Generate SBOM for Docker Hub + env: + DIGEST: ${{ needs.build-and-push.outputs.digest }} + run: | + syft "${DOCKERHUB_IMAGE}@${DIGEST}" -o spdx-json > sbom-dockerhub.spdx.json + + - name: Attach SBOM to GHCR image + env: + DIGEST: ${{ needs.build-and-push.outputs.digest }} + run: | + cosign attest --yes --predicate sbom-ghcr.spdx.json --type spdx "${GHCR_IMAGE}@${DIGEST}" + + - name: Attach SBOM to Docker Hub image + env: + DIGEST: ${{ needs.build-and-push.outputs.digest }} + run: | + cosign attest --yes --predicate sbom-dockerhub.spdx.json --type spdx "${DOCKERHUB_IMAGE}@${DIGEST}" + + verify: + name: Verify Signatures + runs-on: ubuntu-latest + needs: [build-and-push, sign-and-attest] + steps: + - name: Install Cosign + uses: sigstore/cosign-installer@v3.7.0 + + - name: Verify GHCR signature + env: + DIGEST: ${{ needs.build-and-push.outputs.digest }} + run: | + cosign verify \ + --certificate-identity-regexp="https://github.com/meigma/blobber" \ + --certificate-oidc-issuer="https://token.actions.githubusercontent.com" \ + "${GHCR_IMAGE}@${DIGEST}" + + - name: Verify Docker Hub signature + env: + DIGEST: ${{ needs.build-and-push.outputs.digest }} + run: | + cosign verify \ + --certificate-identity-regexp="https://github.com/meigma/blobber" \ + --certificate-oidc-issuer="https://token.actions.githubusercontent.com" \ + "${DOCKERHUB_IMAGE}@${DIGEST}" + + - name: Verify GHCR SBOM attestation + env: + DIGEST: ${{ needs.build-and-push.outputs.digest }} + run: | + cosign verify-attestation \ + --type spdx \ + --certificate-identity-regexp="https://github.com/meigma/blobber" \ + --certificate-oidc-issuer="https://token.actions.githubusercontent.com" \ + "${GHCR_IMAGE}@${DIGEST}" + + - name: Verify Docker Hub SBOM attestation + env: + DIGEST: ${{ needs.build-and-push.outputs.digest }} + run: | + cosign verify-attestation \ + --type spdx \ + --certificate-identity-regexp="https://github.com/meigma/blobber" \ + --certificate-oidc-issuer="https://token.actions.githubusercontent.com" \ + "${DOCKERHUB_IMAGE}@${DIGEST}" diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..a9940fb --- /dev/null +++ b/Dockerfile @@ -0,0 +1,61 @@ +# syntax=docker/dockerfile:1 + +# ============================================================================= +# Stage 1: Builder +# ============================================================================= +FROM golang:1.25-alpine AS builder + +# Install git for version info and ca-certificates for HTTPS +# hadolint ignore=DL3018 +RUN apk add --no-cache git ca-certificates + +WORKDIR /build + +# Copy dependency files first for better layer caching +COPY go.mod go.sum ./ +COPY sigstore/go.mod sigstore/go.sum ./sigstore/ +RUN go mod download + +# Copy source code +COPY . . + +# Build arguments for version information +ARG VERSION=dev +ARG COMMIT=unknown +ARG DATE=unknown + +# Build the binary with security flags +RUN CGO_ENABLED=0 GOOS=linux go build \ + -ldflags="-s -w \ + -X github.com/meigma/blobber/cmd/blobber/cli.version=${VERSION} \ + -X github.com/meigma/blobber/cmd/blobber/cli.commit=${COMMIT} \ + -X github.com/meigma/blobber/cmd/blobber/cli.date=${DATE}" \ + -trimpath \ + -o /blobber \ + ./cmd/blobber + +# ============================================================================= +# Stage 2: Runtime +# ============================================================================= +FROM dhi.io/debian-base:trixie + +# OCI Image Labels +# https://github.com/opencontainers/image-spec/blob/main/annotations.md +LABEL org.opencontainers.image.title="blobber" \ + org.opencontainers.image.description="Push and pull files to OCI registries" \ + org.opencontainers.image.url="https://github.com/meigma/blobber" \ + org.opencontainers.image.source="https://github.com/meigma/blobber" \ + org.opencontainers.image.documentation="https://blobber.meigma.dev" \ + org.opencontainers.image.licenses="MIT" \ + org.opencontainers.image.vendor="Meigma" + +# Copy CA certificates for HTTPS registry connections +COPY --from=builder /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/ + +# Copy the binary +COPY --from=builder /blobber /usr/local/bin/blobber + +# Use non-root user (65534 is standard 'nobody' user) +USER 65534:65534 + +ENTRYPOINT ["/usr/local/bin/blobber"] From 4f5d1dcb86587c9fc7fe3d7f1523b5b7573bfbd1 Mon Sep 17 00:00:00 2001 From: Joshua Gilman Date: Fri, 9 Jan 2026 18:54:35 -0800 Subject: [PATCH 2/6] fix(docker): add Docker Hub login for DHI base image DHI (Docker Hardened Images) require authentication to pull, even though they're free. Co-Authored-By: Claude Opus 4.5 --- .github/workflows/docker-ci.yml | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/.github/workflows/docker-ci.yml b/.github/workflows/docker-ci.yml index d08601d..e93e257 100644 --- a/.github/workflows/docker-ci.yml +++ b/.github/workflows/docker-ci.yml @@ -51,6 +51,12 @@ jobs: - name: Set up Docker Buildx uses: docker/setup-buildx-action@v3 + - name: Login to Docker Hub + uses: docker/login-action@v3 + with: + username: ${{ secrets.DOCKERHUB_USERNAME }} + password: ${{ secrets.DOCKERHUB_TOKEN }} + - name: Extract metadata id: meta run: | From 1f8117358a0910b18cfb93cf1a27abaeda0309c1 Mon Sep 17 00:00:00 2001 From: Joshua Gilman Date: Fri, 9 Jan 2026 18:58:09 -0800 Subject: [PATCH 3/6] fix(docker): use debian:trixie-slim base image DHI images (dhi.io) require special registry authentication that's separate from Docker Hub. Switch to the official debian:trixie-slim image which is publicly available. Co-Authored-By: Claude Opus 4.5 --- Dockerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Dockerfile b/Dockerfile index a9940fb..a8983c5 100644 --- a/Dockerfile +++ b/Dockerfile @@ -37,7 +37,7 @@ RUN CGO_ENABLED=0 GOOS=linux go build \ # ============================================================================= # Stage 2: Runtime # ============================================================================= -FROM dhi.io/debian-base:trixie +FROM debian:trixie-slim # OCI Image Labels # https://github.com/opencontainers/image-spec/blob/main/annotations.md From fb997bf03caf1c84eaaf70b464d77e02dd23f221 Mon Sep 17 00:00:00 2001 From: Joshua Gilman Date: Fri, 9 Jan 2026 19:02:06 -0800 Subject: [PATCH 4/6] fix(docker): use distroless base image Switch from debian:trixie-slim (which has CVEs) to gcr.io/distroless/static-debian12:nonroot which: - Has virtually no CVEs (minimal attack surface) - Is perfect for static Go binaries - Already runs as non-root (uid 65532) - Includes CA certificates Co-Authored-By: Claude Opus 4.5 --- .github/workflows/docker-ci.yml | 6 ------ Dockerfile | 11 +++-------- 2 files changed, 3 insertions(+), 14 deletions(-) diff --git a/.github/workflows/docker-ci.yml b/.github/workflows/docker-ci.yml index e93e257..d08601d 100644 --- a/.github/workflows/docker-ci.yml +++ b/.github/workflows/docker-ci.yml @@ -51,12 +51,6 @@ jobs: - name: Set up Docker Buildx uses: docker/setup-buildx-action@v3 - - name: Login to Docker Hub - uses: docker/login-action@v3 - with: - username: ${{ secrets.DOCKERHUB_USERNAME }} - password: ${{ secrets.DOCKERHUB_TOKEN }} - - name: Extract metadata id: meta run: | diff --git a/Dockerfile b/Dockerfile index a8983c5..9172a9f 100644 --- a/Dockerfile +++ b/Dockerfile @@ -37,7 +37,7 @@ RUN CGO_ENABLED=0 GOOS=linux go build \ # ============================================================================= # Stage 2: Runtime # ============================================================================= -FROM debian:trixie-slim +FROM gcr.io/distroless/static-debian12:nonroot # OCI Image Labels # https://github.com/opencontainers/image-spec/blob/main/annotations.md @@ -49,13 +49,8 @@ LABEL org.opencontainers.image.title="blobber" \ org.opencontainers.image.licenses="MIT" \ org.opencontainers.image.vendor="Meigma" -# Copy CA certificates for HTTPS registry connections -COPY --from=builder /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/ - -# Copy the binary +# Copy the binary (distroless already includes CA certificates) COPY --from=builder /blobber /usr/local/bin/blobber -# Use non-root user (65534 is standard 'nobody' user) -USER 65534:65534 - +# nonroot tag already runs as non-root user (65532) ENTRYPOINT ["/usr/local/bin/blobber"] From 8059ce47d11a818c4ec30350b56e7c054fef248a Mon Sep 17 00:00:00 2001 From: Joshua Gilman Date: Fri, 9 Jan 2026 20:03:03 -0800 Subject: [PATCH 5/6] refactor(docker): replace Cosign/Syft with GitHub attestations Replace manual Cosign signing and Syft SBOM generation with GitHub's native `actions/attest-build-provenance` action. Changes: - Add `attestations: write` permission - Use `index.docker.io` prefix for Docker Hub (required for attestations) - Remove `sign-and-attest` job (3 separate jobs -> 1 job) - Remove `verify` job (use `gh attestation verify` instead) - Remove BuildKit provenance/sbom (redundant with GitHub attestations) Verification command: gh attestation verify oci://ghcr.io/meigma/blobber:latest -R meigma/blobber Co-Authored-By: Claude Opus 4.5 --- .github/workflows/docker-release.yml | 124 ++++----------------------- 1 file changed, 15 insertions(+), 109 deletions(-) diff --git a/.github/workflows/docker-release.yml b/.github/workflows/docker-release.yml index cd78121..a894652 100644 --- a/.github/workflows/docker-release.yml +++ b/.github/workflows/docker-release.yml @@ -14,19 +14,17 @@ on: permissions: contents: read packages: write - id-token: write # Required for keyless Cosign signing + id-token: write + attestations: write env: GHCR_IMAGE: ghcr.io/meigma/blobber - DOCKERHUB_IMAGE: meigma/blobber + DOCKERHUB_IMAGE: index.docker.io/meigma/blobber jobs: build-and-push: name: Build and Push runs-on: ubuntu-latest - outputs: - digest: ${{ steps.build.outputs.digest }} - version: ${{ steps.version.outputs.version }} steps: - name: Checkout code uses: actions/checkout@v4 @@ -104,111 +102,19 @@ jobs: DATE=${{ steps.version.outputs.date }} cache-from: type=gha cache-to: type=gha,mode=max - provenance: true - sbom: true - sign-and-attest: - name: Sign and Attest - runs-on: ubuntu-latest - needs: build-and-push - steps: - - name: Login to GitHub Container Registry - uses: docker/login-action@v3 + # Attestations for GHCR + - name: Attest GHCR build provenance + uses: actions/attest-build-provenance@v2 with: - registry: ghcr.io - username: ${{ github.actor }} - password: ${{ secrets.GITHUB_TOKEN }} + subject-name: ${{ env.GHCR_IMAGE }} + subject-digest: ${{ steps.build.outputs.digest }} + push-to-registry: true - - name: Login to Docker Hub - uses: docker/login-action@v3 + # Attestations for Docker Hub + - name: Attest Docker Hub build provenance + uses: actions/attest-build-provenance@v2 with: - username: ${{ secrets.DOCKERHUB_USERNAME }} - password: ${{ secrets.DOCKERHUB_TOKEN }} - - - name: Install Cosign - uses: sigstore/cosign-installer@v3.7.0 - - - name: Install Syft - uses: anchore/sbom-action/download-syft@v0.17.8 - - - name: Sign GHCR image with Cosign (keyless) - env: - DIGEST: ${{ needs.build-and-push.outputs.digest }} - run: | - cosign sign --yes "${GHCR_IMAGE}@${DIGEST}" - - - name: Sign Docker Hub image with Cosign (keyless) - env: - DIGEST: ${{ needs.build-and-push.outputs.digest }} - run: | - cosign sign --yes "${DOCKERHUB_IMAGE}@${DIGEST}" - - - name: Generate SBOM for GHCR - env: - DIGEST: ${{ needs.build-and-push.outputs.digest }} - run: | - syft "${GHCR_IMAGE}@${DIGEST}" -o spdx-json > sbom-ghcr.spdx.json - - - name: Generate SBOM for Docker Hub - env: - DIGEST: ${{ needs.build-and-push.outputs.digest }} - run: | - syft "${DOCKERHUB_IMAGE}@${DIGEST}" -o spdx-json > sbom-dockerhub.spdx.json - - - name: Attach SBOM to GHCR image - env: - DIGEST: ${{ needs.build-and-push.outputs.digest }} - run: | - cosign attest --yes --predicate sbom-ghcr.spdx.json --type spdx "${GHCR_IMAGE}@${DIGEST}" - - - name: Attach SBOM to Docker Hub image - env: - DIGEST: ${{ needs.build-and-push.outputs.digest }} - run: | - cosign attest --yes --predicate sbom-dockerhub.spdx.json --type spdx "${DOCKERHUB_IMAGE}@${DIGEST}" - - verify: - name: Verify Signatures - runs-on: ubuntu-latest - needs: [build-and-push, sign-and-attest] - steps: - - name: Install Cosign - uses: sigstore/cosign-installer@v3.7.0 - - - name: Verify GHCR signature - env: - DIGEST: ${{ needs.build-and-push.outputs.digest }} - run: | - cosign verify \ - --certificate-identity-regexp="https://github.com/meigma/blobber" \ - --certificate-oidc-issuer="https://token.actions.githubusercontent.com" \ - "${GHCR_IMAGE}@${DIGEST}" - - - name: Verify Docker Hub signature - env: - DIGEST: ${{ needs.build-and-push.outputs.digest }} - run: | - cosign verify \ - --certificate-identity-regexp="https://github.com/meigma/blobber" \ - --certificate-oidc-issuer="https://token.actions.githubusercontent.com" \ - "${DOCKERHUB_IMAGE}@${DIGEST}" - - - name: Verify GHCR SBOM attestation - env: - DIGEST: ${{ needs.build-and-push.outputs.digest }} - run: | - cosign verify-attestation \ - --type spdx \ - --certificate-identity-regexp="https://github.com/meigma/blobber" \ - --certificate-oidc-issuer="https://token.actions.githubusercontent.com" \ - "${GHCR_IMAGE}@${DIGEST}" - - - name: Verify Docker Hub SBOM attestation - env: - DIGEST: ${{ needs.build-and-push.outputs.digest }} - run: | - cosign verify-attestation \ - --type spdx \ - --certificate-identity-regexp="https://github.com/meigma/blobber" \ - --certificate-oidc-issuer="https://token.actions.githubusercontent.com" \ - "${DOCKERHUB_IMAGE}@${DIGEST}" + subject-name: ${{ env.DOCKERHUB_IMAGE }} + subject-digest: ${{ steps.build.outputs.digest }} + push-to-registry: true From b1e0eeb282aa3c1e37d05f251b9b2e867bbfe462 Mon Sep 17 00:00:00 2001 From: Joshua Gilman Date: Fri, 9 Jan 2026 20:09:32 -0800 Subject: [PATCH 6/6] feat(docker): add SBOM generation and attestation Add SBOM generation using anchore/sbom-action and attestation using actions/attest-sbom for both GHCR and Docker Hub images. - Generate SBOM with Syft via anchore/sbom-action - Attest SBOM to both registries with actions/attest-sbom - Disable artifact upload (attestation is the primary distribution) Verification: gh attestation verify oci://ghcr.io/meigma/blobber:latest -R meigma/blobber \ --predicate-type https://spdx.dev/Document Co-Authored-By: Claude Opus 4.5 --- .github/workflows/docker-release.yml | 27 +++++++++++++++++++++++++-- 1 file changed, 25 insertions(+), 2 deletions(-) diff --git a/.github/workflows/docker-release.yml b/.github/workflows/docker-release.yml index a894652..cbdc307 100644 --- a/.github/workflows/docker-release.yml +++ b/.github/workflows/docker-release.yml @@ -103,7 +103,7 @@ jobs: cache-from: type=gha cache-to: type=gha,mode=max - # Attestations for GHCR + # Build provenance attestations - name: Attest GHCR build provenance uses: actions/attest-build-provenance@v2 with: @@ -111,10 +111,33 @@ jobs: subject-digest: ${{ steps.build.outputs.digest }} push-to-registry: true - # Attestations for Docker Hub - name: Attest Docker Hub build provenance uses: actions/attest-build-provenance@v2 with: subject-name: ${{ env.DOCKERHUB_IMAGE }} subject-digest: ${{ steps.build.outputs.digest }} push-to-registry: true + + # SBOM generation and attestation + - name: Generate SBOM + uses: anchore/sbom-action@v0 + with: + image: ${{ env.GHCR_IMAGE }}@${{ steps.build.outputs.digest }} + output-file: sbom.spdx.json + upload-artifact: false + + - name: Attest GHCR SBOM + uses: actions/attest-sbom@v2 + with: + subject-name: ${{ env.GHCR_IMAGE }} + subject-digest: ${{ steps.build.outputs.digest }} + sbom-path: sbom.spdx.json + push-to-registry: true + + - name: Attest Docker Hub SBOM + uses: actions/attest-sbom@v2 + with: + subject-name: ${{ env.DOCKERHUB_IMAGE }} + subject-digest: ${{ steps.build.outputs.digest }} + sbom-path: sbom.spdx.json + push-to-registry: true