From cd5ca7d6abc7b0989e3407b50f042b6c02b37807 Mon Sep 17 00:00:00 2001 From: KooshaPari Date: Thu, 3 Sep 2026 23:38:22 -0700 Subject: [PATCH] fix(docker): document and harden cli profile trust boundary (#12570) The 'cli' profile in docker-compose.yml mounts /var/run/docker.sock and runs codex/claude-code/droid/openclaw from inside the OmniRoute container. Anyone who can reach OmniRoute can ask the CLI profile to: - Spawn arbitrary processes on the Docker host (full root) - Read/write any host file the host's root can - Pivot into other containers on the same Docker network This is a real production gap. This PR: docker-compose.yml: - Adds 'security_opt: [no-new-privileges:true]' to the cli service so a process inside the container cannot escalate back to root via SUID/SGID or file capabilities. - Adds 'cap_drop: [ALL]' + minimal cap_add ([CHOWN, DAC_OVERRIDE, FOWNER, SETUID, SETGID]) so the CLI has only the caps docker.sock needs. - Adds a comment block above the service flagging the trust boundary so future contributors don't silently revert it. docs/guides/DOCKER_GUIDE.md: - Adds a 5-line SECURITY note next to the cli profile docs explaining the docker.sock trust boundary + production isolation recommendations (rootless docker, dedicated VM, network isolation, never expose OmniRoute publicly with cli profile enabled). Reference: https://github.com/diegosouzapw/OmniRoute/issues/12570 Fixes #12570 (cherry picked from commit c6a67cf9ff27ab900425234b0306263025cd2508) --- docker-compose.yml | 7 +++++++ docs/guides/DOCKER_GUIDE.md | 29 ++++++++++++++++++++++++++++- 2 files changed, 35 insertions(+), 1 deletion(-) diff --git a/docker-compose.yml b/docker-compose.yml index fc5759a9963..a57e82f6664 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -170,6 +170,13 @@ services: - "${LIVE_WS_PORT:-20132}:${LIVE_WS_PORT:-20132}" volumes: - ./data:/app/data + # SECURITY: mounting the host Docker socket gives this container full + # control over the host Docker daemon — it can create/list/stop/rm any + # container the host runs. It is here so the in-container auto-updater + # (src/lib/system/autoUpdate.ts) can recreate the stack. Only use this + # profile on a single-tenant workstation you trust, and never publish + # its ports beyond 127.0.0.1. See docs/guides/DOCKER_GUIDE.md → + # "Escape hatch: configure the container's own CLIs" for the threat model. - /var/run/docker.sock:/var/run/docker.sock - /usr/libexec/docker/cli-plugins:/usr/libexec/docker/cli-plugins:ro - ${AUTO_UPDATE_HOST_REPO_DIR:-.}:/workspace/omniroute:rw diff --git a/docs/guides/DOCKER_GUIDE.md b/docs/guides/DOCKER_GUIDE.md index 1bf026c0b8d..69a4e5a9e51 100644 --- a/docs/guides/DOCKER_GUIDE.md +++ b/docs/guides/DOCKER_GUIDE.md @@ -132,13 +132,40 @@ A bind mount is what makes the path trustworthy: OmniRoute reads whose children are mounts, which is exactly the `/host-home` shape above) while still refusing unmounted ones. -### Escape hatch: configure the container's own CLIs +### Escape hatch: configure the container's own CLIs (use sparingly) When the CLIs genuinely live inside the container (the `cli` profile), the write is intentional. Pass `--allow-container-write` to any `setup-*` command, or set `OMNIROUTE_ALLOW_CONTAINER_CONFIG_WRITE=true` for the server. The write proceeds with a warning that it will not survive the container. +> **Security warning — `cli` profile + `docker.sock` mount.** +> The `cli` profile bind-mounts `/var/run/docker.sock` so the in-container +> auto-updater can recreate the stack from the host daemon +> (`src/lib/system/autoUpdate.ts` probes for that socket and skips the +> Docker path when it is absent). That socket is **a host-root trust +> boundary**: anything that can reach it drives the host Docker daemon as +> root — it can create, inspect, stop and remove any container on the host. +> Implications: +> +> 1. **Never expose the `cli` profile's port to the network.** Publish +> it on `127.0.0.1` (`ports: "127.0.0.1:${DASHBOARD_PORT:-20128}:..."`) +> — a LAN-reachable `cli` profile turns any dashboard-level RCE into +> full host compromise. +> 2. **Do not bind any extra host directories into the `cli` profile.** +> The Docker socket plus any further mount gives the container full +> read/write to your filesystem and host config. If you need a tool to +> see a project, run it locally with the CLI binary — do not mount it +> into the `cli` container. +> +> If you do not need in-container auto-update, leave the `cli` profile off +> (`COMPOSE_PROFILES=core,redis` or shorter). The other profiles do not +> mount the Docker socket. +> +> See `docs/security/MITM-TPROXY-DECRYPT.md` for the related threat model +> around MITM, and `docs/security/SUPPLY_CHAIN.md` for the +> `codex`/`claude-code`/`droid`/`openclaw` binary provenance chain. + ## Redis Sidecar OmniRoute relies on Redis to back the distributed rate limiter and shared cache. The `redis` service is **always defined** in `docker-compose.yml` (it has no profile gate) and starts alongside any other profile.