Skip to content

fix(docker): drop the postgres image's Go privilege-drop binary - #1714

Merged
FelixTJDietrich merged 1 commit into
mainfrom
fix/postgres-gosu-cves
Sep 1, 2026
Merged

fix(docker): drop the postgres image's Go privilege-drop binary#1714
FelixTJDietrich merged 1 commit into
mainfrom
fix/postgres-gosu-cves

Conversation

@FelixTJDietrich

Copy link
Copy Markdown
Collaborator

Closes #1713

What

gosu is a static Go binary the official postgres image bakes in for exactly one purpose: both upstream entrypoints (docker-entrypoint.sh, docker-ensure-initdb.sh) call gosu postgres "$BASH_SOURCE" "$@" once, as root, to step down to the postgres user. apt cannot reach it, so its Go stdlib CVEs cannot leave the image while the binary is in it.

The image now deletes the binary and puts a setpriv (util-linux) stand-in at the same path. setpriv --reuid postgres --regid <gid> --init-groups makes the identical switch and then execs, and it is an ordinary Debian package the OS updates keep patched — so these findings do not come back on the next rebuild the way a freshly rebuilt gosu would.

Two details worth reviewing:

  • The removal is its own layer. Trivy reads a path a later layer merely overwrote as the file the lower layer put there. Copying the stand-in over gosu left all 22 findings reported (Number of language-specific files num=1). Deleting first writes a whiteout, and the scan then reports num=0. This is the whole difference between 22 rejected and 0.
  • The stand-in only accepts gosu <user> <command> [args...]. A user:group spec or an option exits 1 rather than guessing at gosu's semantics and running a command with privileges nobody asked for. The build asserts the resulting identity — uid, primary gid and supplementary groups, since postgres is also in ssl-cert — instead of trusting the script.

Options considered

# Option Outcome
1 Newer upstream base No. docker/postgres/Dockerfile already floats on postgres:${PG_MAJOR}-bookworm; a fresh pull today (sha256:1c59e2c3…) still ships gosu 1.19 (go1.24.6 on linux/amd64; gc). There is no rebuild to wait for.
2 Run as the postgres user and delete gosu Measured and rejected. server/compose.yaml bind-mounts ./postgres-data, which Docker creates root-owned on the host; a non-root container cannot create PGDATA inside it, so every contributor's pnpm run dev would break. docker/preview/compose.app.yaml likewise documents the root step-down in its cap_add list. The root path has to keep working.
3 Replace the binary Taken, in the form that ends the problem. Rebuilding gosu against a current Go would clear today's 22 and start accruing tomorrow's; setpriv moves the privilege drop onto the OS package set that apt already maintains.
4 Time-boxed exceptions Not needed. security/vulnerability-policy.json is untouched, and no entry expires into a re-cut.

Vulnerability scans

