Skip to content

sec(build): build the shipped image on go1.26.6 to clear the stdlib CVEs - #1835

Merged
cristim merged 4 commits into
mainfrom
sec/1833-go-1266-artifact
Aug 17, 2026
Merged

cristim merged 4 commits into
mainfrom
sec/1833-go-1266-artifact

Conversation

@cristim

@cristim cristim commented Aug 16, 2026 •

Copy link
Copy Markdown
Member

What

#1832 bumped GO_VERSION in ci.yml and the go-version pin in pre-commit.yml to 1.26.6, which turned govulncheck green. It did not change what we ship: the production builder stage still compiled on go1.26.5, so all seven stdlib advisories from #1829 stayed in the deployed binary.

This makes the whole shipped image stdlib-clean. Three things:

  1. The base image in Dockerfile (production builder), Dockerfile.dev and Dockerfile.test, from 1.26.5-alpine3.24 to 1.26.6-alpine3.24.
  2. The go directives in go.work and the five module go.mod files.
  3. golang-migrate is now compiled from source instead of downloaded as an upstream prebuilt release tarball. See below: this was the larger half of the problem.

No suppression of any kind: no ignore list, no -scan narrowing, no continue-on-error, no allowlist.

The image ships two Go binaries, and the base bump only fixed one

/usr/local/bin/migrate was an upstream prebuilt release (golang-migrate v4.19.1) downloaded from GitHub and copied into the final stage. It is not compiled by our toolchain, so nothing about a base-image bump touched it. Measured on an image built from this branch before the fix:

go version -m /usr/local/bin/migrate  ->  go1.25.4
Your code is affected by 69 vulnerabilities from 12 modules and the Go standard library.

35 of those are stdlib, and all seven advisories named in #1833 were present (GO-2026-6218, GO-2026-6091, GO-2026-6090, GO-2026-6089, GO-2026-5026, GO-2026-6088, GO-2026-5972).

This is not dormant code. scripts/entrypoint.sh runs migrate -path ... -database ... up on every container start, and DB_AUTO_MIGRATE defaults to true in this image. The vulnerable symbols govulncheck lists include url.URL.Parse, url.URL.ResolveReference and tls.Conn.Handshake, which are exactly the paths that parse the database URL and negotiate TLS to the database.

Bumping the pinned release was not an option: v4.19.1 is the latest upstream tag, and the fix for the 1.25 line is go1.25.13. So it is now built with go install on the stage's own pinned toolchain, the same way make install-tools does.

Two notes on that change:

  • Supply chain: this trades two hardcoded per-arch tarball SHA256s for Go module verification against sum.golang.org (no GOPROXY/GOFLAGS/GONOSUMDB overrides exist in this repo). The mod line in the built binary now carries the module hash, where the prebuilt binary's was blank.
  • Scope: built with -tags=postgres, so only the postgres/postgresql driver is linked in rather than upstream's 14 database drivers and 9 sources. The file source is registered unconditionally by the CLI, and entrypoint.sh only ever constructs a postgresql:// URL. The binary drops from 51.9 MB to 5.5 MB.

The digest pin was refreshed, not dropped

The @sha256: pin is kept. The new digest was resolved two independent ways, which agree:

  • Docker Hub registry v2 Docker-Content-Digest for library/golang:1.26.6-alpine3.24
  • docker buildx imagetools inspect golang:1.26.6-alpine3.24

Both return sha256:3889b425f035be855a72fb4755265311293b6d414521f0a519d819df32222d83, an OCI image index covering linux/amd64 and linux/arm64. The alpine3.24 variant does exist for 1.26.6, so the tag shape is unchanged.

Worth recording: the 1.26.6-alpine3.24 tag was re-pushed on 2026-08-16 and previously resolved to sha256:af8d6740.... Both digests carry org.opencontainers.image.version: 1.26.6-alpine3.24, so this was a legitimate rebuild on a patched Alpine base rather than anything suspicious. It is a good illustration of why the digest pin is worth keeping: the tag moved underneath us, and a digest-pinned build does not silently follow it.

The go directive bump is load-bearing, not tidiness

aws_sanity.yml, azure_sanity.yml and database-migration.yml (three call sites) resolve their toolchain with go-version-file: go.mod, not from GO_VERSION. No module carries a toolchain directive, so setup-go reads the go directive directly.

Measured rather than reasoned about. azure_sanity run 31898391027 from 2026-08-15, after #1832 merged, logs:

Setup go version spec 1.26.5
Successfully set up Go version 1.26.5
go version go1.26.5 linux/amd64

