diff --git a/.gitea/workflows/release.yaml b/.gitea/workflows/release.yaml index ee2bee5..7b57dd8 100644 --- a/.gitea/workflows/release.yaml +++ b/.gitea/workflows/release.yaml @@ -187,3 +187,37 @@ jobs: set -eu echo "$TOKEN" | helm registry login "$REGISTRY" -u niklas --password-stdin make helm-package helm-push VERSION="$REF_NAME" + + # Host mode, like image and chart: this needs a docker daemon to run trivy in, and a + # `container:` job would sit on the dind bridge with none. + # + # It scans the pushed image rather than a locally built one, because trivy cannot read a + # local image on this runner -- Talos has no docker socket and the dind sidecar shares no + # filesystem with the job -- so it pulls from the registry. That is also why this runs + # after `image` rather than gating it: a red scan does not unpublish anything. + # + # What a red scan means is therefore not "the release failed" but "do not bump the wrapper + # chart in Ryuvia/charts to this version". The image and chart are already published by + # the time this runs, and deliberately so -- this pipeline does not deploy. + # + # riksdata and rd-web have had this since they were set up; terdut-server went without any + # image scanning until 2026-09-02, so every release before v0.9.4 was published with no + # CVE check at all. + scan-image: + needs: image + runs-on: ubuntu-latest + steps: + - name: Checkout + env: + REF_NAME: ${{ github.ref_name }} + run: git clone --depth=1 --branch "$REF_NAME" "$REPO_URL" . + + # Credentials are passed even though these packages are anonymously pullable -- that + # is a property of the personal namespace this publishes to, not something a release + # should depend on staying true. + - name: Scan the pushed image (trivy) + env: + TRIVY_USERNAME: niklas + TRIVY_PASSWORD: ${{ secrets.REGISTRY_TOKEN }} + REF_NAME: ${{ github.ref_name }} + run: make security-image VERSION="$REF_NAME" diff --git a/CLAUDE.md b/CLAUDE.md index 171fd1b..2cc2ba1 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -16,9 +16,11 @@ Config is `.release.conf` here plus `make release-vars`. The process itself live Two things about this repo specifically: -- **The pipeline has no image scan.** `.gitea/workflows/release.yaml` runs `test`, - `binaries`, `image` and `chart`. A green release run is not evidence the image is - CVE-clean, and a release note must not imply it is. +- **The image is scanned after it is published, not before.** `scan-image` runs trivy + against the pushed image, because trivy cannot read a locally built one on this runner. + A red scan therefore unpublishes nothing — it means: do not bump the wrapper chart in + `Ryuvia/charts` to this version. Added 2026-09-02; every release up to and including + v0.9.3 was published with no CVE check at all. - **The wrapper chart has two `tag:` lines** — the app image and the python backup sidecar — so `chart-bump` needs `--image "$IMAGE"` to know which one moves. diff --git a/Makefile b/Makefile index 19b50da..93552aa 100644 --- a/Makefile +++ b/Makefile @@ -100,6 +100,8 @@ CHART_VERSION := $(shell echo "$(VERSION)" | sed 's/^v//') PLATFORMS ?= linux/amd64,linux/arm64 BUILDX_BUILDER ?= terdut +TRIVY_VERSION := 0.73.0 + # --pull, not --no-cache: refresh the base image without discarding the layer cache. DOCKER_BUILD_FLAGS ?= --pull @@ -113,7 +115,7 @@ HELM_ISOLATED = HELM_REPOSITORY_CONFIG=$(CURDIR)/.helm-repos.yaml .PHONY: require-version require-version: @test "$(VERSION)" != "dev" || \ - (echo "refusing to publish VERSION=dev — pass VERSION=vX.Y.Z (the workflow passes the tag)" && exit 1) + (echo "VERSION=dev names no release — pass VERSION=vX.Y.Z (the workflow passes the tag)" && exit 1) .PHONY: build build: ## Build the image for this host only, without pushing (local check / CI smoke) @@ -166,3 +168,29 @@ binaries: require-version ## Cross-compile the release binaries into dist/ .PHONY: release release: push helm-package helm-push ## Publish image + chart (the workflow's one call) + +## --- security --- + +# Scans the pushed image, not a local one: trivy cannot read a locally built image on the +# runner -- Talos has no docker socket, and the dind sidecar shares no filesystem with the +# job -- so it pulls from the registry. Same reason riksdata and rd-web scan after pushing. +# +# It cannot gate a deploy, because this pipeline does not deploy. A red scan means: do not +# bump the wrapper chart in Ryuvia/charts to this version. +# +# The image is FROM scratch, so there are no OS packages to scan and trivy sees exactly one +# target -- the Go binary and its module graph. That also makes scanning a single platform +# sufficient here: linux/amd64 and linux/arm64 are the same modules built for a different +# GOARCH, so a CVE in one is a CVE in both. On an image with a base layer that would not +# hold and both platforms would need scanning. +# +# riksdata and rd-web also run govulncheck and gitleaks in a `security` CI job. This repo +# does not, so this target is the only security scanning it has. +.PHONY: security-image +security-image: require-version ## Scan the pushed image for CVEs (needs VERSION) + @# The named volume persists trivy's vulnerability DB between runs; without it every + @# scan re-downloads the whole database. + docker run --rm -e TRIVY_USERNAME -e TRIVY_PASSWORD \ + -v trivy-cache:/root/.cache/trivy \ + docker.io/aquasec/trivy:$(TRIVY_VERSION) image --severity HIGH,CRITICAL \ + --ignore-unfixed --exit-code 1 $(IMAGE):$(VERSION) diff --git a/README.md b/README.md index a2be216..451e6cc 100644 --- a/README.md +++ b/README.md @@ -709,8 +709,10 @@ by pushing a tag. Two things the release process needs to know about this repo: -- **The pipeline has no image scan**, unlike riksdata and rd-web. A green release run is not - evidence the image is CVE-clean. +- **The image scan runs after publishing**, like riksdata's and rd-web's: trivy cannot read + a locally built image on this runner, so it pulls the pushed one. A red `scan-image` means + do not bump the wrapper chart to that version — it cannot unpublish anything. The image is + `FROM scratch`, so trivy sees exactly one target, the Go binary and its module graph. - **The wrapper chart's `values.yaml` has two `tag:` lines** — the app image and the python backup sidecar — so `chart-bump` is given `--image` to say which one moves.