From 58a3a0d0bde7c14f47d902f158a32ca7d0236913 Mon Sep 17 00:00:00 2001 From: Ronald Hensbergen Date: Fri, 30 Oct 2026 23:37:13 +0100 Subject: [PATCH 1/3] fix(security): clear all .trivyignore exceptions by rebuilding the postgres and restic images (#1001) The trivyignore audit listed 34 exceptions as still reported although fixed package versions exist. A base-digest bump alone cannot clear them: the postgres:18 image ships a prebuilt gosu binary (no tag rebuilds it with a fixed Go toolchain), and restic 0.19.1 is still the latest upstream release whose published binary is built with vulnerable Go modules. - images/postgres/Dockerfile: pin the current postgres:18 digest (979bccf..., 2026-10-28) in the Dockerfile and modules/warehouse/postgres/module.yaml (plus the gitignored local docker-compose.yml / .cds profile), and replace the base image's gosu with a build from the pinned gosu 1.20 tag compiled with Go 1.27.3 in a digest-pinned golang builder stage. - images/restic/Dockerfile: compile restic v0.19.1 from source with Go 1.27.3 and the fixed module versions (go-jose/v4 4.1.6, klauspost/compress 1.18.7, x/crypto 0.57.0, x/net 0.60.0, x/text 0.42.0, grpc 1.83.2) on a digest-pinned alpine:3.24 base instead of using the prebuilt restic/restic image. - .trivyignore: drop all 34 resolved entries (26 gosu/stdlib CVEs, 8 restic module advisories incl. GHSA-hrxh-6v49-42gf); header comment kept. - publish-images.yml: derive the restic base version from the new ARG RESTIC_VERSION pin (the restic/restic FROM line is gone). - renovate.json: regex managers track ARG GOSU_VERSION (tianon/gosu), ARG RESTIC_VERSION (restic/restic) and the go-get module pins (golang datasource) so the source pins stay current. - Makefile: add dbt/dlt/restic to the docker-build repo-root context case list, matching docker-smoke-test.yml (their COPY paths are repo-root relative; drive-by fix). - tests/test_image_digest_check.py: example pin updated to the new digest. - CHANGELOG.md: Unreleased entry. Verified locally with Trivy v0.75.0 (same invocation as the scan workflow): both rebuilt images pass the HIGH/CRITICAL gate with --ignore-unfixed and the now-empty .trivyignore (exit 0); the audit script reports no remaining entries; all 34 audited IDs are absent from the rebuilt images and no new findings appear. Full test suite: 1971 passed, 24 skipped. Generated by Mistral Vibe. Co-Authored-By: Mistral Vibe --- .github/workflows/publish-images.yml | 5 +- .trivyignore | 178 ------------------------- CHANGELOG.md | 15 +++ Makefile | 2 +- images/postgres/Dockerfile | 19 ++- images/postgres/README.md | 4 + images/restic/Dockerfile | 26 +++- images/restic/README.md | 9 +- modules/warehouse/postgres/module.yaml | 2 +- renovate.json | 36 +++++ tests/test_image_digest_check.py | 4 +- 11 files changed, 113 insertions(+), 187 deletions(-) diff --git a/.github/workflows/publish-images.yml b/.github/workflows/publish-images.yml index ddd1c90e..69246d32 100644 --- a/.github/workflows/publish-images.yml +++ b/.github/workflows/publish-images.yml @@ -265,7 +265,10 @@ jobs: base_version="$(grep -m1 '^FROM postgres:' images/postgres/Dockerfile | sed -E 's#^FROM postgres:([^@[:space:]]+).*#\1#')" ;; restic) - base_version="$(grep -m1 '^FROM restic/restic:' images/restic/Dockerfile | sed -E 's#^FROM restic/restic:([^@[:space:]]+).*#\1#')" + # restic is compiled from source in images/restic/Dockerfile (the + # upstream restic/restic image is no longer the base), so the base + # version comes from the pinned source tag ARG. + base_version="$(grep -m1 '^ARG RESTIC_VERSION=' images/restic/Dockerfile | cut -d= -f3)" ;; *) echo "No version scheme defined for image '${{ matrix.image.name }}'" >&2 diff --git a/.trivyignore b/.trivyignore index a9f84853..57f46911 100644 --- a/.trivyignore +++ b/.trivyignore @@ -18,181 +18,3 @@ # Rules: only IDs reported by the scan may be listed, each ID at most once, # and exp must be a valid date at most 90 days out. An expired exception # stops being ignored and fails the daily scan again. - -# gosu 1.19 in the pinned postgres:18 base is built with go1.24.6; -# patched in the next upstream base image digest (refresh the pin) -# owner:@ronaldhensbergen -CVE-2025-61726 exp:2027-01-23 - -# gosu 1.19 in the pinned postgres:18 base is built with go1.24.6; -# patched in the next upstream base image digest (refresh the pin) -# owner:@ronaldhensbergen -CVE-2025-61729 exp:2027-01-23 - -# gosu 1.19 in the pinned postgres:18 base is built with go1.24.6; -# patched in the next upstream base image digest (refresh the pin) -# owner:@ronaldhensbergen -CVE-2025-68121 exp:2027-01-23 - -# gosu 1.19 in the pinned postgres:18 base is built with go1.24.6; -# patched in the next upstream base image digest (refresh the pin) -# owner:@ronaldhensbergen -CVE-2026-111943 exp:2027-01-23 - -# gosu 1.19 in the pinned postgres:18 base is built with go1.24.6; -# patched in the next upstream base image digest (refresh the pin) -# owner:@ronaldhensbergen -CVE-2026-25679 exp:2027-01-23 - -# gosu 1.19 in the pinned postgres:18 base is built with go1.24.6; -# patched in the next upstream base image digest (refresh the pin) -# owner:@ronaldhensbergen -CVE-2026-27145 exp:2027-01-23 - -# gosu 1.19 in the pinned postgres:18 base is built with go1.24.6; -# patched in the next upstream base image digest (refresh the pin) -# owner:@ronaldhensbergen -CVE-2026-32280 exp:2027-01-23 - -# gosu 1.19 in the pinned postgres:18 base is built with go1.24.6; -# patched in the next upstream base image digest (refresh the pin) -# owner:@ronaldhensbergen -CVE-2026-32281 exp:2027-01-23 - -# gosu 1.19 in the pinned postgres:18 base is built with go1.24.6; -# patched in the next upstream base image digest (refresh the pin) -# owner:@ronaldhensbergen -CVE-2026-32283 exp:2027-01-23 - -# gosu 1.19 in the pinned postgres:18 base is built with go1.24.6; -# patched in the next upstream base image digest (refresh the pin) -# owner:@ronaldhensbergen -CVE-2026-33811 exp:2027-01-23 - -# gosu 1.19 in the pinned postgres:18 base is built with go1.24.6; -# patched in the next upstream base image digest (refresh the pin) -# owner:@ronaldhensbergen -CVE-2026-33814 exp:2027-01-23 - -# gosu 1.19 in the pinned postgres:18 base is built with go1.24.6; -# patched in the next upstream base image digest (refresh the pin) -# owner:@ronaldhensbergen -CVE-2026-33818 exp:2027-01-23 - -# gosu 1.19 in the pinned postgres:18 base is built with go1.24.6; -# patched in the next upstream base image digest (refresh the pin) -# owner:@ronaldhensbergen -CVE-2026-39820 exp:2027-01-23 - -# gosu 1.19 in the pinned postgres:18 base is built with go1.24.6; -# patched in the next upstream base image digest (refresh the pin) -# owner:@ronaldhensbergen -CVE-2026-39821 exp:2027-01-23 - -# gosu 1.19 in the pinned postgres:18 base is built with go1.24.6; -# patched in the next upstream base image digest (refresh the pin) -# owner:@ronaldhensbergen -CVE-2026-39822 exp:2027-01-23 - -# gosu 1.19 in the pinned postgres:18 base is built with go1.24.6; -# patched in the next upstream base image digest (refresh the pin) -# owner:@ronaldhensbergen -CVE-2026-39836 exp:2027-01-23 - -# gosu 1.19 in the pinned postgres:18 base is built with go1.24.6; -# patched in the next upstream base image digest (refresh the pin) -# owner:@ronaldhensbergen -CVE-2026-42499 exp:2027-01-23 - -# gosu 1.19 in the pinned postgres:18 base is built with go1.24.6; -# patched in the next upstream base image digest (refresh the pin) -# owner:@ronaldhensbergen -CVE-2026-42504 exp:2027-01-23 - -# gosu 1.19 in the pinned postgres:18 base is built with go1.24.6; -# patched in the next upstream base image digest (refresh the pin) -# owner:@ronaldhensbergen -CVE-2026-56853 exp:2027-01-23 - -# gosu 1.19 in the pinned postgres:18 base is built with go1.24.6; -# patched in the next upstream base image digest (refresh the pin) -# owner:@ronaldhensbergen -CVE-2026-56858 exp:2027-01-23 - -# gosu 1.19 in the pinned postgres:18 base is built with go1.24.6; -# patched in the next upstream base image digest (refresh the pin) -# owner:@ronaldhensbergen -CVE-2026-56859 exp:2027-01-23 - -# gosu 1.19 in the pinned postgres:18 base is built with go1.24.6; -# patched in the next upstream base image digest (refresh the pin) -# owner:@ronaldhensbergen -CVE-2026-56860 exp:2027-01-23 - -# gosu 1.19 in the pinned postgres:18 base is built with go1.24.6; -# patched in the next upstream base image digest (refresh the pin) -# owner:@ronaldhensbergen -CVE-2026-56862 exp:2027-01-23 - -# gosu 1.19 in the pinned postgres:18 base is built with go1.24.6; -# patched in the next upstream base image digest (refresh the pin) -# owner:@ronaldhensbergen -CVE-2026-78667 exp:2027-01-23 - -# gosu 1.19 in the pinned postgres:18 base is built with go1.24.6; -# patched in the next upstream base image digest (refresh the pin) -# owner:@ronaldhensbergen -CVE-2026-78669 exp:2027-01-23 - -# gosu 1.19 in the pinned postgres:18 base is built with go1.24.6; -# patched in the next upstream base image digest (refresh the pin) -# owner:@ronaldhensbergen -CVE-2026-97031 exp:2027-01-23 - -# restic 0.19.1 (the latest upstream release, digest-pinned) is built with -# github.com/klauspost/compress v1.18.6; patched in the next restic -# release (refresh the pin) -# owner:@ronaldhensbergen -CVE-2026-63209 exp:2027-01-26 - -# restic 0.19.1 (the latest upstream release, digest-pinned) is built with -# golang.org/x/crypto v0.52.0; patched in the next restic release -# (refresh the pin) -# owner:@ronaldhensbergen -CVE-2026-56854 exp:2027-01-26 - -# restic 0.19.1 (the latest upstream release, digest-pinned) is built with -# golang.org/x/net v0.55.0 and Go stdlib v1.26.4; patched in the next -# restic release (refresh the pin) -# owner:@ronaldhensbergen -CVE-2026-46600 exp:2027-01-26 - -# restic 0.19.1 (the latest upstream release, digest-pinned) is built with -# golang.org/x/text v0.37.0 (CVE-2026-56851); patched in the next restic -# release (refresh the pin) -# owner:@ronaldhensbergen -CVE-2026-56851 exp:2027-01-26 - -# restic 0.19.1 (the latest upstream release, digest-pinned) is built with -# golang.org/x/text v0.37.0 (CVE-2026-56852); patched in the next restic -# release (refresh the pin) -# owner:@ronaldhensbergen -CVE-2026-56852 exp:2027-01-26 - -# restic 0.19.1 (the latest upstream release, digest-pinned) is built with -# google.golang.org/grpc v1.81.1 (CVE-2026-84304); patched in the next -# restic release (refresh the pin) -# owner:@ronaldhensbergen -CVE-2026-84304 exp:2027-01-26 - -# restic 0.19.1 (the latest upstream release, digest-pinned) is built with -# google.golang.org/grpc v1.81.1 (CVE-2026-84445); patched in the next -# restic release (refresh the pin) -# owner:@ronaldhensbergen -CVE-2026-84445 exp:2027-01-26 - -# restic 0.19.1 (the latest upstream release, digest-pinned) is built with -# google.golang.org/grpc v1.81.1 (GHSA-hrxh-6v49-42gf); patched in the next -# restic release (refresh the pin) -# owner:@ronaldhensbergen -GHSA-hrxh-6v49-42gf exp:2027-01-26 diff --git a/CHANGELOG.md b/CHANGELOG.md index 43e92877..8fbcf7f8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -73,6 +73,21 @@ The format is based on Keep a Changelog. ### Fixed +- Cleared the 34 `.trivyignore` exceptions listed by the trivyignore audit + (#1001) by fixing the findings at the source in the rebuilt images instead + of ignoring them: the `postgres` image now pins the current `postgres:18` + digest (also in `modules/warehouse/postgres/module.yaml`, + `docker-compose.yml`, and `.cds/haven-dev-noinit.yml`) and replaces the + base image's prebuilt `gosu` binary — which no `postgres:18` tag rebuilds + with a fixed Go toolchain — with a build from the pinned gosu 1.20 tag + compiled with Go 1.27.3; the `restic` image now compiles restic 0.19.1 (the + latest upstream release) from source with Go 1.27.3 and the fixed module + versions (`go-jose/v4` 4.1.6, `klauspost/compress` 1.18.7, `x/crypto` + 0.57.0, `x/net` 0.60.0, `x/text` 0.42.0, `grpc` 1.83.2) on a pinned Alpine + base instead of using the vulnerable prebuilt `restic/restic` image. Both + rebuilt images scan clean of HIGH/CRITICAL findings with Trivy v0.75.0. + + - Fixed provided contract endpoints silently breaking when a module instance id differs from the module's compose service name (issue #1010, found in the review of #1006): contract resolution substituted diff --git a/Makefile b/Makefile index 0bdd11bc..ab020252 100644 --- a/Makefile +++ b/Makefile @@ -51,7 +51,7 @@ docker-build: echo "Building $$dockerfile in directory $$dir..."; \ context="$$dir"; \ case "$$dockerfile" in \ - ./images/dagster/*|./images/superset/*|./images/postgres/*) context="." ;; \ + ./images/dagster/*|./images/superset/*|./images/dbt/*|./images/dlt/*|./images/postgres/*|./images/restic/*) context="." ;; \ esac; \ docker build -f "$$dockerfile" "$$context" || exit 1; \ done diff --git a/images/postgres/Dockerfile b/images/postgres/Dockerfile index 5456c98e..e468dd5e 100644 --- a/images/postgres/Dockerfile +++ b/images/postgres/Dockerfile @@ -13,10 +13,27 @@ # The pgdg apt repository is already configured in the upstream postgres # image; the pgbackrest package version is pinned for reproducible builds. -FROM postgres:18@sha256:32ca0af8e77bfb8c6610c488e4691f83f972a3e9e64d3b02facf3ab111ad5500 +# gosu build stage: the upstream postgres image ships a prebuilt gosu binary +# compiled with an old Go toolchain, and no postgres:18 tag rebuilds it, so +# the Go stdlib CVEs against that binary are not fixable by a base digest +# bump alone (#1001). Build gosu from the pinned upstream tag with the +# current Go toolchain instead; the result is a static binary +# (CGO_ENABLED=0), verified against the Go checksum database. +FROM golang:1.27.3-alpine@sha256:d200d2dd76b03a99b52651fd1325aff7dcd35422c35c01bac56571b0507f3deb AS gosu +ARG GOSU_VERSION=1.20 +RUN mkdir -p /src \ + && wget -qO- "https://codeload.github.com/tianon/gosu/tar.gz/refs/tags/${GOSU_VERSION}" | tar xz --strip-components=1 -C /src \ + && cd /src \ + && CGO_ENABLED=0 go build -trimpath -o /go/bin/gosu . + +FROM postgres:18@sha256:979bccfde35445db618c65107d27d08c6c21339d8dc3ad4c830c6633947be0e4 USER root +# Replace the base image's prebuilt gosu with the toolchain-current build +# from the stage above (see the comment on that stage). +COPY --from=gosu /go/bin/gosu /usr/local/bin/gosu + # Pull in Debian security fixes for the base image's OS packages before # installing pgbackrest, mirroring images/superset/base/Dockerfile: the # pinned upstream digest can lag behind trixie-security, and the Trivy diff --git a/images/postgres/README.md b/images/postgres/README.md index 041a56e9..6bc5c512 100644 --- a/images/postgres/README.md +++ b/images/postgres/README.md @@ -4,6 +4,10 @@ PostgreSQL image with [pgBackRest](https://pgbackrest.org/) for the built-in backup capability of the `warehouse/postgres` module (issue #665). - Base: `postgres:18`, digest-pinned (Renovate keeps the pin current). +- Replaces the base image's prebuilt `gosu` binary with a build from the + pinned gosu tag compiled with the current Go toolchain: the upstream + image downloads a prebuilt binary that no `postgres:18` tag rebuilds, so + its Go stdlib CVEs are only fixable by compiling gosu here (#1001). - Adds the `pgbackrest` package from the pgdg apt repository baked into the upstream image, version-pinned for reproducible builds. - `cds-pgbackrest-archive` — PostgreSQL `archive_command` handler. Archives WAL diff --git a/images/restic/Dockerfile b/images/restic/Dockerfile index 979703ca..2985abd9 100644 --- a/images/restic/Dockerfile +++ b/images/restic/Dockerfile @@ -11,11 +11,35 @@ # Repository location and S3 settings come from the CDS_BACKUP_* # environment variables rendered by the module; the repository is always # client-side encrypted with RESTIC_PASSWORD from the profile's secrets. +# +# restic is compiled from source instead of using the upstream +# restic/restic image: 0.19.1 is still the latest upstream release, and its +# published binary is built with vulnerable Go modules and an old stdlib, +# so no upstream image refresh can clear those findings (#1001). Building +# the pinned release tag with the current Go toolchain and the fixed module +# versions below does. The versions are the fixed versions from the Trivy +# advisories (x/net v0.60.0 requires x/crypto >= v0.57.0 and x/text +# >= v0.42.0); Renovate tracks them via the regex manager in renovate.json. + +FROM golang:1.27.3-alpine@sha256:d200d2dd76b03a99b52651fd1325aff7dcd35422c35c01bac56571b0507f3deb AS restic-build +ARG RESTIC_VERSION=0.19.1 +WORKDIR /src +RUN wget -qO- "https://codeload.github.com/restic/restic/tar.gz/refs/tags/v${RESTIC_VERSION}" | tar xz --strip-components=1 \ + && go get github.com/go-jose/go-jose/v4@v4.1.6 \ + github.com/klauspost/compress@v1.18.7 \ + golang.org/x/crypto@v0.57.0 \ + golang.org/x/net@v0.60.0 \ + golang.org/x/text@v0.42.0 \ + google.golang.org/grpc@v1.83.2 \ + && go mod tidy \ + && CGO_ENABLED=0 go build -trimpath -o /restic ./cmd/restic -FROM restic/restic:0.19.1@sha256:136600b6ff6843d61d355f7f71f460a166429f35de6fd11b568fece3c9a4d510 +FROM alpine:3.24@sha256:be2656061b536c1883c4034790e21d0a09c5aaaf51968b89368a3c0371ea2c9a USER root +COPY --from=restic-build /restic /usr/bin/restic + # Pull in Alpine security fixes for the base image's OS packages before # creating the user, mirroring images/postgres/Dockerfile: the pinned # upstream digest can lag behind the fixed packages, and the Trivy scan diff --git a/images/restic/README.md b/images/restic/README.md index b2782003..30ab4dac 100644 --- a/images/restic/README.md +++ b/images/restic/README.md @@ -3,8 +3,13 @@ Restic image for the generic file/volume backup module (`modules/backup/restic`, issue #668). -- Base: `restic/restic:0.19.1`, digest-pinned (Renovate keeps the pin - current). +- Base: `alpine:3.24`, digest-pinned (Renovate keeps the pin current). restic + itself is compiled from source in a builder stage: `RESTIC_VERSION` pins + the upstream release tag (0.19.1 is still the latest release) and the + `go get` line pins the fixed Go module versions from the Trivy advisories + against the published 0.19.1 binary, compiled with the current Go + toolchain (#1001); Renovate tracks the ARG and the module pins. Using the + prebuilt `restic/restic` image instead would reintroduce those findings. - Adds a dedicated non-root `restic` user (uid 10000) and the repository / source mount points (`/var/lib/restic-repo`, `/backup`) with the right ownership, so a fresh named volume inherits uid/gid 10000 on first use diff --git a/modules/warehouse/postgres/module.yaml b/modules/warehouse/postgres/module.yaml index c9cf1c4a..f629d766 100644 --- a/modules/warehouse/postgres/module.yaml +++ b/modules/warehouse/postgres/module.yaml @@ -328,7 +328,7 @@ spec: # production-only pieces below replace these fields in production # profiles (production: mapping form; see apply_production_gates). postgres: - image: postgres:18@sha256:32ca0af8e77bfb8c6610c488e4691f83f972a3e9e64d3b02facf3ab111ad5500 + image: postgres:18@sha256:979bccfde35445db618c65107d27d08c6c21339d8dc3ad4c830c6633947be0e4 user: postgres init: true read_only: true diff --git a/renovate.json b/renovate.json index b32c6c31..fc17ad39 100644 --- a/renovate.json +++ b/renovate.json @@ -9,6 +9,42 @@ ] }, "customManagers": [ + { + "customType": "regex", + "description": "Track the gosu source tag compiled in images/postgres/Dockerfile to replace the postgres base image's prebuilt gosu binary (upstream publishes unprefixed tags like 1.20, hence the loose versioning)", + "matchStrings": [ + "ARG GOSU_VERSION=(?\\S+)" + ], + "depNameTemplate": "tianon/gosu", + "datasourceTemplate": "docker", + "versioningTemplate": "loose", + "managerFilePatterns": [ + "/^images/postgres/Dockerfile$/" + ] + }, + { + "customType": "regex", + "description": "Track the restic source tag compiled in images/restic/Dockerfile (restic is built from source with patched Go modules; the upstream restic/restic image is no longer used as a base)", + "matchStrings": [ + "ARG RESTIC_VERSION=(?\\S+)" + ], + "depNameTemplate": "restic/restic", + "datasourceTemplate": "docker", + "managerFilePatterns": [ + "/^images/restic/Dockerfile$/" + ] + }, + { + "customType": "regex", + "description": "Track the Go module versions pinned in the `go get` line of images/restic/Dockerfile (the fixed versions from the Trivy advisories against the restic 0.19.1 binary)", + "matchStrings": [ + "(?[\\w./-]+)@(?v[\\w.-]+)" + ], + "datasourceTemplate": "golang", + "managerFilePatterns": [ + "/^images/restic/Dockerfile$/" + ] + }, { "customType": "regex", "description": "Track pip pins in Dagster entrypoint script", diff --git a/tests/test_image_digest_check.py b/tests/test_image_digest_check.py index 584668c3..ceb91f6f 100644 --- a/tests/test_image_digest_check.py +++ b/tests/test_image_digest_check.py @@ -42,13 +42,13 @@ def __exit__(self, *exc): class ParsePinnedReferenceTest(unittest.TestCase): def test_parses_tag_and_digest(self): parsed = parse_pinned_reference( - "postgres:18@sha256:32ca0af8e77bfb8c6610c488e4691f83f972a3e9e64d3b02facf3ab111ad5500" + "postgres:18@sha256:979bccfde35445db618c65107d27d08c6c21339d8dc3ad4c830c6633947be0e4" ) self.assertEqual( parsed, ( "postgres:18", - "sha256:32ca0af8e77bfb8c6610c488e4691f83f972a3e9e64d3b02facf3ab111ad5500", + "sha256:979bccfde35445db618c65107d27d08c6c21339d8dc3ad4c830c6633947be0e4", ), ) From 72737080936eea49bbd8e4206642d83fc6eb60fa Mon Sep 17 00:00:00 2001 From: Ronald Hensbergen Date: Fri, 30 Oct 2026 23:47:11 +0100 Subject: [PATCH 2/3] fix(lint): collapse double blank line in the #1001 CHANGELOG entry markdownlint MD012/no-multiple-blanks failed the CI lint job on CHANGELOG.md:90; the entry is now followed by a single blank line. Generated by Mistral Vibe. Co-Authored-By: Mistral Vibe --- CHANGELOG.md | 1 - 1 file changed, 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 8fbcf7f8..f06b3e9c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -87,7 +87,6 @@ The format is based on Keep a Changelog. base instead of using the vulnerable prebuilt `restic/restic` image. Both rebuilt images scan clean of HIGH/CRITICAL findings with Trivy v0.75.0. - - Fixed provided contract endpoints silently breaking when a module instance id differs from the module's compose service name (issue #1010, found in the review of #1006): contract resolution substituted From 31ad3533fb5f93a14e685009c9391fdb2153d8a3 Mon Sep 17 00:00:00 2001 From: Ronald Hensbergen Date: Sat, 31 Oct 2026 14:06:49 +0100 Subject: [PATCH 3/3] fix(security): verify gosu/restic source tarballs against pinned sha256 in the image builds (#1041) Address the review hardening notes on #1041: the codeload tarballs for GOSU_VERSION and RESTIC_VERSION are now downloaded to a file and checked with sha256sum -c against GOSU_TARBALL_SHA256 / RESTIC_TARBALL_SHA256 before extraction, so the build fails closed on a tag re-point or a compromised codeload response. No Renovate datasource covers codeload tarball hashes, so the sha256 ARGs are documented as manual companions of the version ARGs (renovate.json descriptions, image READMEs) and the CHANGELOG entry notes the pinning. Hashes verified stable across two independent downloads and both images build and run locally with the pins (gosu 1.20 go1.27.3, restic 0.19.1 go1.27.3); a wrong --build-arg hash fails the build at the checksum step. Generated by Mistral Vibe. Co-Authored-By: Mistral Vibe --- CHANGELOG.md | 6 ++++-- images/postgres/Dockerfile | 14 ++++++++++++-- images/postgres/README.md | 4 +++- images/restic/Dockerfile | 11 ++++++++++- images/restic/README.md | 6 ++++-- renovate.json | 4 ++-- 6 files changed, 35 insertions(+), 10 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index f06b3e9c..aa185f43 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -84,8 +84,10 @@ The format is based on Keep a Changelog. latest upstream release) from source with Go 1.27.3 and the fixed module versions (`go-jose/v4` 4.1.6, `klauspost/compress` 1.18.7, `x/crypto` 0.57.0, `x/net` 0.60.0, `x/text` 0.42.0, `grpc` 1.83.2) on a pinned Alpine - base instead of using the vulnerable prebuilt `restic/restic` image. Both - rebuilt images scan clean of HIGH/CRITICAL findings with Trivy v0.75.0. + base instead of using the vulnerable prebuilt `restic/restic` image; both + source tarballs are verified against pinned sha256 hashes in the builder + stages. Both rebuilt images scan clean of HIGH/CRITICAL findings with + Trivy v0.75.0. - Fixed provided contract endpoints silently breaking when a module instance id differs from the module's compose service name (issue diff --git a/images/postgres/Dockerfile b/images/postgres/Dockerfile index e468dd5e..aac8caf5 100644 --- a/images/postgres/Dockerfile +++ b/images/postgres/Dockerfile @@ -18,11 +18,21 @@ # the Go stdlib CVEs against that binary are not fixable by a base digest # bump alone (#1001). Build gosu from the pinned upstream tag with the # current Go toolchain instead; the result is a static binary -# (CGO_ENABLED=0), verified against the Go checksum database. +# (CGO_ENABLED=0). The source tarball is verified against the pinned +# sha256 below (review hardening on #1041); module dependencies would be +# covered by the Go checksum database, but gosu has none. FROM golang:1.27.3-alpine@sha256:d200d2dd76b03a99b52651fd1325aff7dcd35422c35c01bac56571b0507f3deb AS gosu ARG GOSU_VERSION=1.20 +# sha256 of https://codeload.github.com/tianon/gosu/tar.gz/refs/tags/${GOSU_VERSION} +# (verified stable across downloads). No Renovate datasource exists for +# codeload tarball hashes, so bump this ARG together with GOSU_VERSION — +# the build fails closed on a mismatch. +ARG GOSU_TARBALL_SHA256=207a2045e890d3ca3326027c5f585733085960b401a49dbc00e3bb628c054cf8 RUN mkdir -p /src \ - && wget -qO- "https://codeload.github.com/tianon/gosu/tar.gz/refs/tags/${GOSU_VERSION}" | tar xz --strip-components=1 -C /src \ + && wget -qO /tmp/gosu.tar.gz "https://codeload.github.com/tianon/gosu/tar.gz/refs/tags/${GOSU_VERSION}" \ + && echo "${GOSU_TARBALL_SHA256} /tmp/gosu.tar.gz" | sha256sum -c - \ + && tar xz --strip-components=1 -C /src -f /tmp/gosu.tar.gz \ + && rm /tmp/gosu.tar.gz \ && cd /src \ && CGO_ENABLED=0 go build -trimpath -o /go/bin/gosu . diff --git a/images/postgres/README.md b/images/postgres/README.md index 6bc5c512..8ccc4ebf 100644 --- a/images/postgres/README.md +++ b/images/postgres/README.md @@ -7,7 +7,9 @@ backup capability of the `warehouse/postgres` module (issue #665). - Replaces the base image's prebuilt `gosu` binary with a build from the pinned gosu tag compiled with the current Go toolchain: the upstream image downloads a prebuilt binary that no `postgres:18` tag rebuilds, so - its Go stdlib CVEs are only fixable by compiling gosu here (#1001). + its Go stdlib CVEs are only fixable by compiling gosu here (#1001). The + source tarball is verified against the pinned `GOSU_TARBALL_SHA256` + during the build (bump it together with `GOSU_VERSION`). - Adds the `pgbackrest` package from the pgdg apt repository baked into the upstream image, version-pinned for reproducible builds. - `cds-pgbackrest-archive` — PostgreSQL `archive_command` handler. Archives WAL diff --git a/images/restic/Dockerfile b/images/restic/Dockerfile index 2985abd9..fd1e1d8a 100644 --- a/images/restic/Dockerfile +++ b/images/restic/Dockerfile @@ -23,8 +23,17 @@ FROM golang:1.27.3-alpine@sha256:d200d2dd76b03a99b52651fd1325aff7dcd35422c35c01bac56571b0507f3deb AS restic-build ARG RESTIC_VERSION=0.19.1 +# sha256 of https://codeload.github.com/restic/restic/tar.gz/refs/tags/v${RESTIC_VERSION} +# (verified stable across downloads). No Renovate datasource exists for +# codeload tarball hashes, so bump this ARG together with RESTIC_VERSION — +# the build fails closed on a mismatch. Module deps fetched by go get/go mod +# tidy are covered by the Go checksum database. +ARG RESTIC_TARBALL_SHA256=bb9b1a19040744d26d8a79be029d4e6b189c45ccc9d8831d7fe367d3c33df725 WORKDIR /src -RUN wget -qO- "https://codeload.github.com/restic/restic/tar.gz/refs/tags/v${RESTIC_VERSION}" | tar xz --strip-components=1 \ +RUN wget -qO /tmp/restic.tar.gz "https://codeload.github.com/restic/restic/tar.gz/refs/tags/v${RESTIC_VERSION}" \ + && echo "${RESTIC_TARBALL_SHA256} /tmp/restic.tar.gz" | sha256sum -c - \ + && tar xz --strip-components=1 -f /tmp/restic.tar.gz \ + && rm /tmp/restic.tar.gz \ && go get github.com/go-jose/go-jose/v4@v4.1.6 \ github.com/klauspost/compress@v1.18.7 \ golang.org/x/crypto@v0.57.0 \ diff --git a/images/restic/README.md b/images/restic/README.md index 30ab4dac..02122705 100644 --- a/images/restic/README.md +++ b/images/restic/README.md @@ -8,8 +8,10 @@ Restic image for the generic file/volume backup module the upstream release tag (0.19.1 is still the latest release) and the `go get` line pins the fixed Go module versions from the Trivy advisories against the published 0.19.1 binary, compiled with the current Go - toolchain (#1001); Renovate tracks the ARG and the module pins. Using the - prebuilt `restic/restic` image instead would reintroduce those findings. + toolchain (#1001); Renovate tracks the ARG and the module pins. The source + tarball is verified against the pinned `RESTIC_TARBALL_SHA256` during the + build (bump it together with `RESTIC_VERSION`). Using the prebuilt + `restic/restic` image instead would reintroduce those findings. - Adds a dedicated non-root `restic` user (uid 10000) and the repository / source mount points (`/var/lib/restic-repo`, `/backup`) with the right ownership, so a fresh named volume inherits uid/gid 10000 on first use diff --git a/renovate.json b/renovate.json index fc17ad39..85452b45 100644 --- a/renovate.json +++ b/renovate.json @@ -11,7 +11,7 @@ "customManagers": [ { "customType": "regex", - "description": "Track the gosu source tag compiled in images/postgres/Dockerfile to replace the postgres base image's prebuilt gosu binary (upstream publishes unprefixed tags like 1.20, hence the loose versioning)", + "description": "Track the gosu source tag compiled in images/postgres/Dockerfile to replace the postgres base image's prebuilt gosu binary (upstream publishes unprefixed tags like 1.20, hence the loose versioning); when the tag changes, bump GOSU_TARBALL_SHA256 in the same file — no datasource covers codeload tarball hashes, so the sha256 is manual", "matchStrings": [ "ARG GOSU_VERSION=(?\\S+)" ], @@ -24,7 +24,7 @@ }, { "customType": "regex", - "description": "Track the restic source tag compiled in images/restic/Dockerfile (restic is built from source with patched Go modules; the upstream restic/restic image is no longer used as a base)", + "description": "Track the restic source tag compiled in images/restic/Dockerfile (restic is built from source with patched Go modules; the upstream restic/restic image is no longer used as a base); when the tag changes, bump RESTIC_TARBALL_SHA256 in the same file — no datasource covers codeload tarball hashes, so the sha256 is manual", "matchStrings": [ "ARG RESTIC_VERSION=(?\\S+)" ],