So those jobs were still installing 1.26.5. aws_sanity short-circuits before setup-go when its AWS secrets are absent, and database-migration.yml is workflow_dispatch-only so it has no recent runs, but all five call sites read the same root go.mod and are fixed by the same one-line change.

pre-commit.yml was the last workflow still carrying a literal go-version, and now reads go-version-file: go.mod too. The resolved version is unchanged; this removes one of the independent places the Go version had to be bumped by hand, which is the arrangement that produced #1833.

tests/e2e/go.mod deliberately stays at go 1.25

No workflow resolves a toolchain from it, GOTOOLCHAIN=auto only ever upgrades and never downgrades, the module is stdlib-only and test-only, and it is not part of the shipped artifact. It reported clean on both 1.26.5 and 1.26.6. Raising its floor would be an unrelated change.

Verification

Full runtime images were built from this branch for both target architectures, and both binaries extracted from each image and scanned with govulncheck -mode=binary at the CI-pinned v1.1.4:

binary arch go version -m govulncheck summary, verbatim stdlib
/app/cudly arm64 go1.26.6 Your code is affected by 2 vulnerabilities from 2 modules. 0
/app/cudly amd64 go1.26.6 Your code is affected by 2 vulnerabilities from 2 modules. 0
/usr/local/bin/migrate arm64 go1.26.6 No vulnerabilities found. 0
/usr/local/bin/migrate amd64 go1.26.6 No vulnerabilities found. 0

All seven advisories from #1833 are absent from all four binaries.

On how that was counted. The stdlib column is derived from govulncheck -format json, counting distinct OSV entries whose finding traces into module stdlib. It is deliberately not a text grep: govulncheck v1.1.4 prints a bare Standard library line and never the string Module: stdlib, so a grep -c "Module: stdlib" returns 0 whether or not stdlib CVEs are present. The scan harness was validated against a known-dirty control (the previously shipped prebuilt binary, checksum-matched to the SHA256 this PR removes), where it correctly reports 35 stdlib findings and all seven advisories present.

Runtime checks in the final image: migrate resolves on PATH to /usr/local/bin/migrate owned cudly:cudly, migrate -version reports v4.19.1, and a real migrate -path /app/migrations -database postgresql://... up gets as far as the TCP connect, confirming both the file source and the postgresql driver are registered. The entrypoint is unchanged.

The two remaining findings on /app/cudly are pre-existing and non-stdlib:

Advisory Module Fixed in
GO-2026-5932 golang.org/x/crypto (openpgp, unmaintained) N/A
GO-2026-5158 go.opentelemetry.io/otel v1.43.0 v1.44.0

Neither is introduced here: this PR changes zero dependency versions (go.sum diff is empty). GO-2026-5158 has a fix available and is worth a follow-up issue.

Also run at the exact CI pins: go build ./... (0), go vet ./... (0), golangci-lint v2.10.1 (0 issues.), gosec v2.28.0 (0).

Known gaps, stated rather than left implicit

  • CI still never scans the image it builds. The only scan-types in the workflow set are fs and config; docker-build builds the image, runs --version, and discards it. That is the structural reason a vulnerable binary could sit in the image invisibly. Being filed separately rather than bundled here.
  • Dockerfile.dev still downloads a prebuilt migrate (v4.17.0, older still). It is a local development image, not shipped, so it is out of scope for this PR, but it is the same defect class and worth a follow-up.

Closes #1833

Summary by CodeRabbit

  • Chores

    • Updated application, development, and test environments to Go 1.26.6.
    • Refreshed pinned container image versions and digests for consistent builds and tests.
    • Applied the Go version update across the core module and cloud provider modules.
    • Improved cross-platform availability of the database migration tooling.
  • Documentation

    • Updated deployment, development, contribution, and project prerequisites to require Go 1.26.6 or newer.
    • Aligned CI configuration with the project’s declared Go version.

#1832 bumped GO_VERSION and the pre-commit go-version pin, which turned
govulncheck green in CI, but the artifact we deploy was still compiled by
the go1.26.5 builder stage, so all seven stdlib advisories remained in the
binary we ship.

Bump the golang base image in Dockerfile (the production builder),
Dockerfile.dev and Dockerfile.test from 1.26.5-alpine3.24 to
1.26.6-alpine3.24, refreshing the SHA256 digest pin rather than dropping
it. The digest was resolved from the registry v2 manifest endpoint and
cross-checked against docker buildx imagetools inspect.