Trivy --severity HIGH,CRITICAL --scanners vuln on locally built images, evaluated through evaluate() from scripts/check-release-vulnerabilities.ts on ci/scan-images-at-build-time (#1710) against this branch's security/vulnerability-policy.json, so the numbers are what the gate will say and not a raw count:

HIGH/CRITICAL Rejected
before 98 22
after 76 0

All 22 rejected findings were stdlib v1.24.6 in usr/local/bin/gosu (CVE-2025-68121 CRITICAL, 21 HIGH). The remaining 76 are unfixed in Debian 12 (affected / will_not_fix), which the gate does not reject.

libexpat1

Already resolved, and not by this PR. #1710 measured libexpat1 2.5.0-1+deb12u2 / CVE-2026-56408 as rejected; a build against today's index installs 2.5.0-1+deb12u3, which fixes it. The four libexpat1 findings that remain (CVE-2025-59375, CVE-2026-25210, CVE-2026-45186, CVE-2026-66046) all carry no fix in Debian 12, so none is rejected. Nothing to do here — the Dockerfile's apt-get update already picks it up, and #1709's apt-get upgrade keeps it that way.

Verification

Built and smoke-tested the real image. The fresh-init run used the preview stack's confinement (--security-opt no-new-privileges:true, --cap-drop ALL, --cap-add CHOWN,DAC_OVERRIDE,FOWNER,SETGID,SETUID) to prove setpriv drops privileges under exactly the capability set the deployed stack grants:

Fresh initdb on an empty volume

PostgreSQL 18.6 (Debian 18.6-1.pgdg12+2) on x86_64-pc-linux-gnu
CREATE EXTENSION pg_partman -> 5.5.0
/proc/1/status: Name: postgres   Uid: 999 999 999 999   Gid: 999 999 999 999
LOG:  database system is ready to accept connections

PID 1 is the server running as uid 999 — the step-down happened and setpriv exec'd rather than forked, so PostgreSQL keeps PID 1 and its signal handling.

Restart on an existing data directory

PostgreSQL Database directory appears to contain a database; Skipping initialization
LOG:  database system is ready to accept connections
pg_partman still 5.5.0

docker stopLOG: database system is shut down after a complete checkpoint, so SIGTERM still reaches PID 1.

Root-owned bind mount (the server/compose.yaml shape, and the case that rules out option 2): container starts, chowns the directory, steps down, PGDATA initialises at 18/docker owned by uid 999.

Stand-in refuses what it does not implement

gosu postgres id -u        -> 999
gosu --version             -> usage, rc=1
gosu postgres:postgres id  -> usage, rc=1
gosu postgres              -> usage, rc=1

Overlap with #1709

#1709 adds apt-get upgrade -y inside the existing RUN in docker/postgres/Dockerfile. This PR appends a new RUN + COPY below that block and does not touch it, so the two should merge without a conflict; whichever lands second needs no rework. They are complementary: #1709 keeps the OS packages patched, which is now the mechanism that keeps the privilege drop patched too. libexpat1 is covered under either.

Quality gates

pnpm run format then pnpm run check — both pass.

The upstream postgres image bakes in gosu, a static Go binary the entrypoints
run once as root to step down to the postgres user. apt cannot reach it, so its
Go stdlib CVEs — 22 HIGH/CRITICAL against Go 1.24.6, all of them fixable and
therefore all rejected by the release vulnerability policy — cannot be patched
out of the image. The current postgres:18-bookworm still carries gosu 1.19 built
against that toolchain, so waiting for a docker-library rebuild is not a fix.

setpriv (util-linux) performs the identical uid/gid/supplementary-group switch
and execs, and is an ordinary Debian package that OS updates keep patched. The
binary is deleted in its own layer, which writes a whiteout, before the stand-in
is copied over the same path: a scanner reads a merely-overwritten path as the
file the lower layer put there, so an overwrite alone leaves the findings.

Running the container as the postgres user instead was measured and rejected:
the local development stack bind-mounts server/postgres-data, which Docker
creates root-owned, so a non-root container cannot initialise it.

Closes #1713

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@coderabbitai

coderabbitai Bot commented Sep 1, 2026

Copy link
Copy Markdown
Contributor

Warning

Review limit reached

Next included review available in 22 minutes.

Check out review usage here.

View limit details

Limit details: You’ve used the included review currently available.

You've used all free OSS reviews for now. Wait for the free limit to reset to keep reviewing this public repository.

Learn how review limits work.

Review configuration:

⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Team

Run ID: 9b00302c-9861-48bf-88d7-b01a82a5b63b

📥 Commits

Reviewing files that changed from the base of the PR and between 33f30c2 and 516933d.

📒 Files selected for processing (3)
  • .changeset/postgres-image-sheds-its-go-helper.md
  • docker/postgres/Dockerfile
  • docker/postgres/gosu-setpriv.sh

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

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

@github-actions github-actions Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Approved automatically: @FelixTJDietrich is listed in the REVIEW_POLICY_MAINTAINERS repository variable, which the repository treats as satisfying the review requirement. See the review policy in docs/contributor/ci-cd.mdx.

@github-project-automation github-project-automation Bot moved this from Backlog to In Review in Hephaestus Sep 1, 2026
@github-actions github-actions Bot added bug Something isn't working ci GitHub Actions, workflows, build pipeline changes infrastructure Docker, containers, and deployment infrastructure size:M This PR changes 30-99 lines, ignoring generated files. labels Sep 1, 2026
@FelixTJDietrich
FelixTJDietrich added this pull request to the merge queue Sep 1, 2026
Merged via the queue into main with commit 6de54b4 Sep 1, 2026
30 checks passed
@FelixTJDietrich
FelixTJDietrich deleted the fix/postgres-gosu-cves branch September 1, 2026 15:11
@github-project-automation github-project-automation Bot moved this from In Review to Done in Hephaestus Sep 1, 2026
@github-actions

github-actions Bot commented Sep 1, 2026

Copy link
Copy Markdown
Contributor

📚 Documentation Preview

Preview has been removed (PR closed)

@github-actions

github-actions Bot commented Sep 1, 2026

Copy link
Copy Markdown
Contributor

🧩 Storybook Preview

Preview has been removed (PR closed)

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

Labels

bug Something isn't working ci GitHub Actions, workflows, build pipeline changes infrastructure Docker, containers, and deployment infrastructure size:M This PR changes 30-99 lines, ignoring generated files.

Projects

Status: Done

Development

Successfully merging this pull request may close these issues.

fix(docker): resolve the Go stdlib CVEs in the postgres image's gosu binary

1 participant