Repository navigation
sec(build): bump the runtime base to alpine:3.24.1 for fixable OS advisories - #1843
Conversation
…isories (#1842) The runtime stage was pinned to alpine:3.21.3, which carries 17 OS package advisories with a published fix: 2 CRITICAL and 15 HIGH across libcrypto3, libssl3, musl, musl-utils and zlib. The CRITICAL is CVE-2026-31789, a heap buffer overflow reachable from a large X.509 certificate, in a service that terminates TLS and connects to its database over TLS. The measurement uses --ignore-unfixed, so every one of the 17 had a fix available upstream and none of them was waiting on anything. The base is now alpine:3.24.1, pinned to the multi-arch index digest rather than a per-platform one so the pin stays correct for every TARGETARCH this image is built for. 3.21.7 also clears the fixable set and would have been the smaller move, but 3.24 is the current release line and 3.21 reaches end of support first, so this buys a longer runway for the same change. The digest was resolved three ways that agree: the Digest field from docker buildx imagetools inspect, the registry v2 Docker-Content-Digest header, and an independently recomputed SHA-256 over the raw index document. The third one matters because it hashes the content rather than trusting a value the server reports, and a genuine digest went stale mid-flight during #1835 when the upstream tag was re-pushed onto a rebuilt image. Measured on the built artifact, not the base tag, because a clean base does not prove a clean image: the runtime stage installs ca-certificates, postgresql-client, curl and tzdata on top of it. Building the identical Dockerfile on both bases and scanning the results gives 17 fixable CRITICAL and HIGH on alpine:3.21.3 and 0 on alpine:3.24.1, with /app/cudly and /usr/local/bin/migrate at 0 in both. The image scanner added by #1841 still passes on the rebuilt image, and its own self-test suite is 9 passed, 0 failed. Confirmed the runtime stage still provides what the binary and entrypoint.sh need, since a minor bump can move package names. psql, pg_dump, curl, sh, addgroup, adduser, ca-certificates and tzdata all resolve on both linux/amd64 and linux/arm64, and the multi-arch index covers the same eight platforms as before. End to end against a postgres container the image runs all 97 migrations, serves /health with HTTP 200, and Docker's own HEALTHCHECK reports healthy, which exercises the in-image curl and shell. Trivy scan-type: image is deliberately still not added. That is the next change and is tracked separately; adding it in the same commit as the bump would mean the gate and the fix it depends on land together with no independent evidence that the gate can fail. The stale justification in scripts/scan-shipped-image.sh, which cited alpine:3.21.3 as the reason the gate would land red, is updated to say the blocker is gone. Closes #1842
|
No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review info⚙️ Run configurationConfiguration used: defaults Review profile: CHILL Plan: Pro Run ID: 📒 Files selected for processing (2)
Included review availability: 1 review is currently available. Based on recent review activity, included reviews refill at 2 per hour. 📝 WalkthroughWalkthroughThe runtime stage now uses Alpine 3.24.1 with a refreshed pinned multi-architecture digest. The shipped-image scan documentation records that fixable CRITICAL and HIGH OS-package advisories are cleared. ChangesRuntime base refresh
Estimated code review effort: 1 (Trivial) | ~5 minutes Merge Risk: 🟡 Moderate · up to This PR changes the runtime image and its security-scan claim. Merge readiness depends on confirming that the rebuilt image passes integration tests and that the final shipped image is free of the claimed fixable OS advisories; until those checks are completed or explicitly accepted, merge should wait. 🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
Comment |
What
The runtime stage was pinned to
alpine:3.21.3, which ships 17 OS package advisories that all have a published fix: 2 CRITICAL and 15 HIGH acrosslibcrypto3,libssl3,musl,musl-utilsandzlib.The CRITICAL is CVE-2026-31789, a heap buffer overflow reachable from a large X.509 certificate, in a service that terminates TLS and connects to its database over TLS.
The base is now
alpine:3.24.1, pinned by digest.Why 3.24.1
3.21.7also clears the fixable set and would have been the smaller move, but 3.24 is the current release line and 3.21 reaches end of support first, so this buys a longer runway for the same change. All four candidate lines were measured rather than assumed, because3.23.5and3.22.5were rebuilt on 2026-06-22, after3.24.1was published on 2026-06-16, so "latest" was not automatically the cleanest:alpine:3.21.3(current)alpine:3.21.7alpine:3.22.5alpine:3.23.5alpine:3.24.1(chosen)The digest
sha256:28bd5fe8b56d1bd048e5babf5b10710ebe0bae67db86916198a6eec434943f8bThis is the multi-arch OCI index digest, not a per-platform manifest digest, so the pin stays correct for every
TARGETARCHthis image is built for. The new index covers the same eight platforms as the old one (386,amd64,arm64v8,armv6,armv7,ppc64le,riscv64,s390x).Resolved three ways that agree:
docker buildx imagetools inspectDigest:sha256:28bd5fe8...943f8bDocker-Content-Digestheadersha256:28bd5fe8...943f8bsha256:28bd5fe8...943f8bThe third matters because it hashes the content rather than trusting a value the server reports. A genuine digest went stale mid-flight during #1835 when the upstream tag was re-pushed onto a rebuilt image, and the pin is what caught it.
Measured on the built artifact, not the base tag
A clean base does not prove a clean image: the runtime stage installs
ca-certificates,postgresql-client,curlandtzdataon top of it. So the identical Dockerfile was built on both bases and the resulting images scanned.Before, base tag:
trivy image --severity CRITICAL,HIGH --ignore-unfixed --scanners vuln alpine:3.21.3After, base tag: same command against
alpine:3.24.1Built artifact, old base (
cudly:1842-oldbase)Built artifact, new base (
cudly:1842-amd64)Both Go binaries (
/app/cudly,/usr/local/bin/migrate) are at 0 on both bases, so the entire delta is OS packages, which is exactly the class #1841's govulncheck-based scanner does not cover.The #1841 image scanner still passes
bash scripts/scan-shipped-image.sh cudly:1842-amd64exits 0:Its own self-test suite (
scripts/test-scan-shipped-image.sh) is 9 passed, 0 failed.The application still starts
An Alpine minor bump can move package names, so this was checked rather than assumed.
psql,pg_dump,curl,sh,addgroup,adduser,ca-certificatesandtzdataall resolve on bothlinux/amd64andlinux/arm64.End to end against a
postgres:16-alpinecontainer, the image:migratev4.19.1, exercisingpsql/libpq and TLS libs),/healthwith HTTP 200,HEALTHCHECKreported healthy, which exercises the in-imagecurland/bin/sh.hadolintv2.15.1 (the digest pinned in.pre-commit-config.yaml) is clean on the modified Dockerfile, andshellcheckis clean on the modified script.Scope
This is the base bump only. Trivy
scan-type: imageis deliberately not added here, and remains tracked separately. Adding the gate in the same commit as the fix it depends on would mean both land together with no independent evidence that the gate can actually fail. Ordering is bump first, confirm clean, then gate.The stale justification in
scripts/scan-shipped-image.sh, which citedalpine:3.21.3as the reason an image gate would land red, is updated in this PR to record that the blocker is gone.Pin sites checked
Searched with a loose pattern (
alpine:?3,FROM[[:space:]]+alpine) across the whole repo and classified every hit, rather than a targeted one:Dockerfile:123FROM alpine:3.21.3@sha256:...scripts/scan-shipped-image.sh:42Dockerfile:18golang:1.26.6-alpine3.24Dockerfile:94node:24-alpineDockerfile.dev:8,Dockerfile.test:8docker-compose*.yml,internal/**/postgres.gopostgres:16-alpine,nginx:alpine.github/runbooks/compromised-dependency.md:74,77.hadolint.yamlDockerfileis the only file in the repo with a runtimeFROM alpine.Note on this issue's state
This issue was closed automatically on 2026-08-18T06:16:22Z by the merge of #1841, whose body contains a sentence of the form "This PR does not resolve" immediately followed by a reference to this issue number. GitHub's closing-keyword parser matches the keyword and ignores the negation, so the issue was closed despite the text saying the opposite. Issue #1837 hit the same thing earlier in this run.
That sentence is deliberately paraphrased rather than quoted here, because quoting it verbatim would re-trigger the same parser from this PR body. It has been reopened, and this PR closes it for real.
Refs #1836, #1841.
Closes #1842
Summary by CodeRabbit
Security
Maintenance