Bump the go directives in go.work and the five module go.mod files. This
is load-bearing rather than tidiness: aws_sanity.yml, azure_sanity.yml and
database-migration.yml resolve their toolchain via go-version-file: go.mod,
and an azure_sanity run from after #1832 merged logs "Successfully set up
Go version 1.26.5". No module carries a toolchain directive, so setup-go
reads the go directive directly.

tests/e2e/go.mod stays at go 1.25: no workflow resolves a toolchain from
it, GOTOOLCHAIN=auto only ever upgrades and never downgrades, and the
module is test-only, stdlib-only, and not part of the shipped artifact.
@coderabbitai

coderabbitai Bot commented Aug 16, 2026 •

Copy link
Copy Markdown
Contributor

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 1b198277-21b3-4da8-91a0-5f0bdc8f9b05

📥 Commits

Reviewing files that changed from the base of the PR and between 007829b and 78ad1bb.

📒 Files selected for processing (7)
  • .github/workflows/README.md
  • .github/workflows/pre-commit.yml
  • CONTRIBUTING.md
  • Dockerfile
  • README.md
  • docs/DEPLOYMENT.md
  • docs/DEVELOPMENT.md

Included review availability: 0 reviews are currently available. Based on recent review activity, included reviews refill at 3 per hour.


📝 Walkthrough

Walkthrough

The pull request updates the repository to Go 1.26.6. It refreshes pinned Docker images, compiles golang-migrate from source, aligns module and CI toolchain declarations, and updates documented prerequisites.

Changes

Go toolchain update

