From 0bc6b5f69c0c15d20412fbdac6028cb7d8f0895f Mon Sep 17 00:00:00 2001 From: Nelson Spence Date: Sat, 18 Jul 2026 12:46:48 -0500 Subject: [PATCH] =?UTF-8?q?ci:=20harden=20release=20workflow=20=E2=80=94?= =?UTF-8?q?=20gate=20on=20vulns,=20enforce=20egress?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Close the two substantive gaps from the hardening review: - Trivy no longer runs after 'push: true' (which published before scanning). Restructured to build-and-load → gate scan → push: a fixable CRITICAL now fails the job BEFORE the image is published. Full CRITICAL+HIGH results still go to the Security tab via SARIF (non-blocking, since CUDA base layers carry unactionable HIGH advisories); ignore-unfixed keeps the gate actionable. - harden-runner egress-policy audit → block with an allow-list derived from the workflow's known egress (GitHub/GHCR, PyPI, PyTorch, NVIDIA NGC, HF model download, Sigstore, Trivy DB). Observability → enforcement. Plus: checkout persist-credentials: false; attest the CycloneDX SBOM to the registry (verifiable, matching the build provenance). All actions remain SHA-pinned; actionlint clean. Co-Authored-By: Claude Fable 5 --- .github/workflows/release.yml | 115 ++++++++++++++++++++++++++++------ 1 file changed, 96 insertions(+), 19 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index d619842a..6ff5ee75 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -1,7 +1,7 @@ # Fieldnote-Echo/Kokoro-FastAPI — Hardened GHCR Release # Replaces upstream's multi-arch bake workflow with a GPU-only, amd64-only -# build that includes supply-chain hardening (pinned actions, Sigstore -# attestation, Trivy scan, CycloneDX SBOM). +# build that includes supply-chain hardening (pinned actions, egress +# enforcement, Sigstore attestation, gating Trivy scan, attested SBOM). # # Incorporates from upstream's reworked release workflow: concurrency group, # duplicate-release guard, ubuntu-24.04 runner, and the new multi-stage @@ -22,7 +22,7 @@ permissions: packages: write # GHCR push id-token: write # Sigstore OIDC security-events: write # SARIF upload - attestations: write # Build provenance + attestations: write # Build provenance + SBOM attestation concurrency: group: release-${{ github.ref_name }} @@ -33,16 +33,48 @@ jobs: runs-on: ubuntu-24.04 env: IMAGE: ghcr.io/fieldnote-echo/kokoro-fastapi-gpu + SCAN_TAG: ghcr.io/fieldnote-echo/kokoro-fastapi-gpu:ci-scan # local-only, never pushed steps: # ── Runner hardening ────────────────────────────────────────────── + # egress-policy: block — the runner may only reach the endpoints below. + # Derived from this workflow's known egress (GitHub/GHCR, PyPI, PyTorch, + # NVIDIA NGC, Hugging Face model download, Sigstore, Trivy DB). If a + # release fails with a "Blocked" egress line, add the reported host here; + # harden-runner fails closed and names the exact host. - name: Harden runner uses: step-security/harden-runner@5ef0c079ce82195b2a36a210272d6b661572d83e # v2.14.2 with: - egress-policy: audit + egress-policy: block + allowed-endpoints: > + github.com:443 + api.github.com:443 + objects.githubusercontent.com:443 + codeload.github.com:443 + release-assets.githubusercontent.com:443 + ghcr.io:443 + pkg-containers.githubusercontent.com:443 + actions-results-receiver-production.githubapp.com:443 + productionresultssa0.blob.core.windows.net:443 + pypi.org:443 + files.pythonhosted.org:443 + download.pytorch.org:443 + nvcr.io:443 + huggingface.co:443 + cdn-lfs.huggingface.co:443 + cdn-lfs-us-1.hf.co:443 + fulcio.sigstore.dev:443 + rekor.sigstore.dev:443 + tuf-repo-cdn.sigstore.dev:443 + mirror.gcr.io:443 + registry-1.docker.io:443 + auth.docker.io:443 + production.cloudflare.docker.com:443 # ── Checkout ────────────────────────────────────────────────────── - name: Checkout uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + with: + persist-credentials: false # don't leave the token in .git/config for later steps # ── Guard: don't re-publish an existing release ─────────────────── - name: Check release does not exist @@ -84,10 +116,60 @@ jobs: type=semver,pattern={{major}}.{{minor}} type=sha,prefix= - # ── Build & push ────────────────────────────────────────────────── + # ── Build for scanning (load locally, DO NOT push yet) ──────────── # CUDA base images are pinned by digest, mirroring docker-bake.hcl's # gpu-amd64 target (CUDA 12.6.3). Keep these in sync with the bake file; # refresh with: docker buildx imagetools inspect nvcr.io/nvidia/cuda: + - name: Build image (load for scan) + id: build_scan + uses: docker/build-push-action@10e90e3645eae34f1e60eeb005ba3a3d33f178e8 # v6.19.2 + with: + context: . + file: docker/gpu/Dockerfile.optimized + platforms: linux/amd64 + load: true + push: false + build-args: | + CUDA_VERSION=12.6.3 + CUDA_BUILDER_IMAGE=nvcr.io/nvidia/cuda:12.6.3-cudnn-devel-ubuntu24.04@sha256:50efab398f76258daa91ceebb33b6467e40217c67ea44fb5a2cebc6be7d9cce3 + CUDA_RUNTIME_IMAGE=nvcr.io/nvidia/cuda:12.6.3-cudnn-runtime-ubuntu24.04@sha256:8aef630a54bc5c5146ae5ce68e6af5caa3df0fb690bb91544175c91f307e4356 + tags: ${{ env.SCAN_TAG }} + cache-from: type=gha + cache-to: type=gha,mode=max + + # ── Trivy: GATE the release (fail on fixable CRITICAL) ──────────── + # Runs BEFORE push so a vulnerable image is never published. ignore-unfixed + # avoids blocking on CVEs in the CUDA/OS base that have no available fix; + # gate on CRITICAL only (HIGH is reported below but non-blocking, since + # CUDA base layers routinely carry unactionable HIGH advisories). + - name: Trivy scan (gate) + uses: aquasecurity/trivy-action@c1824fd6edce30d7ab345a9989de00bbd46ef284 # 0.34.0 + with: + image-ref: ${{ env.SCAN_TAG }} + format: table + severity: CRITICAL + ignore-unfixed: true + exit-code: "1" + + # ── Trivy: full report to the Security tab (non-blocking) ───────── + - name: Trivy scan (SARIF report) + if: always() && steps.build_scan.outcome == 'success' + uses: aquasecurity/trivy-action@c1824fd6edce30d7ab345a9989de00bbd46ef284 # 0.34.0 + with: + image-ref: ${{ env.SCAN_TAG }} + format: sarif + output: trivy-results.sarif + severity: CRITICAL,HIGH + + - name: Upload Trivy SARIF + if: always() && hashFiles('trivy-results.sarif') != '' + uses: github/codeql-action/upload-sarif@9e907b5e64f6b83e7804b09294d44122997950d6 # v4.32.3 + with: + sarif_file: trivy-results.sarif + + # ── Push (only reached if the gate scan passed) ─────────────────── + # Reuses the layers built above via the GHA cache, so this is a fast + # cache hit rather than a second full build. - name: Build and push id: build uses: docker/build-push-action@10e90e3645eae34f1e60eeb005ba3a3d33f178e8 # v6.19.2 @@ -113,20 +195,6 @@ jobs: subject-digest: ${{ steps.build.outputs.digest }} push-to-registry: true - # ── Trivy container scan ────────────────────────────────────────── - - name: Trivy vulnerability scan - uses: aquasecurity/trivy-action@c1824fd6edce30d7ab345a9989de00bbd46ef284 # 0.34.0 - with: - image-ref: ${{ env.IMAGE }}@${{ steps.build.outputs.digest }} - format: sarif - output: trivy-results.sarif - severity: CRITICAL,HIGH - - - name: Upload Trivy SARIF - uses: github/codeql-action/upload-sarif@9e907b5e64f6b83e7804b09294d44122997950d6 # v4.32.3 - with: - sarif_file: trivy-results.sarif - # ── CycloneDX SBOM ─────────────────────────────────────────────── - name: Generate SBOM uses: anchore/sbom-action@28d71544de8eaf1b958d335707167c5f783590ad # v0.22.2 @@ -135,6 +203,15 @@ jobs: format: cyclonedx-json output-file: sbom.cdx.json + # ── Attest the SBOM to the registry (verifiable, like provenance) ── + - name: Attest SBOM + uses: actions/attest-sbom@4651f806c01d8637787e274ac3bdf724ef169f34 # v3.0.0 + with: + subject-name: ${{ env.IMAGE }} + subject-digest: ${{ steps.build.outputs.digest }} + sbom-path: sbom.cdx.json + push-to-registry: true + # ── GitHub Release ──────────────────────────────────────────────── - name: Create GitHub Release if: startsWith(github.ref, 'refs/tags/')