Przeglądaj źródła

fix: mount api/ui rootfs read-only at runtime (SEC_REVIEW F20)

F18 made /app root-owned so the unprivileged `app` user cannot
write to it via standard perms; F20 closes the gap further by
mounting the entire rootfs read-only at the kernel level. Now even
a kernel-bypass / capability-escalation primitive (or a future
regression that flips ownership) cannot land a persistent payload
into /app/vendor, /app/src, or /app/public/index.php.

Each service (migrate, api, ui) gets `read_only: true` plus tmpfs
mounts for the few paths that actually need writes:

  - /tmp                          PHP scratch / sessions
  - /home/app/.config             Caddy autosaved-config (XDG)
  - /home/app/.local/share        FrankenPHP/Caddy state (XDG)

All three tmpfs mounts are owned by uid=1000 so the unprivileged
runtime user can write to them. /data on api/migrate stays as the
named `irdb-data` volume — phinx, the SQLite db, and the geoip
mmdb files all live there.

Verification: brought the stack up clean (`docker compose up -d`).
phinx ran all 22 migrations; api and ui reached healthy; both
/healthz endpoints returned 200; `touch /app/...` and friends
returned `Read-only file system`; /tmp, /home/app/.config,
/home/app/.local/share, and /data accepted writes.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
chiappa 5 dni temu
rodzic
commit
1ec9d04008
1 zmienionych plików z 24 dodań i 0 usunięć
  1. 24 0
      docker-compose.yml

+ 24 - 0
docker-compose.yml

@@ -4,6 +4,16 @@ services:
     build: { context: ./api }
     command: migrate
     env_file: .env
+    # SEC_REVIEW F20: rootfs read-only — /app source cannot be overwritten
+    # by an RCE. Writable paths are restricted to the irdb-data volume
+    # plus a few small tmpfs mounts (PHP `/tmp`, Caddy/FrankenPHP XDG
+    # state). These tmpfs mounts inherit uid=1000 so the unprivileged
+    # `app` user can write to them.
+    read_only: true
+    tmpfs:
+      - /tmp:uid=1000,gid=1000,mode=1777
+      - /home/app/.config:uid=1000,gid=1000,mode=0700
+      - /home/app/.local/share:uid=1000,gid=1000,mode=0700
     volumes:
       - irdb-data:/data
     restart: "no"
@@ -12,6 +22,12 @@ services:
     image: irdb-api:latest
     command: api
     env_file: .env
+    # SEC_REVIEW F20: see migrate above.
+    read_only: true
+    tmpfs:
+      - /tmp:uid=1000,gid=1000,mode=1777
+      - /home/app/.config:uid=1000,gid=1000,mode=0700
+      - /home/app/.local/share:uid=1000,gid=1000,mode=0700
     ports:
       - "8081:8081"
     volumes:
@@ -30,6 +46,14 @@ services:
     image: irdb-ui:latest
     build: { context: ./ui }
     env_file: .env
+    # SEC_REVIEW F20: rootfs read-only. The ui has no /data volume —
+    # all writes (PHP sessions in /tmp, Caddy/FrankenPHP XDG state) go
+    # to ephemeral tmpfs mounts owned by uid=1000.
+    read_only: true
+    tmpfs:
+      - /tmp:uid=1000,gid=1000,mode=1777
+      - /home/app/.config:uid=1000,gid=1000,mode=0700
+      - /home/app/.local/share:uid=1000,gid=1000,mode=0700
     ports:
       - "8080:8080"
     depends_on: