fix(docker): drop the postgres image's Go privilege-drop binary - #1714
Conversation
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>
|
Warning Review limit reachedNext included review available in 22 minutes. View limit detailsLimit 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. Review configuration: ⚙️ Run configurationConfiguration used: defaults Review profile: CHILL Plan: Team Run ID: 📒 Files selected for processing (3)
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. Comment |
There was a problem hiding this comment.
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.
📚 Documentation Preview
|
🧩 Storybook Preview
|
Closes #1713
What
gosuis a static Go binary the official postgres image bakes in for exactly one purpose: both upstream entrypoints (docker-entrypoint.sh,docker-ensure-initdb.sh) callgosu 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-groupsmakes 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 rebuiltgosuwould.Two details worth reviewing:
gosuleft all 22 findings reported (Number of language-specific files num=1). Deleting first writes a whiteout, and the scan then reportsnum=0. This is the whole difference between 22 rejected and 0.gosu <user> <command> [args...]. Auser:groupspec 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 inssl-cert— instead of trusting the script.Options considered
docker/postgres/Dockerfilealready floats onpostgres:${PG_MAJOR}-bookworm; a fresh pull today (sha256:1c59e2c3…) still shipsgosu 1.19 (go1.24.6 on linux/amd64; gc). There is no rebuild to wait for.gosuserver/compose.yamlbind-mounts./postgres-data, which Docker creates root-owned on the host; a non-root container cannot createPGDATAinside it, so every contributor'spnpm run devwould break.docker/preview/compose.app.yamllikewise documents the root step-down in itscap_addlist. The root path has to keep working.gosuagainst a current Go would clear today's 22 and start accruing tomorrow's;setprivmoves the privilege drop onto the OS package set thataptalready maintains.security/vulnerability-policy.jsonis untouched, and no entry expires into a re-cut.Vulnerability scans
Trivy
--severity HIGH,CRITICAL --scanners vulnon locally built images, evaluated throughevaluate()fromscripts/check-release-vulnerabilities.tsonci/scan-images-at-build-time(#1710) against this branch'ssecurity/vulnerability-policy.json, so the numbers are what the gate will say and not a raw count:All 22 rejected findings were
stdlib v1.24.6inusr/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.libexpat1Already 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 installs2.5.0-1+deb12u3, which fixes it. The fourlibexpat1findings 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'sapt-get updatealready picks it up, and #1709'sapt-get upgradekeeps 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 provesetprivdrops privileges under exactly the capability set the deployed stack grants:Fresh
initdbon an empty volumePID 1 is the server running as uid 999 — the step-down happened and
setprivexec'd rather than forked, so PostgreSQL keeps PID 1 and its signal handling.Restart on an existing data directory
docker stop→LOG: database system is shut downafter a complete checkpoint, so SIGTERM still reaches PID 1.Root-owned bind mount (the
server/compose.yamlshape, and the case that rules out option 2): container starts, chowns the directory, steps down,PGDATAinitialises at18/dockerowned by uid 999.Stand-in refuses what it does not implement
Overlap with #1709
#1709 adds
apt-get upgrade -yinside the existingRUNindocker/postgres/Dockerfile. This PR appends a newRUN+COPYbelow 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.libexpat1is covered under either.Quality gates
pnpm run formatthenpnpm run check— both pass.