refactor(docker): replace the init-perms container with a gosu privilege-drop in the app entrypoints#127
Merged
Conversation
…ege-drop in the api and worker entrypoints
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Description
Removes the one-shot
nojoin-init-permscontainer, which existed only tochownthe root-created./databind mount before the non-root app services could write to it. Because it gated the app services viadepends_on: condition: service_completed_successfully, Compose had to keep it around as an exited (orphaned) container after every successful start.Its single privileged operation now lives inside the app images' entrypoints: they enter as root only to repair
./dataownership, then immediately drop toappuser(uid 1000) viagosubefore the long-running process starts. This is the standard postgres/redis official-image pattern.Runtime behaviour is preserved — the api, worker, and worker-io processes still run as uid 1000. What changed is the image contract: the api and worker images now default to a root
USER(dropping at runtime) instead of declaringUSER appuser, so the release non-root gate was reworked to assert the dropped runtime uid rather than the static image-configUSER.New dependency:
gosu(from the distro package — Debian trixie1.17-3+b4, Ubuntu 24.041.17-1ubuntu0.24.04.3), chosen over a hand-copied static binary so it stays within Trivy's OS-package scanner coverage and the distro security auto-patch stream.Key changes
backend/entrypoint.sh,backend/worker_entrypoint.sh: root preamble (mkdir+ guarded recursivechown) thenexec gosu appuser "$0" "$@". Thestat -c %uguard skips the recursive walk when ownership is already correct — cheaper than the old container, which ranchown -Runconditionally on every boot.docker/Dockerfile.api,docker/Dockerfile.worker: installgosu; dropUSER appuserso the entrypoint starts as root. Worker also gains an explicitchmod +xon its entrypoint.docker/Dockerfile.worker-io: trailingUSER appuser->USER rootso the inherited worker entrypoint can perform the drop.docker-compose.example.yml: removed theinit-permsservice and its twodepends_ongates..github/workflows/release.yml: the non-root smoke now asserts runtime uid — frontend viaid -u(unchanged image), api via PID 1's real uid in/proc/1/status, worker by running the image through its entrypoint (docker run ... id -u). This is strictly stronger than the old image-configUSERcheck, at the cost of pulling the worker image.Type of change
Note: "breaking" refers to the image/compose contract (image default
USERis now root-with-runtime-drop; theinit-permsservice is gone). End-user runtime behaviour is unchanged.Checks run
python3 scripts/validate_docs.pypython3 scripts/validate_alembic.pyThe diff touches only Docker build files, entrypoint shell scripts, the release workflow, and docs — no Python, frontend, capture, or migration code — so the backend/frontend gates have nothing in scope to exercise. CI runs the full required set regardless. Additionally ran:
bash -non both entrypoints, YAML parse ofrelease.yml, anddocker compose configon the example compose.Migration impact
Documentation impact
docs/DEPLOYMENT.mdpre-publication verification,docs/SECURITY.mdworker-io non-root wording).Security impact
docs/SECURITY.mdboundaries preserved and updated. The runtime non-root property is unchanged (process is uid 1000); the images now enter root briefly by design solely to repair mount ownership, then drop viagosu. The release gate was updated to keep asserting the non-root property at runtime.Manual verification
gosuis apt-installable on both real bases (versions above).--user 1000-> no root phase, runs directly as 1000. All correct.docker compose configparses cleanly with no danglinginit-permsreferences.Pending before deploy (not blocking review): rebuild the real
nojoin-api/nojoin-worker/nojoin-worker-ioimages and boot them live on the homelab. The release pipeline's health + non-root smoke validates the drop on the real built images before any tag is published.