fix(security): run frontend nginx container as non-root user - #683
Conversation
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
birme
left a comment
There was a problem hiding this comment.
code-reviewer (daily-backlog-pr Phase 3):
Code Review
Verdict: LGTM
Summary: A well-scoped, correctly reasoned security hardening change. The USER nginx directive works with the existing non-privileged port config, and every runtime-writable path is chowned to the nginx user before the switch. No application code is touched.
Blocking
None.
Non-root port binding verified: nginx/nginx.conf listens on 8080 (>1024) and scripts/entrypoint.sh defaults PORT to 3000 (also >1024), so no cap_net_bind_service is required. The pid file (/var/run/nginx.pid from the stock main config), /var/cache/nginx, and /var/log/nginx are all covered by the chown -R. The three entrypoint write targets (/app build output, /usr/share/nginx/html copy target, /etc/nginx/conf.d sed) are also chowned. npm hygiene is intact — the Dockerfile still uses npm ci and npm run build, no yarn reintroduced.
Warnings
Dockerfile:35(USER nginx) — Runtime footgun:entrypoint.shrewrites the listen port from thePORTenv var (sed -i "s/listen\s*8080;/listen $LISTENPORT;/"). If an operator setsPORTto a privileged value (<1024), the now-unprivileged nginx will fail to bind withpermission denied. The default is safe, but consider documenting thatPORTmust stay >=1024 (or add aNET_BIND_SERVICEcapability note) since previously root could bind anything.
Suggestions
- The stock official-image main
/etc/nginx/nginx.confstill contains a top-leveluser nginx;directive. When the master starts as a non-root user, nginx logs a benign warning:the "user" directive makes sense only if the master process runs with super-user privileges. Non-fatal — no action required, just expect it in logs. Dockerfile:26-34—chown -R nginx:nginx /apprecursively rewrites ownership of the fullnode_modulestree, which adds a large layer. Optional: scope the app chown to the build output dir if image size/build time matters. Not a correctness issue.
No WebRTC/SDP, state, or API-surface code is affected by this change.
birme
left a comment
There was a problem hiding this comment.
Code Review
Verdict: LGTM
Summary: A clean, well-scoped hardening change that runs the frontend nginx container as the unprivileged nginx user (uid 101). The port choice and runtime-write path ownership are handled correctly, so the container remains fully functional rootless. No Blocking items.
Blocking
None.
Warnings
None.
The core security concerns for this change all check out:
- Port binding —
nginx/nginx.conf:2listens on8080andscripts/entrypoint.sh:3defaultsPORTto3000. Both are >1024, so a non-root user can bind them withoutCAP_NET_BIND_SERVICEornet.ipv4.ip_unprivileged_port_starttweaks. No capability adjustment needed. Confirmed no privileged (<=1024) port is ever bound. - Runtime write paths — every path the entrypoint mutates at runtime is covered by
Dockerfile:28-35:/app(npm run build->/app/dist),/usr/share/nginx/html(cp -rof built assets,entrypoint.sh:14),/etc/nginx/conf.d(sed -ion default.conf,entrypoint.sh:4), plus nginx cache/log dirs and/var/run+/runfor the pid file. The chown list matches the actual write set. - User exists —
nginx:nginx(uid 101) ships with the officialnginx:1.29.0base image (Dockerfile:1), soUSER nginxon line 38 resolves without a precedinguseradd. - npm hygiene —
npm ci(Dockerfile:15) is used, no yarn reintroduced, nopackage.json/lockfile touched.
Suggestions
Dockerfile:38— The stock/etc/nginx/nginx.confstill containsuser nginx;andpid /var/run/nginx.pid;. When the master starts as non-root, nginx emits athe "user" directive makes sense only if the master process runs with super-user privilegeswarning to stderr on every boot. It is harmless (the worker already runs as the launching user) but noisy. Optional: strip theuserline from the main config to silence it. Not required for correctness.Dockerfile:28-35— Consider adding aUSER nginxsmoke check to CI (docker run --rm <img> idasserting uid=101 and a container-start test asserting nginx binds the port without EACCES), mirroring the PR's manual test-plan checkboxes. This locks the hardening in against future regressions.Dockerfile:5— Pre-existing (outside this diff):EXPOSE $MANAGER_URLis not a valid EXPOSE argument (EXPOSE expects a port, not a URL). Not introduced by this PR; flagging only for awareness.
No application code, WebRTC/SDP, state-management, or test files are touched by this PR, so categories 1, 2, 4, 5, and 7 are not applicable.
Code ReviewVerdict: LGTM Summary: Adds Reviewed against the Open Intercom |
Summary
USER nginxdirective to the frontendDockerfileso the nginx master, workers, and entrypoint no longer run asroot, reducing the blast radius of a container escape or web server vulnerability.chown -R nginx:nginxevery path written at runtime —/app(npm run buildoutput),/usr/share/nginx/html(built assets copied by the entrypoint),/etc/nginx/conf.d(entrypointsed -i), plus nginx's cache, log, and pid dirs — so the unprivileged user can still serve traffic.nginx/nginx.confalready listens on8080(defaultPORT), so the process runs fully rootless without binding port 80.Dockerfileonly; no application code touched.Test plan
npm test)npm run typecheck)npm run lint)docker build .succeedsidreporting a non-root uid (nginx, uid 101)8080and writes its pid/cache/logs without permission errorsCloses #648
🤖 Generated with Claude Code
Co-Authored-By: Claude Sonnet 4.6 noreply@anthropic.com