fix(ci): scan multi-arch images per-arch by digest, pin trivy-action (#476)

Resolves the intermittent "no child with platform linux/amd64 in
index" failure on the merge-backend job — and fixes the same latent
bug on merge-frontend before it surfaces.

Two compounding root causes per Luca's diagnosis:

1. aquasecurity/trivy-action@master was unpinned, so the action and
   its bundled Trivy binary float on every CI run. A green build
   could flip red overnight without a single repo change.
2. Trivy was asked to scan a multi-platform OCI index by tag (the
   merge-* jobs ran AFTER manifest creation). Its remote resolver
   cannot reliably pick the right per-arch child out of an index
   reference — it needs a single-platform reference (digest, or a
   --platform flag).

Fix:

- Move the Trivy + upload-sarif steps OUT of merge-backend /
  merge-frontend and INTO the per-arch build-backend / build-frontend
  matrix jobs. Each leg scans the image it just pushed by its
  sha256 digest (`...@${{ steps.build.outputs.digest }}`), which is
  always single-platform by construction.
- Pin aquasecurity/trivy-action@0.28.0 (was @master).
- Distinct SARIF category per arch
  (`backend-vulnerabilities-linux-amd64`, …-arm64) so an
  amd64-only finding in a base layer doesn't get masked by the
  arm64 scan in the Security tab.
- Move security-events: write down to the build-* jobs (where the
  scan now runs) and remove it from the merge-* jobs (which only
  publish the manifest now).

Out of scope: flipping `exit-code: '1'` to actually gate CI on
findings. Worth doing as a separate follow-up after an audit pass —
landing it here would surprise beta with a red build for any
pre-existing CRITICAL/HIGH in current images. Inline TODO in the
workflow notes the deferral.
This commit is contained in:
Paul Nothaft
2026-05-13 17:56:29 +02:00
parent 16e4d191c2
commit caf0d61857
+65 -36
View File
@@ -69,6 +69,11 @@ jobs:
permissions:
contents: read
packages: write
# Trivy uploads its SARIF to the Security tab from this job — see
# the "Run Trivy" step below. Scanning per-arch by digest (#476)
# is reliable; scanning the multi-arch index by tag from the
# merge-* job was not.
security-events: write
steps:
- name: Checkout code
@@ -153,13 +158,47 @@ jobs:
if-no-files-found: error
retention-days: 1
# Per-arch vulnerability scan (#476). Scanning the multi-arch
# manifest from the merge-* job by tag is unreliable — Trivy's
# remote resolver crashes intermittently with "no child with
# platform linux/amd64 in index". The fix is to scan each leg
# by its single-platform digest right here, where it just landed
# in GHCR. Tag pinned (was @master) so the action + bundled
# Trivy binary don't float between runs.
#
# exit-code is left unset (=0) for now: Trivy reports findings
# to the Security tab but doesn't fail the build. Flipping that
# to '1' to actually gate CI is a deliberate follow-up — needs an
# audit pass first so the next beta build doesn't surprise red.
- name: Run Trivy vulnerability scanner (per-arch, by digest)
if: steps.push-decision.outputs.push == 'true'
uses: aquasecurity/trivy-action@0.28.0
with:
image-ref: ${{ env.REGISTRY }}/${{ env.BACKEND_IMAGE_NAME }}@${{ steps.build.outputs.digest }}
format: 'sarif'
output: 'trivy-backend-${{ env.PLATFORM_PAIR }}.sarif'
severity: 'CRITICAL,HIGH'
timeout: '10m'
- name: Upload Trivy scan results to GitHub Security tab
if: steps.push-decision.outputs.push == 'true'
uses: github/codeql-action/upload-sarif@v4
with:
sarif_file: 'trivy-backend-${{ env.PLATFORM_PAIR }}.sarif'
# Distinct category per arch so the Security tab surfaces
# per-platform findings independently — an amd64-only CVE in
# a base layer doesn't get masked by the arm64 scan.
category: 'backend-vulnerabilities-${{ env.PLATFORM_PAIR }}'
merge-backend:
needs: build-backend
runs-on: ubuntu-latest
# No security-events permission here — vulnerability scanning moved
# to per-arch build-backend jobs (#476). This job's only job is to
# combine the per-arch digests into a multi-arch manifest.
permissions:
contents: read
packages: write
security-events: write
# Only run when at least one digest was pushed (i.e. not on PRs without push intent).
if: github.event_name != 'pull_request' || github.event.inputs.push == 'true'
@@ -231,23 +270,6 @@ jobs:
run: |
docker buildx imagetools inspect ${{ env.REGISTRY }}/${{ env.BACKEND_IMAGE_NAME }}:${{ steps.meta-backend.outputs.version }}
- name: Run Trivy vulnerability scanner
if: github.event_name != 'pull_request' && steps.login-ghcr.outcome == 'success'
uses: aquasecurity/trivy-action@master
with:
image-ref: ${{ env.REGISTRY }}/${{ env.BACKEND_IMAGE_NAME }}:${{ steps.meta-backend.outputs.version }}
format: 'sarif'
output: 'trivy-backend.sarif'
severity: 'CRITICAL,HIGH'
timeout: '10m'
- name: Upload Trivy scan results to GitHub Security tab
if: github.event_name != 'pull_request' && steps.login-ghcr.outcome == 'success'
uses: github/codeql-action/upload-sarif@v4
with:
sarif_file: 'trivy-backend.sarif'
category: 'backend-vulnerabilities'
# -----------------------------------------------------------------------------
# Frontend: per-arch build, then merge into a multi-arch manifest
# -----------------------------------------------------------------------------
@@ -264,6 +286,9 @@ jobs:
permissions:
contents: read
packages: write
# See build-backend for the rationale (#476). Same pattern: per-arch
# vulnerability scan by digest, SARIF uploaded to the Security tab.
security-events: write
steps:
- name: Checkout code
@@ -348,13 +373,34 @@ jobs:
if-no-files-found: error
retention-days: 1
# Per-arch vulnerability scan (#476). See build-backend for the
# full rationale; identical pattern here, only the image-ref +
# SARIF filename + category change.
- name: Run Trivy vulnerability scanner (per-arch, by digest)
if: steps.push-decision.outputs.push == 'true'
uses: aquasecurity/trivy-action@0.28.0
with:
image-ref: ${{ env.REGISTRY }}/${{ env.FRONTEND_IMAGE_NAME }}@${{ steps.build.outputs.digest }}
format: 'sarif'
output: 'trivy-frontend-${{ env.PLATFORM_PAIR }}.sarif'
severity: 'CRITICAL,HIGH'
timeout: '10m'
- name: Upload Trivy scan results to GitHub Security tab
if: steps.push-decision.outputs.push == 'true'
uses: github/codeql-action/upload-sarif@v4
with:
sarif_file: 'trivy-frontend-${{ env.PLATFORM_PAIR }}.sarif'
category: 'frontend-vulnerabilities-${{ env.PLATFORM_PAIR }}'
merge-frontend:
needs: build-frontend
runs-on: ubuntu-latest
# See merge-backend — vulnerability scanning moved to the per-arch
# build-frontend matrix (#476). This job only publishes the manifest.
permissions:
contents: read
packages: write
security-events: write
if: github.event_name != 'pull_request' || github.event.inputs.push == 'true'
steps:
@@ -425,23 +471,6 @@ jobs:
run: |
docker buildx imagetools inspect ${{ env.REGISTRY }}/${{ env.FRONTEND_IMAGE_NAME }}:${{ steps.meta-frontend.outputs.version }}
- name: Run Trivy vulnerability scanner
if: github.event_name != 'pull_request' && steps.login-ghcr.outcome == 'success'
uses: aquasecurity/trivy-action@master
with:
image-ref: ${{ env.REGISTRY }}/${{ env.FRONTEND_IMAGE_NAME }}:${{ steps.meta-frontend.outputs.version }}
format: 'sarif'
output: 'trivy-frontend.sarif'
severity: 'CRITICAL,HIGH'
timeout: '10m'
- name: Upload Trivy scan results to GitHub Security tab
if: github.event_name != 'pull_request' && steps.login-ghcr.outcome == 'success'
uses: github/codeql-action/upload-sarif@v4
with:
sarif_file: 'trivy-frontend.sarif'
category: 'frontend-vulnerabilities'
summary:
needs: [build-backend, merge-backend, build-frontend, merge-frontend]
if: always()