Selaa lähdekoodia

fix: drop container root; run api/ui as uid 1000 (SEC_REVIEW F18)

Both Dockerfiles now create an `app` system user (UID/GID 1000) and
switch to it via `USER app`. /app stays root-owned and world-readable
(partial mitigation for F20 — an RCE under `app` can't overwrite
source); /data is chown'd to app so SQLite writes,
`auth:bootstrap-service-token`, and phinx migrations succeed without
root. /home/app/.config and /home/app/.local/share are pre-created
and re-pointed via XDG_CONFIG_HOME / XDG_DATA_HOME so Caddy's
autosaved-config and TLS-cache state lands somewhere writable.

PHP sessions in the UI keep the default /tmp save path (world-
writable) so no extra mount is needed for the BFF session store.
FrankenPHP/Caddy bind to unprivileged ports (8080/8081); no setcap.

**Upgrade note.** Existing volumes from pre-F18 deployments were
created when /data was root-owned; the new uid=1000 process cannot
write to them and phinx fails with "attempt to write a readonly
database". Recover with one of:

  # one-shot chown (preserves data)
  docker run --rm -u 0 -v irdb_irdb-data:/data alpine \
      chown -R 1000:1000 /data
  # or, if the SQLite data is disposable
  docker compose down -v && docker compose up -d

Verified by rebuilding both images and confirming
`docker compose exec {api,ui} id` reports uid=1000(app), api healthz
returns 200, the UI Caddy log shows "serving initial configuration"
with TLS storage under /home/app/.local/share/caddy, and all 429
integration tests still pass under the new user.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
chiappa 4 päivää sitten
vanhempi
sitoutus
33179d8bba
2 muutettua tiedostoa jossa 34 lisäystä ja 3 poistoa
  1. 18 2
      api/Dockerfile
  2. 16 1
      ui/Dockerfile

+ 18 - 2
api/Dockerfile

@@ -33,9 +33,25 @@ COPY . ./
 # Caddyfile and entrypoint
 COPY docker/Caddyfile /etc/Caddyfile
 COPY docker/entrypoint.sh /usr/local/bin/entrypoint.sh
-RUN chmod +x /usr/local/bin/entrypoint.sh \
+
+# SEC_REVIEW F18: drop root. /app stays root-owned and world-readable
+# (the runtime only needs to read source); /data is app-owned so the
+# SQLite db, attached volume, and `auth:bootstrap-service-token`
+# writes succeed without root. /home/app/.{config,local/share} are
+# pre-created and owned so FrankenPHP/Caddy XDG state has somewhere
+# to land. apk + install-php-extensions still ran above as root
+# because they touch /usr/local; no further root-owned work remains.
+RUN addgroup -S -g 1000 app \
+    && adduser -S -u 1000 -G app -h /home/app app \
+    && chmod +x /usr/local/bin/entrypoint.sh \
     && chmod +x bin/console \
-    && mkdir -p /data
+    && mkdir -p /data /home/app/.config /home/app/.local/share \
+    && chown -R app:app /data /home/app
+
+ENV XDG_CONFIG_HOME=/home/app/.config \
+    XDG_DATA_HOME=/home/app/.local/share
+
+USER app
 
 EXPOSE 8081
 

+ 16 - 1
ui/Dockerfile

@@ -39,7 +39,22 @@ COPY --from=assets /app/public/assets ./public/assets
 
 COPY docker/Caddyfile /etc/Caddyfile
 COPY docker/entrypoint.sh /usr/local/bin/entrypoint.sh
-RUN chmod +x /usr/local/bin/entrypoint.sh
+
+# SEC_REVIEW F18: drop root. /app stays root-owned and world-readable;
+# the UI has no per-container persistent volume so only /home/app's
+# XDG dirs need to be app-writable. PHP sessions live under /tmp by
+# default which is world-writable, so no extra path setup is needed
+# for the BFF session store.
+RUN addgroup -S -g 1000 app \
+    && adduser -S -u 1000 -G app -h /home/app app \
+    && chmod +x /usr/local/bin/entrypoint.sh \
+    && mkdir -p /home/app/.config /home/app/.local/share \
+    && chown -R app:app /home/app
+
+ENV XDG_CONFIG_HOME=/home/app/.config \
+    XDG_DATA_HOME=/home/app/.local/share
+
+USER app
 
 EXPOSE 8080