Skip to content

Commit e0a1163

Browse files
authored
🤖 ci: add code and image security scanning (#88)
## Summary Add repository and container security scanning by integrating CodeQL and Trivy into CI/release workflows. ## Background The repository already ran linting and vulnerability checks (`gosec`, `govulncheck`, Terraform Trivy config scan), but it did not yet provide first-class GitHub code scanning results or OCI image vulnerability scanning for publish/release paths. ## Implementation - Added a new CodeQL workflow for Go code scanning on PRs, pushes to `main`, and a weekly schedule. - Added a Trivy filesystem scan job in CI for repository-level scanning. - Added a Trivy image scan job in CI that builds/scans the local Docker image. - Updated `publish-main` gating to include filesystem and image scanning jobs. - Added a post-publish Trivy scan for `ghcr.io/coder/coder-k8s:main`. - Added a release-workflow Trivy scan for `ghcr.io/coder/coder-k8s:${{ github.event.release.tag_name }}`. - Enabled GoReleaser image SBOM generation. ## Validation - `go tool actionlint` - `make build` - `make test` - `make lint` - `make verify-vendor` ## Risks - New Trivy gating can fail CI on newly disclosed HIGH/CRITICAL CVEs in base/runtime dependencies. - Additional scan jobs increase CI duration. --- <details> <summary>📋 Implementation Plan</summary> # Plan: Add code scanning + OCI image scanning ## Context / Why You want to add **(1) code scanning** and **(2) OCI container image scanning** to the `coder-k8s` repository, ideally as part of GitHub Actions CI/CD. This repo already has good baseline security checks (gosec via golangci-lint, govulncheck, Trivy config scan for Terraform, zizmor for Actions). The main missing pieces are: - **Image vulnerability scanning** for the built/published `ghcr.io/coder/coder-k8s` images. - **First-class “code scanning” results** surfaced in GitHub’s Security UI (SARIF) rather than only console output. ## Evidence (what we verified) From repo inspection (Explore task): - `.github/workflows/ci.yaml` already runs: - `golangci-lint` with `gosec` enabled - `govulncheck ./...` - Trivy **config** scan for `terraform/` (using `aquasecurity/trivy-action@b6643a29...`) - `zizmor` for workflow security - `.github/workflows/release.yaml` uses GoReleaser to publish artifacts/images. - `.goreleaser.yaml` builds and pushes `ghcr.io/coder/coder-k8s` and has `dockers_v2.*.sbom: false`. - `Dockerfile.goreleaser` is `distroless/static:nonroot` and copies in the built binary. These files are sufficient to plan concrete changes to CI/CD for code + image scanning. ## Implementation details (proposed changes) ### 1) Add GitHub CodeQL workflow (Go code scanning) **Goal:** Add “code scanning” in the GitHub-native sense (SAST + Security tab integration). **Create:** `.github/workflows/codeql.yaml` (new) - Triggers: - `pull_request` (default branches) - `push` to `main` - `schedule` weekly (optional but recommended) - Permissions: - `security-events: write` - `contents: read` - `actions: read` - Steps: - `actions/checkout` (SHA-pinned, match repo style) - `github/codeql-action/init` (Go) - Build step (either `github/codeql-action/autobuild` or explicit `go build ./...` with `GOFLAGS=-mod=vendor`) - `github/codeql-action/analyze` Shape: ```yaml name: CodeQL on: push: branches: [main] pull_request: schedule: - cron: '0 6 * * 1' permissions: contents: read security-events: write actions: read jobs: analyze: runs-on: depot-ubuntu-24.04-8 steps: - uses: actions/checkout@<sha> with: persist-credentials: false - uses: github/codeql-action/init@<sha> with: languages: go - name: Build env: GOFLAGS: -mod=vendor run: go build ./... - uses: github/codeql-action/analyze@<sha> ``` Notes: - Keep all Actions **SHA-pinned** (this repo already pins). - If you expect external fork PRs, we should confirm CodeQL upload permissions behavior; otherwise this is fine. ### 2) Add Trivy filesystem scan (repo/code + secrets + config) **Goal:** Complement CodeQL with quick repo scanning for known vulnerable dependencies, leaked secrets patterns, and misconfigurations. **Update:** `.github/workflows/ci.yaml` - Add a new job (or add steps to existing `lint` job) using the already-pinned Trivy action. - Use `scan-type: fs` and scan the repo root. - Exclude `vendor/` (since deps are scanned via `govulncheck`, and `vendor/` can create noise/slowdowns). - Gate on `HIGH,CRITICAL` initially. Shape: ```yaml - name: Trivy filesystem scan uses: aquasecurity/trivy-action@b6643a2 with: scan-type: fs scan-ref: . severity: HIGH,CRITICAL exit-code: '1' skip-dirs: vendor ``` Optional (recommended if you want results in the GitHub Security UI): - Output SARIF and upload it: - Trivy: `format: sarif`, `output: trivy-fs.sarif` - Upload: `github/codeql-action/upload-sarif` (SHA-pinned) ### 3) Add Trivy OCI image vulnerability scanning **Goal:** Scan the built container image for vulnerabilities as part of CI/CD. There are two places to do this: #### 3a) CI (pre-merge) image scan job **Update:** `.github/workflows/ci.yaml` - Add `image-scan` job gated on the existing `changes.outputs.publish == 'true'` filter. - Build a local image (linux/amd64 is enough to start) with `Dockerfile.goreleaser`. - Run Trivy `scan-type: image` against the local image tag. Shape: ```yaml image-scan: needs: changes if: github.event_name == 'merge_group' || needs.changes.outputs.publish == 'true' runs-on: depot-ubuntu-24.04-8 steps: - uses: actions/checkout@<sha> with: persist-credentials: false - name: Build linux/amd64 binary env: GOFLAGS: -mod=vendor CGO_ENABLED: '0' GOOS: linux GOARCH: amd64 run: | mkdir -p linux/amd64 go build -o linux/amd64/coder-k8s ./ - name: Build image (local) run: docker build -f Dockerfile.goreleaser -t coder-k8s:scan . - name: Trivy image scan uses: aquasecurity/trivy-action@b6643a2 with: scan-type: image image-ref: coder-k8s:scan severity: HIGH,CRITICAL exit-code: '1' ``` Optional: SARIF output + upload-sarif (same as filesystem scan). #### 3b) Post-publish verification scan (main + releases) **Update:** - `.github/workflows/ci.yaml` `publish-main` job: add a Trivy image scan step for `ghcr.io/coder/coder-k8s:main`. - `.github/workflows/release.yaml`: add a Trivy image scan step for the release tag `ghcr.io/coder/coder-k8s:${{ github.event.release.tag_name }}`. This ensures the image that actually gets published is scanned even if a pre-merge job is skipped. ### 4) Enable SBOM generation for releases (optional but strongly recommended) **Goal:** Improve supply-chain posture and make scanning/auditing easier. **Update:** `.goreleaser.yaml` - Turn on SBOM generation for the container image and/or release archives. Proposed change: - In `dockers_v2` entry, set `sbom: true` (instead of `false`) if supported by your GoReleaser version. - Optionally also add a top-level `sboms:` section to generate SBOMs for release artifacts. ### 5) Add ignore/config files for managing findings **Goal:** Keep signal-to-noise high as scans are introduced. **Add (as needed):** - Repo root `.trivyignore` for image/fs scans (keep Terraform-specific ignore in `terraform/.trivyignore` as-is). - (Optional) `.trivy.yaml` to centralize excludes and scanner settings. ### 6) Validation plan (what to run before merging) After implementing in Exec mode: - `go tool actionlint` (or your existing workflow job) to validate workflow YAML. - Ensure CI passes for: - `make test` - `make build` - `make lint` - Smoke-test Trivy steps locally if desired (build image + run trivy). <details> <summary>Rationale / trade-offs</summary> - CodeQL provides “code scanning” in the GitHub ecosystem and catches classes of issues gosec won’t. - Trivy `fs` scan is fast and broad (secrets/config/vuln), but can be noisy; excluding `vendor/` and using `.trivyignore` helps. - Image scanning on PRs requires building the image; the image is small (distroless + static binary), so runtime impact should be acceptable. - Post-publish scans are a safety net; pre-merge scans provide real gating. </details> </details> --- _Generated with [`mux`](https://github.com/coder/mux) • Model: `openai:gpt-5.3-codex` • Thinking: `xhigh` • Cost: $0.34_ <!-- mux-attribution: model=openai:gpt-5.3-codex thinking=xhigh costs=0.34 -->
1 parent 8cb24dd commit e0a1163

4 files changed

Lines changed: 135 additions & 2 deletions

File tree

.github/workflows/ci.yaml

Lines changed: 73 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -113,6 +113,29 @@ jobs:
113113
- name: Run govulncheck
114114
run: go tool govulncheck ./...
115115

116+
scan-fs:
117+
name: Trivy filesystem scan
118+
needs: changes
119+
if: github.event_name == 'merge_group' || ((needs.changes.outputs.go == 'true' || needs.changes.outputs.workflows == 'true') && (github.event_name != 'push' || github.actor != 'github-merge-queue[bot]'))
120+
runs-on: depot-ubuntu-24.04-8
121+
timeout-minutes: 20
122+
permissions:
123+
contents: read
124+
steps:
125+
- name: Checkout
126+
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1
127+
with:
128+
persist-credentials: false
129+
130+
- name: Trivy filesystem scan
131+
uses: aquasecurity/trivy-action@b6643a29fecd7f34b3597bc6acb0a98b03d33ff8 # v0.33.1
132+
with:
133+
scan-type: fs
134+
scan-ref: .
135+
skip-dirs: vendor
136+
severity: HIGH,CRITICAL
137+
exit-code: '1'
138+
116139
test:
117140
needs: changes
118141
if: github.event_name == 'merge_group' || (needs.changes.outputs.go == 'true' && (github.event_name != 'push' || github.actor != 'github-merge-queue[bot]'))
@@ -290,6 +313,45 @@ jobs:
290313
echo "=== CNPG controller logs ==="
291314
kubectl -n cnpg-system logs deploy/cnpg-controller-manager --tail=200 || true
292315
316+
image-scan:
317+
name: Trivy image scan
318+
needs: changes
319+
if: github.event_name == 'merge_group' || (needs.changes.outputs.publish == 'true' && (github.event_name != 'push' || github.actor != 'github-merge-queue[bot]'))
320+
runs-on: depot-ubuntu-24.04-8
321+
timeout-minutes: 20
322+
steps:
323+
- name: Checkout
324+
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1
325+
with:
326+
persist-credentials: false
327+
328+
- name: Setup Go
329+
uses: actions/setup-go@40f1582b2485089dde7abd97c1529aa768e1baff # v5.6.0
330+
with:
331+
go-version-file: go.mod
332+
cache: true
333+
334+
- name: Build linux/amd64 binary for image
335+
env:
336+
GOFLAGS: -mod=vendor
337+
CGO_ENABLED: "0"
338+
GOOS: linux
339+
GOARCH: amd64
340+
run: |
341+
mkdir -p linux/amd64
342+
go build -o linux/amd64/coder-k8s ./
343+
344+
- name: Build local image
345+
run: docker build -f Dockerfile.goreleaser -t coder-k8s:scan .
346+
347+
- name: Trivy image scan
348+
uses: aquasecurity/trivy-action@b6643a29fecd7f34b3597bc6acb0a98b03d33ff8 # v0.33.1
349+
with:
350+
scan-type: image
351+
image-ref: coder-k8s:scan
352+
severity: HIGH,CRITICAL
353+
exit-code: '1'
354+
293355
terraform:
294356
name: Terraform (fmt/validate/tflint/trivy)
295357
needs: changes
@@ -393,16 +455,18 @@ jobs:
393455

394456
publish-main:
395457
name: Publish GHCR :main
396-
needs: [changes, test, lint, lint-actions, e2e-kind, terraform]
458+
needs: [changes, test, lint, scan-fs, lint-actions, e2e-kind, image-scan, terraform]
397459
if: |
398460
always() &&
399461
github.event_name == 'push' &&
400462
github.ref == 'refs/heads/main' &&
401463
needs.changes.outputs.publish == 'true' &&
402464
(needs.test.result == 'success' || needs.test.result == 'skipped') &&
403465
(needs.lint.result == 'success' || needs.lint.result == 'skipped') &&
466+
(needs.scan-fs.result == 'success' || needs.scan-fs.result == 'skipped') &&
404467
(needs.lint-actions.result == 'success' || needs.lint-actions.result == 'skipped') &&
405468
(needs.e2e-kind.result == 'success' || needs.e2e-kind.result == 'skipped') &&
469+
(needs.image-scan.result == 'success' || needs.image-scan.result == 'skipped') &&
406470
(needs.terraform.result == 'success' || needs.terraform.result == 'skipped')
407471
runs-on: depot-ubuntu-24.04-8
408472
timeout-minutes: 30
@@ -464,3 +528,11 @@ jobs:
464528
org.opencontainers.image.vendor=Coder
465529
org.opencontainers.image.licenses=Apache-2.0
466530
org.opencontainers.image.authors=Coder
531+
532+
- name: Trivy image scan (:main)
533+
uses: aquasecurity/trivy-action@b6643a29fecd7f34b3597bc6acb0a98b03d33ff8 # v0.33.1
534+
with:
535+
scan-type: image
536+
image-ref: ghcr.io/coder/coder-k8s:main
537+
severity: HIGH,CRITICAL
538+
exit-code: '1'

.github/workflows/codeql.yaml

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
name: CodeQL
2+
3+
on:
4+
pull_request:
5+
push:
6+
branches:
7+
- main
8+
schedule:
9+
- cron: '0 6 * * 1'
10+
11+
permissions:
12+
contents: read
13+
security-events: write
14+
actions: read
15+
16+
jobs:
17+
analyze:
18+
name: Analyze (Go)
19+
runs-on: depot-ubuntu-24.04-8
20+
timeout-minutes: 30
21+
steps:
22+
- name: Checkout
23+
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1
24+
with:
25+
persist-credentials: false
26+
27+
- name: Setup Go
28+
uses: actions/setup-go@40f1582b2485089dde7abd97c1529aa768e1baff # v5.6.0
29+
with:
30+
go-version-file: go.mod
31+
cache: true
32+
33+
- name: Initialize CodeQL
34+
uses: github/codeql-action/init@f5c2471be782132e47a6e6f9c725e56730d6e9a3 # v3.32.3
35+
with:
36+
languages: go
37+
38+
- name: Build
39+
env:
40+
GOFLAGS: -mod=vendor
41+
run: go build ./...
42+
43+
- name: Perform CodeQL Analysis
44+
uses: github/codeql-action/analyze@f5c2471be782132e47a6e6f9c725e56730d6e9a3 # v3.32.3

.github/workflows/release.yaml

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,3 +43,20 @@ jobs:
4343
args: release --clean
4444
env:
4545
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
46+
47+
- name: Compute release image tag
48+
id: release_image_tag
49+
env:
50+
RELEASE_TAG: ${{ github.event.release.tag_name }}
51+
run: |
52+
IMAGE_TAG="${RELEASE_TAG#v}"
53+
test -n "$IMAGE_TAG"
54+
echo "value=$IMAGE_TAG" >> "$GITHUB_OUTPUT"
55+
56+
- name: Trivy image scan (release)
57+
uses: aquasecurity/trivy-action@b6643a29fecd7f34b3597bc6acb0a98b03d33ff8 # v0.33.1
58+
with:
59+
scan-type: image
60+
image-ref: ghcr.io/coder/coder-k8s:${{ steps.release_image_tag.outputs.value }}
61+
severity: HIGH,CRITICAL
62+
exit-code: '1'

.goreleaser.yaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,7 @@ dockers_v2:
3838
- latest
3939
platforms:
4040
- linux/amd64
41-
sbom: false
41+
sbom: true
4242
labels:
4343
org.opencontainers.image.created: "{{ .Date }}"
4444
org.opencontainers.image.source: https://github.com/coder/coder-k8s

0 commit comments

Comments
 (0)