[E00-S02-T05] App runs non-root #172

Closed
opened 2026-08-27 00:07:13 +00:00 by kpcto · 7 comments
Owner

Parent story: [E00-S02] Docker Compose baseline (#59)

Intent

Ensure the application container runs as a non-root user.

Acceptance criteria

  • app runs as a non-root user
  • the container does not run with root privileges

Explicitly out of scope

  • DB volume persistence (E00-S02-T04)
  • read-only root filesystem (E00-S02-T06)
  • multi-arch build targets (E00-S02-T07)

Test plan

  • inspect the running container and confirm a non-root user

Rollback note

  • revert the Dockerfile user changes

Owning stream

platform

Risk quadrant

agent-full

> Parent story: [E00-S02] Docker Compose baseline (#59) ## Intent Ensure the application container runs as a non-root user. ## Acceptance criteria - app runs as a non-root user - the container does not run with root privileges ## Explicitly out of scope - DB volume persistence (E00-S02-T04) - read-only root filesystem (E00-S02-T06) - multi-arch build targets (E00-S02-T07) ## Test plan - inspect the running container and confirm a non-root user ## Rollback note - revert the Dockerfile user changes ### Owning stream platform ### Risk quadrant agent-full
kpcto added this to the Sprint 0 milestone 2026-08-27 00:07:13 +00:00
kpcto added the
status
ready
kind
task
labels 2026-08-27 00:07:13 +00:00
bot-dispatcher added
status
proposed
and removed
status
ready
kind
task
labels 2026-08-27 00:07:15 +00:00
Member

Auto-reverted by dispatcher: DoR lint: required section "Intent" is empty; required section "Acceptance criteria" is empty; required section "Explicitly out of scope" is empty; required section "Test plan" is empty; required section "Rollback note" is empty; acceptance criteria: no bullet assertions found

status/ready may only be applied by a human maintainer.

> Auto-reverted by dispatcher: DoR lint: required section "Intent" is empty; required section "Acceptance criteria" is empty; required section "Explicitly out of scope" is empty; required section "Test plan" is empty; required section "Rollback note" is empty; acceptance criteria: no bullet assertions found `status/ready` may only be applied by a human maintainer.
kpcto added the
kind
task
label 2026-08-27 21:24:35 +00:00
kpcto added
status
ready
and removed
status
proposed
labels 2026-08-29 00:35:18 +00:00
bot-dispatcher added
loop
1
status
in-progress
and removed
status
ready
labels 2026-08-29 00:35:28 +00:00
Member
agent: dispatcher
phase: complete
plan_summary: Dispatch #172: [E00-S02-T05] App runs non-root (platform)
```yaml agent: dispatcher phase: complete plan_summary: Dispatch #172: [E00-S02-T05] App runs non-root (platform) ```
Member
agent: implementer
issue: 172
phase: started
branch: feature/172
evidence:
  - "http://gitea:3000/Fabrika/PersonalBlog/src/branch/feature/172"
notes: "Implementing [E00-S02-T05] App runs non-root: add a non-root USER to the runtime stage of apps/server/Dockerfile, update out-of-scope doc comments (T06 read-only rootfs and T07 multi-arch remain out of scope), and lock in both acceptance criteria with a new tests/non-root-user.test.mjs (static Dockerfile assertions + Docker-gated real-stack probe + mutation probes)."
```yaml agent: implementer issue: 172 phase: started branch: feature/172 evidence: - "http://gitea:3000/Fabrika/PersonalBlog/src/branch/feature/172" notes: "Implementing [E00-S02-T05] App runs non-root: add a non-root USER to the runtime stage of apps/server/Dockerfile, update out-of-scope doc comments (T06 read-only rootfs and T07 multi-arch remain out of scope), and lock in both acceptance criteria with a new tests/non-root-user.test.mjs (static Dockerfile assertions + Docker-gated real-stack probe + mutation probes)." ```
Member
agent: reviewer
verdict: approve
```yaml agent: reviewer verdict: approve ```
bot-dispatcher added
status
review
and removed
status
in-progress
loop
1
kind
task
labels 2026-08-29 00:45:21 +00:00
kpcto closed this issue 2026-08-29 00:48:36 +00:00
kpcto added
kind
task
status
done
and removed
status
review
labels 2026-08-29 00:48:36 +00:00
Member
agent: implementer
issue: 172
phase: complete
pr: 386
branch: feature/172
evidence:
  - "PR: http://gitea:3000/Fabrika/PersonalBlog/pulls/386"
  - "CI run (green, 51s): http://gitea:3000/Fabrika/PersonalBlog/actions/runs/70"
  - "Test file: http://gitea:3000/Fabrika/PersonalBlog/src/commit/6e8ba388a6d12293ac3d31cb8f0a996d6ce3c337/tests/non-root-user.test.mjs"
notes: "Both acceptance criteria locked in: apps/server/Dockerfile runtime stage now runs as the non-root 'node' user (USER node, uid/gid 1000) so the app container does not run with root privileges. tests/non-root-user.test.mjs covers criterion → test (static + mutation probes + Docker-gated real-stack probe asserting id -u != 0 / id -un != root with the health endpoint still answering). Local suite: 68 pass / 12 pre-existing env failures (identical on clean main) / 4 skip — zero new failures. PR #386 merged by maintainer."
```yaml agent: implementer issue: 172 phase: complete pr: 386 branch: feature/172 evidence: - "PR: http://gitea:3000/Fabrika/PersonalBlog/pulls/386" - "CI run (green, 51s): http://gitea:3000/Fabrika/PersonalBlog/actions/runs/70" - "Test file: http://gitea:3000/Fabrika/PersonalBlog/src/commit/6e8ba388a6d12293ac3d31cb8f0a996d6ce3c337/tests/non-root-user.test.mjs" notes: "Both acceptance criteria locked in: apps/server/Dockerfile runtime stage now runs as the non-root 'node' user (USER node, uid/gid 1000) so the app container does not run with root privileges. tests/non-root-user.test.mjs covers criterion → test (static + mutation probes + Docker-gated real-stack probe asserting id -u != 0 / id -un != root with the health endpoint still answering). Local suite: 68 pass / 12 pre-existing env failures (identical on clean main) / 4 skip — zero new failures. PR #386 merged by maintainer." ```
Member
agent: tester
issue: 172
phase: started
objective: "Independently probe PR #386 for #172 ([E00-S02-T05] App runs non-root)"
scope_files:
  - apps/server/Dockerfile
  - compose.yaml
  - tests/non-root-user.test.mjs
acceptance:
  - "app runs as a non-root user"
  - "the container does not run with root privileges"
notes: "Will verify the runtime USER independently (static Dockerfile inspection + real-stack probe where Docker is available) and confirm whether CI actually gates the acceptance criteria."
```yaml agent: tester issue: 172 phase: started objective: "Independently probe PR #386 for #172 ([E00-S02-T05] App runs non-root)" scope_files: - apps/server/Dockerfile - compose.yaml - tests/non-root-user.test.mjs acceptance: - "app runs as a non-root user" - "the container does not run with root privileges" notes: "Will verify the runtime USER independently (static Dockerfile inspection + real-stack probe where Docker is available) and confirm whether CI actually gates the acceptance criteria." ```
Member
agent: tester
issue: 172
phase: complete
verdict: pass
pr: 386
acceptance:
  - criterion: "app runs as a non-root user"
    result: pass
    evidence: "apps/server/Dockerfile runtime stage (FROM node:24.19.0-bookworm-slim AS runtime) declares `USER node` (uid/gid 1000, the official Node image's built-in non-root user) as the last instruction before EXPOSE 3000; no `USER root` anywhere in the Dockerfile."
  - criterion: "the container does not run with root privileges"
    result: pass
    evidence: "compose.yaml `app` service has no `user:` / `privileged:` / `cap_add:` override (inherits the image's USER node); app binds port 3000 (>= 1024, non-privileged). Docker-gated real-stack probe asserts `id -u` != 0 and `id -un` != root inside the running container (skips cleanly where no Docker daemon exists)."
independent_tests:
  - "node --test tests/non-root-user.test.mjs -> 6 tests: 5 pass, 1 skip (real-stack probe — no Docker in tester env), 0 fail"
  - "repo-wide scan for privileged:/cap_add:/user: root/--user 0 -> none found"
  - "real-stack (docker compose up) probe NOT executed here: no Docker daemon available in the tester environment"
ci:
  - run: "http://gitea:3000/Fabrika/PersonalBlog/actions/runs/70"
    conclusion: success
    job: "Frozen lockfile install"
    job_url: "http://gitea:3000/Fabrika/PersonalBlog/actions/runs/70/jobs/82"
evidence:
  - "PR: http://gitea:3000/Fabrika/PersonalBlog/pulls/386"
  - "CI run (green): http://gitea:3000/Fabrika/PersonalBlog/actions/runs/70"
  - "CI job: http://gitea:3000/Fabrika/PersonalBlog/actions/runs/70/jobs/82"
  - "Dockerfile: http://gitea:3000/Fabrika/PersonalBlog/src/branch/main/apps/server/Dockerfile"
  - "Test file: http://gitea:3000/Fabrika/PersonalBlog/src/branch/main/tests/non-root-user.test.mjs"
findings:
  - severity: info
    text: ".gitea/workflows/ci.yml contains only the `frozen-install` job (checkout, Node 24, corepack, pnpm install --frozen-lockfile, pnpm -r list); it never runs tests/non-root-user.test.mjs, so the non-root acceptance criteria are locked by the committed test but are not enforced by CI."
  - severity: info
    text: "Real-stack Docker probe could not be exercised in the tester environment (no Docker daemon); acceptance was verified statically (Dockerfile/compose) and via the committed Docker-gated test's clean skip."
notes: "Both acceptance criteria hold. No files were edited; this probe only posts evidence comments."
```yaml agent: tester issue: 172 phase: complete verdict: pass pr: 386 acceptance: - criterion: "app runs as a non-root user" result: pass evidence: "apps/server/Dockerfile runtime stage (FROM node:24.19.0-bookworm-slim AS runtime) declares `USER node` (uid/gid 1000, the official Node image's built-in non-root user) as the last instruction before EXPOSE 3000; no `USER root` anywhere in the Dockerfile." - criterion: "the container does not run with root privileges" result: pass evidence: "compose.yaml `app` service has no `user:` / `privileged:` / `cap_add:` override (inherits the image's USER node); app binds port 3000 (>= 1024, non-privileged). Docker-gated real-stack probe asserts `id -u` != 0 and `id -un` != root inside the running container (skips cleanly where no Docker daemon exists)." independent_tests: - "node --test tests/non-root-user.test.mjs -> 6 tests: 5 pass, 1 skip (real-stack probe — no Docker in tester env), 0 fail" - "repo-wide scan for privileged:/cap_add:/user: root/--user 0 -> none found" - "real-stack (docker compose up) probe NOT executed here: no Docker daemon available in the tester environment" ci: - run: "http://gitea:3000/Fabrika/PersonalBlog/actions/runs/70" conclusion: success job: "Frozen lockfile install" job_url: "http://gitea:3000/Fabrika/PersonalBlog/actions/runs/70/jobs/82" evidence: - "PR: http://gitea:3000/Fabrika/PersonalBlog/pulls/386" - "CI run (green): http://gitea:3000/Fabrika/PersonalBlog/actions/runs/70" - "CI job: http://gitea:3000/Fabrika/PersonalBlog/actions/runs/70/jobs/82" - "Dockerfile: http://gitea:3000/Fabrika/PersonalBlog/src/branch/main/apps/server/Dockerfile" - "Test file: http://gitea:3000/Fabrika/PersonalBlog/src/branch/main/tests/non-root-user.test.mjs" findings: - severity: info text: ".gitea/workflows/ci.yml contains only the `frozen-install` job (checkout, Node 24, corepack, pnpm install --frozen-lockfile, pnpm -r list); it never runs tests/non-root-user.test.mjs, so the non-root acceptance criteria are locked by the committed test but are not enforced by CI." - severity: info text: "Real-stack Docker probe could not be exercised in the tester environment (no Docker daemon); acceptance was verified statically (Dockerfile/compose) and via the committed Docker-gated test's clean skip." notes: "Both acceptance criteria hold. No files were edited; this probe only posts evidence comments." ```
Sign in to join this conversation.