[E00-S02-T05] App runs non-root #386
No Reviewers
Labels
Clear labels
agent/analyst-drafted
agent/analyst-drafted
needs/human-decision
needs/human-decision
needs/security-review
needs/security-review
tier/t0
tier/t1
tier/t2
tier/t3
kind
bug
kind
bug
kind
epic
kind
epic
kind
initiative
EPPP programme initiative
kind
story
kind
story
kind
task
EPPP engineering card/task decomposed from a story
kind
toil
kind
toil
loop
1
loop
1
loop
2
loop
2
loop
3
loop
3
risk
agent-full
risk
agent-full
risk
human-gated
risk
human-gated
risk
human-only
risk
human-only
size
l
size
l
size
m
size
m
size
s
size
s
status
blocked
status
blocked
status
done
Workflow: Done
status
in-progress
status
in-progress
status
proposed
status
proposed
status
ready
status
ready
status
review
status
review
stream
checkout
stream
checkout
stream
onboarding
stream
onboarding
stream
platform
stream
platform
trivial — implementer only, auto-merge
standard — implementer + reviewer + tester
complex — security if triggered, human merge
critical — full chain + security, human merge
No labels
Milestone
No items
No Milestone
Projects
Clear projects
No projects
No Assignees
Notifications
Due Date
No due date set.
Dependencies
No dependencies set.
Reference: Fabrika/PersonalBlog#386
Reference in New Issue
Block a user
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.
What changed
Implements the [E00-S02-T05] App runs non-root (#172) on top of the [E00-S02-T01..T04] Compose baseline (#59): the application container now runs as a non-root user and does not run with root privileges.
apps/server/Dockerfile— the runtime stage now drops root privileges withUSER node(the non-root user, uid/gid 1000, that the officialnode:24.19.0-bookworm-slimimage ships with). The server binds port 3000 (≥ 1024, no privileged port needed) and only reads the root-owned application files copied above, so no extra user creation or ownership changes are required.USERis the last instruction beforeEXPOSEso everyCOPYlands before the privilege drop. Header comment updated: T05 is in scope; T06 (read-only rootfs) and T07 (multi-arch) remain later tasks.compose.yaml— header doc-accuracy only: T05 is in scope (no Compose-leveluser:override is needed — the image'sUSERis inherited by the container); T06/T07 remain out of scope.tests/non-root-user.test.mjs(new) — locks in both acceptance criteria: a static assertion that the Dockerfile runtime stage declares a non-rootUSER(not root/uid 0,USER nodeexactly), non-vacuous mutation probes (removing the USER / switching it back to root / a runtime stage with no USER all fail), and a Docker-gated real-stack probe that starts the stack and assertsid -uandid -uninside the running app container report a non-root user, with the health endpoint still answering 200 as a regression guard (the privilege drop must not break startup). The probe cleans up withdocker compose downso runs stay isolated.Explicitly out of scope per the brief, not touched: DB volume persistence (E00-S02-T04 — unchanged behavior), read-only root filesystem (E00-S02-T06), multi-arch build targets (E00-S02-T07).
Criterion → test table
tests/non-root-user.test.mjs— "the app image runs as a non-root user (runtime stage declares a non-root USER)" (static: the Dockerfile runtime stage must declare aUSERinstruction naming a non-root user) and "the runtime USER is the image's built-in non-root node user (uid/gid 1000)" (locksUSER node). Mutation probes removing the USER or leaving a runtime stage with no USER both failtests/non-root-user.test.mjs— "the running app container does not run with root privileges (real-stack probe)" (Docker-gated): afterdocker compose up -d,id -uinside the running app container must not be0andid -unmust not reportroot; the health endpoint must still answer HTTP 200 unprivileged (regression guard). Static: the runtime USER must not be root/uid 0 (the "switching the runtime USER back to root" mutation probe fails)Test plan executed
node --test tests/non-root-user.test.mjs→ 5/5 pass, 1 skipped (the Docker-gated real-stack probe skips cleanly where no Docker daemon exists; this sandbox has no Docker) ✓node --test tests/*.test.mjs(full suite) → 68 pass / 12 fail / 4 skip, and the identical 12 failures exist on cleanmainin this sandbox (frozen-install / root-commands / strict-tsconfig / node-engine suites needpnpm install+ Node 24, which this environment lacks — nonode_modules, Node 22.23.2 here). My branch adds +5 passing tests and +1 Docker-gated skip, zero new failures (baseline: 63 pass / 12 fail / 3 skip) ✓docker compose up -d→ inspectidinside the container → health check →docker compose down) and is designed to run on CI/dev machines with Docker.Risks / notes
nodeuser (uid/gid 1000) rather than creating a dedicated user — minimal change, no extra packages, canonical per the Node image docs; the committed tests lockUSER nodeexplicitly, so a deliberate change of user would update the tests with it.skipwithout a daemon, so the suite stays green everywhere while giving real container-level validation where Docker exists (same pattern as T01..T04).Refs #172