From 096ba3ca98a074de5e40734fc89905c80e51a852 Mon Sep 17 00:00:00 2001 From: boban Date: Mon, 6 Jul 2026 01:15:35 +0200 Subject: [PATCH] harden(compose): scope host-gateway route to the web app only (audit finding) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A multi-agent security audit of this branch flagged that the new `extra_hosts: host.docker.internal:host-gateway` sat on the shared x-app/x-image anchor, so reverb/queue/scheduler inherited it too. Only the web `app` container serves the (admin-gated) "Clusev host" Docker view; the workers run background code with no role gate. Combined with the image's ability to decrypt the root HostCredential and open root SSH via HostCredential::toServer()+FleetService, every worker held a network path to a root shell on the host — defeating the manage-fleet gate that only exists in the HTTP layer. Not reachable as shipped (no job/scheduled code touches the host credential), but a least-privilege regression. Move extra_hosts off the anchor onto ONLY the `app` service (dev + prod). Verified: app resolves host.docker.internal, queue/scheduler do not. Co-Authored-By: Claude Fable 5 --- docker-compose.prod.yml | 10 ++++++---- docker-compose.yml | 11 ++++++----- 2 files changed, 12 insertions(+), 9 deletions(-) diff --git a/docker-compose.prod.yml b/docker-compose.prod.yml index 67bd436..9324d0b 100644 --- a/docker-compose.prod.yml +++ b/docker-compose.prod.yml @@ -32,14 +32,16 @@ x-image: &image security_opt: - "no-new-privileges:true" pids_limit: 1024 - # Reach the Docker host's sshd from the PHP layer so the "Clusev host" Docker view (and any other - # host-targeted SSH) can talk to the real machine — same mapping the terminal sidecar uses. - extra_hosts: - - "host.docker.internal:host-gateway" services: app: <<: *image + # ONLY the web container serves the (admin-gated) "Clusev host" Docker view, so ONLY it gets a + # route to the host gateway. Keeping it off the reverb/queue/scheduler workers (background code, + # not role-gated HTTP) denies them a network path to a root shell on the host if compromised — + # least privilege for the stored root HostCredential. + extra_hosts: + - "host.docker.internal:host-gateway" build: context: . dockerfile: Dockerfile diff --git a/docker-compose.yml b/docker-compose.yml index 97dd90e..f2ea1bd 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -12,11 +12,6 @@ x-app: &app APP_GID: "${HOST_GID:-1002}" image: clusev-app:dev restart: unless-stopped - # Reach the Docker host's sshd from the PHP layer so the "Clusev host" Docker view (and any - # other host-targeted SSH) can talk to the real machine (host.docker.internal → host gateway), - # the same mapping the terminal sidecar already uses. - extra_hosts: - - "host.docker.internal:host-gateway" volumes: - .:/var/www/html - ./run:/var/www/html/storage/app/restart-signal @@ -34,6 +29,12 @@ services: ports: - "${APP_PORT:-80}:80" - "${VITE_PORT:-5173}:5173" + # ONLY the web container serves the (admin-gated) "Clusev host" Docker view, so ONLY it gets a + # route to the host gateway. Keeping it off the reverb/queue/scheduler workers (which run + # background code, not role-gated HTTP) denies them a network path to a root shell on the host + # if one were ever compromised — least privilege for the stored root HostCredential. + extra_hosts: + - "host.docker.internal:host-gateway" depends_on: mariadb: condition: service_healthy