diff --git a/.gitea/workflows/ci.yaml b/.gitea/workflows/ci.yaml index bda34ec..c575a92 100644 --- a/.gitea/workflows/ci.yaml +++ b/.gitea/workflows/ci.yaml @@ -71,6 +71,37 @@ jobs: - name: Format, vet and test run: make fmt lint test + # Runs on every push and pull request, unlike the image scan, which needs something + # published to scan and so lives in release.yaml. Both are needed: govulncheck reads the + # source and its module graph, trivy reads the built artifact, and neither sees what the + # other does. + security: + runs-on: ubuntu-latest + container: + image: golang:1.26.6-bookworm + volumes: + - go-mod-cache:/go/pkg/mod + - go-build-cache:/root/.cache/go-build + - gobin-cache:/go/bin + steps: + - name: Checkout + env: + REF_NAME: ${{ github.ref_name }} + HEAD_SHA: ${{ github.event.pull_request.head.sha }} + run: | + if [ -n "$HEAD_SHA" ]; then + git clone "$REPO_URL" . + git checkout -q "$HEAD_SHA" + else + git clone --depth=1 --branch "$REF_NAME" "$REPO_URL" . + fi + + - name: Go vulnerability scan (govulncheck) + run: make security-go + + - name: Secret scan (gitleaks) + run: make security-secrets + # Host mode, no `container:`: helm is baked into the runner image, and a container job # could not install it -- get.helm.sh is unreachable from the dind bridge. Same reason # release.yaml's chart job runs on the host. diff --git a/CLAUDE.md b/CLAUDE.md index 2cc2ba1..2d91a9d 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -35,3 +35,9 @@ for why. `make release` (build + push the multi-arch image, package + push the chart) is what `release.yaml` invokes. Do not run it by hand — it refuses `VERSION=dev` for that reason, and publishing happens by pushing a tag. + +Three scans, and they see different things: `security-go` (govulncheck) reads the source and +its module graph and reports only vulnerabilities the code can actually reach; +`security-secrets` (gitleaks) reads the working tree, not the history, so it catches a secret +on the way in rather than auditing what is already committed; `security-image` (trivy) reads +the published artifact and therefore only runs on a tag. The first two gate every push. diff --git a/Makefile b/Makefile index 93552aa..feb57dd 100644 --- a/Makefile +++ b/Makefile @@ -100,7 +100,9 @@ CHART_VERSION := $(shell echo "$(VERSION)" | sed 's/^v//') PLATFORMS ?= linux/amd64,linux/arm64 BUILDX_BUILDER ?= terdut -TRIVY_VERSION := 0.73.0 +TRIVY_VERSION := 0.73.0 +GOVULNCHECK_VERSION := v1.1.4 +GITLEAKS_VERSION := v8.30.0 # --pull, not --no-cache: refresh the base image without discarding the layer cache. DOCKER_BUILD_FLAGS ?= --pull @@ -171,6 +173,27 @@ release: push helm-package helm-push ## Publish image + chart (the workflow's on ## --- security --- +# Symbol-level, not dependency-level: govulncheck reports a vulnerability only when the +# code can actually reach it. As of 2026-09-02 this repo imports three chi advisories and +# reports none of them, because all three are middleware.RealIP and router.go uses Logger +# and Recoverer. That is the useful property rather than a loophole -- adding +# middleware.RealIP would turn this red, which is exactly when someone should look. +.PHONY: security-go +security-go: ## Scan Go deps for known CVEs (govulncheck) + go run golang.org/x/vuln/cmd/govulncheck@$(GOVULNCHECK_VERSION) ./... + +# --no-git scans the working tree rather than the history, so this catches a secret on the +# way in. It is not a history audit and finding nothing here says nothing about what is +# already committed. --redact because the finding is printed into a CI log. +# +# Note when testing it that gitleaks allowlists well-known example credentials -- the AWS +# key from their own documentation does not trip it. A private key block does. +.PHONY: security-secrets +security-secrets: ## Scan the working tree for committed secrets (gitleaks) + go run github.com/zricethezav/gitleaks/v8@$(GITLEAKS_VERSION) detect --no-git \ + --source . --redact --no-banner --exit-code 1 + + # 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. @@ -184,8 +207,8 @@ release: push helm-package helm-push ## Publish image + chart (the workflow's on # 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. +# This is the last of the three scans and the only one that needs a published artifact; +# security-go and security-secrets above run on every push. .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 diff --git a/README.md b/README.md index 451e6cc..a47945e 100644 --- a/README.md +++ b/README.md @@ -671,10 +671,13 @@ a flaky incident in production rather than as a red build. ## Releasing ``` -push to main → ci.yaml gofmt, go vet, go test -push tag vX.Y.Z → release.yaml same gate, then publish: +push or PR → ci.yaml gofmt, go vet, go test -race + govulncheck, gitleaks + helm lint + render +push tag vX.Y.Z → release.yaml the same gate, then publish: git.ryuvia.com/niklas/terdut-server:vX.Y.Z oci://git.ryuvia.com/niklas/terdut-server X.Y.Z + then trivy-scan the pushed image PR to Ryuvia/charts → bump the wrapper chart to X.Y.Z; on merge Flux reconciles and the release rolls out ```