Layer / File(s) Summary
Container build and migration binary
Dockerfile, Dockerfile.dev, Dockerfile.test
Pinned images now use Go 1.26.6. The production build compiles golang-migrate v4.19.1 from source and removes the builder-stage curl dependency.
Module and CI toolchain alignment
go.mod, pkg/go.mod, providers/*/go.mod, .github/workflows/pre-commit.yml, .github/workflows/README.md
Affected modules declare Go 1.26.6. Pre-commit reads the version from go.mod, and the documented CI default is updated.
Developer and deployment prerequisites
CONTRIBUTING.md, README.md, docs/DEPLOYMENT.md, docs/DEVELOPMENT.md
Documentation and workspace examples require Go 1.26.6 or later.

Estimated code review effort: 3 (Moderate) | ~20 minutes

Merge Risk: ⚪ Minimal · up to 78ad1

The PR updates the shipped build and related toolchain pins to Go 1.26.6, with the extracted binary verified clean for the targeted stdlib advisories. No actionable merge-blocking risk remains beyond normal checks and review.

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Linked Issues check ✅ Passed The changes address issue #1833 by updating pinned images, Go directives, migration compilation, and documented verification requirements.
Out of Scope Changes check ✅ Passed The Docker, workflow, module, migration, and documentation changes support the Go 1.26.6 security update and are in scope.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly identifies the shipped image build change to Go 1.26.6 and its purpose of addressing standard-library CVEs.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch sec/1833-go-1266-artifact

Comment @coderabbitai help to get the list of available commands.

… toolchain

The runtime image contains two Go binaries and the base-image bump only moved
one of them. /usr/local/bin/migrate was an upstream prebuilt release tarball
compiled on go1.25.4, so it still carried all seven stdlib advisories from
issue #1833.

That binary is not dormant. scripts/entrypoint.sh runs `migrate up` on every
container start and DB_AUTO_MIGRATE defaults to true in this image, and the
vulnerable symbols govulncheck reports include url.URL.Parse,
url.URL.ResolveReference and tls.Conn.Handshake, which are the paths that parse
the database URL and negotiate TLS to the database.

Build it with `go install` on the stage's pinned toolchain, the same way
`make install-tools` does. Bumping the pinned release was not an option:
v4.19.1 is the latest upstream tag, and the fix for the 1.25 line is go1.25.13.
`go install` refuses GOBIN when cross-compiling and writes to
bin/$GOOS_$GOARCH/ instead, so both layouts are resolved and the final `mv`
fails the build if neither produced a binary. curl is dropped from the builder
stage because the download it served is gone.

Verified on full images built from this branch, both target architectures:

  binary                    arch    toolchain   govulncheck -mode=binary
  /app/cudly                arm64   go1.26.6    2 vulnerabilities from 2 modules
  /app/cudly                amd64   go1.26.6    2 vulnerabilities from 2 modules
  /usr/local/bin/migrate    arm64   go1.26.6    No vulnerabilities found.
  /usr/local/bin/migrate    amd64   go1.26.6    No vulnerabilities found.

Stdlib advisory count is 0 on all four, counted from `-format json` by module
rather than by a text pattern. The previously shipped prebuilt binary measures
"69 vulnerabilities from 12 modules and the Go standard library", 35 of them
stdlib. The two remaining findings on /app/cudly are pre-existing and
non-stdlib (GO-2026-5158, GO-2026-5932); this commit changes no dependency.

`migrate -version` still reports v4.19.1 in the final image, and a real
`migrate -path /app/migrations -database postgresql://... up` reaches the TCP
connect step, confirming both the file source and the postgresql driver are
registered under -tags postgres.
…dcoded pin

pre-commit.yml was the last workflow carrying a literal `go-version`. aws_sanity,
azure_sanity and database-migration already use `go-version-file: go.mod`, so
this removes one of the independent places the Go version had to be bumped by
hand. That arrangement is what produced issue #1833: #1832 bumped some of them
and CI went green while the shipped artifact stayed on the old toolchain.

The root go.mod `go` directive is 1.26.6, so the resolved version is unchanged
by this commit.
…quire

The documented prerequisites had drifted well below the `go` directive in
go.mod: "Go 1.23 or later" in CONTRIBUTING.md and README.md, "Go 1.25+" in
README.md, docs/DEPLOYMENT.md and docs/DEVELOPMENT.md.

Most concretely, CONTRIBUTING.md hands contributors a copy-paste `go.work.local`
template whose first line was `go 1.25.0`. A workspace `go` line below the
modules' own 1.26.6 is rejected, so following that section verbatim no longer
built. It now matches the committed go.work and says why it has to.

Also corrects .github/workflows/README.md, which documented the `GO_VERSION`
variable default as 1.25 when ci.yml sets 1.26.6.
@cristim
cristim merged commit 15a42f0 into main Aug 17, 2026
20 checks passed
cristim added a commit that referenced this pull request Aug 18, 2026
…ce (#1836)

Nothing in the pipeline inspected the container image we ship. govulncheck ran in source mode over the six modules, Trivy ran with scan-type fs and config, and docker-build built the image, ran --version against it, and discarded it. A vulnerable binary baked into the image was therefore unreachable by the pipeline by construction.

That gap hid two consecutive rounds of CVE work. #1832 bumped the Go toolchain CI reads and govulncheck went green while the image still built on go1.26.5. #1835 bumped the base image and /app/cudly came out clean, but the image shipped a second Go binary: a prebuilt golang-migrate release carrying go1.25.4 with 69 vulnerabilities across 12 modules, executed on every container start by entrypoint.sh with DB_AUTO_MIGRATE=true by default.

A new job runs govulncheck -mode=binary over every Go binary in the final stage after docker-build, and is wired into ci-success so it gates. It fails on any advisory with a published fixed version at any severity, and tolerates but always prints advisories with no published fix, so it does not land permanently red on GO-2026-5932 (x/crypto/openpgp, introduced: "0", no fixed event, unreachable here) while still failing on the classes that actually occurred.

The scanner was itself fail-open twice, and both were closed before merge. Its jq filter returned an empty array with exit 0 for an empty stream, for {}, and for a well-formed object with no config, all of which the classifier read as zero findings; the existing guards only fired on a parse error and none of those inputs is one, so a govulncheck run that silently produced nothing would have certified the image clean. The parser now requires the stream to open with a config message. That check then accepted any non-empty protocol_version, which would have reproduced the same hole on a tool upgrade emitting a different schema, so it is pinned to v1.0.0 with the expected and found values reported on mismatch.

Verified both directions with recorded fixtures: red on a known-bad image carrying the prebuilt migrate binary, green on the current image, empty and malformed and wrong-schema streams all exit 2 rather than clean, an unfixable-only stream is tolerated, and a fixable advisory fails. Suite is 9 passed, 0 failed, asserting message content rather than exit codes alone.

Trivy scan-type image was deliberately not added. The pinned runtime base alpine:3.21.3 carries fixable CRITICAL and HIGH OS advisories today, including a libssl3 heap overflow from a large X.509 certificate, so a gating step would land red on day one for something this change does not fix and a non-gating one would print an unactionable wall on every PR. Ordering matters: bump the base first, then add the OS-package gate.

Deferred and tracked: #1842 (bump the alpine runtime base, then add Trivy image scanning), #1837 (the unfixable x/crypto advisory this check tolerates by design).

Closes #1836
cristim added a commit that referenced this pull request Aug 18, 2026
…isories

The pinned runtime base alpine:3.21.3 shipped 17 fixable CRITICAL and HIGH OS advisories, measured with --ignore-unfixed so every one had a published fix available. The notable one is a libssl3 heap buffer overflow triggered by a large X.509 certificate, in a service that terminates TLS and connects to databases over TLS.

Bumped to alpine:3.24.1, re-pinned by digest. The digest was confirmed three independent ways rather than trusting a single server-reported value: docker buildx imagetools inspect, the registry v2 API Docker-Content-Digest header, and a content hash of the fetched manifest. It is the multi-arch OCI image index digest rather than a per-platform manifest, so it stays correct for every TARGETARCH. All three were re-verified immediately before committing, because a genuine digest went stale mid-flight during #1835 when the upstream tag was re-pushed onto a rebuilt image.

Every candidate was measured rather than assuming newest-is-cleanest, and that caution was warranted: 3.23.5 and 3.22.5 were rebuilt on 2026-06-22, after 3.24.1 was published on 2026-06-16, so newest-tag and most-recently-rebuilt disagreed.

Verified against the built artifact rather than the base tag alone, which is the distinction #1836 exists for: the image was rebuilt and rescanned, and the image scanner added by #1841 still passes on it, so the gate that now blocks CI is not regressed.

This unblocks adding Trivy scan-type image to CI. That was deliberately left out of #1841 because gating on OS packages while the base carried fixable advisories would have landed red on day one, and not gating would have printed an unactionable wall on every PR. The ordering was bump first, then gate; the gate is the remaining half.

Note on process: #1842 was auto-closed by the #1841 merge without any base bump having landed, because that PR's description contained the phrase resolve #1842 while explaining the ordering. GitHub parses closing keywords in the PR description as well as the commit message and does not read the surrounding prose. It was reopened and the work done here.

Closes #1842
cristim added a commit that referenced this pull request Sep 27, 2026
…ce (#1836)

Nothing in the pipeline inspected the container image we ship. govulncheck ran in source mode over the six modules, Trivy ran with scan-type fs and config, and docker-build built the image, ran --version against it, and discarded it. A vulnerable binary baked into the image was therefore unreachable by the pipeline by construction.

That gap hid two consecutive rounds of CVE work. #1832 bumped the Go toolchain CI reads and govulncheck went green while the image still built on go1.26.5. #1835 bumped the base image and /app/cudly came out clean, but the image shipped a second Go binary: a prebuilt golang-migrate release carrying go1.25.4 with 69 vulnerabilities across 12 modules, executed on every container start by entrypoint.sh with DB_AUTO_MIGRATE=true by default.

A new job runs govulncheck -mode=binary over every Go binary in the final stage after docker-build, and is wired into ci-success so it gates. It fails on any advisory with a published fixed version at any severity, and tolerates but always prints advisories with no published fix, so it does not land permanently red on GO-2026-5932 (x/crypto/openpgp, introduced: "0", no fixed event, unreachable here) while still failing on the classes that actually occurred.

The scanner was itself fail-open twice, and both were closed before merge. Its jq filter returned an empty array with exit 0 for an empty stream, for {}, and for a well-formed object with no config, all of which the classifier read as zero findings; the existing guards only fired on a parse error and none of those inputs is one, so a govulncheck run that silently produced nothing would have certified the image clean. The parser now requires the stream to open with a config message. That check then accepted any non-empty protocol_version, which would have reproduced the same hole on a tool upgrade emitting a different schema, so it is pinned to v1.0.0 with the expected and found values reported on mismatch.

Verified both directions with recorded fixtures: red on a known-bad image carrying the prebuilt migrate binary, green on the current image, empty and malformed and wrong-schema streams all exit 2 rather than clean, an unfixable-only stream is tolerated, and a fixable advisory fails. Suite is 9 passed, 0 failed, asserting message content rather than exit codes alone.

Trivy scan-type image was deliberately not added. The pinned runtime base alpine:3.21.3 carries fixable CRITICAL and HIGH OS advisories today, including a libssl3 heap overflow from a large X.509 certificate, so a gating step would land red on day one for something this change does not fix and a non-gating one would print an unactionable wall on every PR. Ordering matters: bump the base first, then add the OS-package gate.

Deferred and tracked: #1842 (bump the alpine runtime base, then add Trivy image scanning), #1837 (the unfixable x/crypto advisory this check tolerates by design).

Closes #1836
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

effort/s Hours impact/all-users Affects every user priority/p1 Next up; this sprint severity/high Significant harm triaged Item has been triaged type/security Security finding urgency/now Drop other things

Projects

None yet

Development

Successfully merging this pull request may close these issues.

sec(build): shipped image still builds on go1.26.5, so the stdlib CVEs remain in the artifact after #1832

1 participant