harden(compose): scope host-gateway route to the web app only (audit finding)

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 <noreply@anthropic.com>
feat/v1-foundation
boban 2026-07-06 01:15:35 +02:00
parent 4c67b278bd
commit 096ba3ca98
2 changed files with 12 additions and 9 deletions

View File

@ -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

View File

@ -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