Scan the source and the working tree too, not just the image
The image scan added yesterday reads the built artifact. It cannot see a vulnerable dependency the binary never calls into, and it cannot see a credential in a file that never reaches the image — this one is FROM scratch and contains a single binary, so almost nothing in the repo is in it. Those are two different questions and they need two different tools, which is why riksdata and rd-web have run govulncheck and gitleaks all along. Both run on every push and pull request rather than only on a tag, since neither needs anything published. Checked by hand before wiring in, as with the image scan. govulncheck reports no vulnerabilities the code can reach, and gitleaks finds nothing in the tree. What govulncheck does report is worth writing down, because it is the argument for having it. It found three advisories in chi and reports none of them, all three being IP spoofing in middleware.RealIP, which router.go does not use — it uses Logger and Recoverer. The analysis is symbol-level rather than dependency-level, so adding middleware.RealIP would turn this red on the next push. That is precisely when someone should be made to look, and it is a plausible thing to reach for here, since the API sits behind a gateway and real client addresses are exactly what RealIP is for. The fourth finding is an integer overflow in golang.org/x/sys/windows, which a linux/scratch image will not be calling. Both gates were checked for the failure direction as well. gitleaks exits 1 on a private key block. Worth knowing when testing it: it allowlists well-known example credentials, so the AWS key from Amazon's own documentation does not trip it and proves nothing. Neither reads git history. gitleaks runs with --no-git, which scans the working tree, so it stops a secret on the way in and says nothing about what is already committed. Claude-Session: https://claude.ai/code/session_01S7R4gWTz5wh5xCY4nCSJjN
This commit is contained in:
@@ -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.
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -101,6 +101,8 @@ PLATFORMS ?= linux/amd64,linux/arm64
|
||||
BUILDX_BUILDER ?= terdut
|
||||
|
||||
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
|
||||
|
||||
@@ -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
|
||||
```
|
||||
|
||||
Reference in New Issue
Block a user