From feefedfb835dc586ecdbfa797b7d23025956f917 Mon Sep 17 00:00:00 2001 From: Beer van der Drift Date: Mon, 2 Mar 2026 08:28:35 +0700 Subject: [PATCH] fix: allow docker cli container to connect to gateway (#12504) * Docker: route CLI through gateway network namespace * Tests: assert Docker Compose CLI namespace wiring * Changelog: add Docker Compose CLI connectivity fix * Docker: pin docker setup gateway mode and bind * Tests: cover docker setup mode and bind sync * Docs: clarify Docker LAN vs loopback gateway targeting * Changelog: expand Docker #12504 targeting note * Docker: default optional CLAUDE compose vars to empty * Docs(Docker): document non-interactive compose runs * Changelog: note docker compose env-noise reduction * Docker: restore onboarding Tailscale guidance * Docker: simplify onboarding output and clarify Tailscale * Docker: harden shared-namespace CLI container * Docs(Docker): document shared-namespace trust boundary * Changelog: note docker shared-namespace hardening --------- Co-authored-by: Vincent Koc --- CHANGELOG.md | 1 + docker-compose.yml | 20 ++++++++++++----- docker-setup.sh | 26 ++++++++++++++++------ docs/install/docker.md | 48 +++++++++++++++++++++++++++++++++++++++- src/docker-setup.test.ts | 9 ++++++++ 5 files changed, 90 insertions(+), 14 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index b58e2b56b65..1b260a3fd37 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -129,6 +129,7 @@ Docs: https://docs.openclaw.ai - Doctor/macOS state-dir safety: warn when OpenClaw state resolves inside iCloud Drive (`~/Library/Mobile Documents/com~apple~CloudDocs/...`) or `~/Library/CloudStorage/...`, because sync-backed paths can cause slower I/O and lock/sync races. (#31004) Thanks @vincentkoc. - Doctor/Linux state-dir safety: warn when OpenClaw state resolves to an `mmcblk*` mount source (SD or eMMC), because random I/O can be slower and media wear can increase under session and credential writes. (#31033) Thanks @vincentkoc. - CLI/Startup follow-up: add root `--help` fast-path bootstrap bypass with strict root-only matching, lazily resolve CLI channel options only when commands need them, merge build-time startup metadata (`dist/cli-startup-metadata.json`) with runtime catalog discovery so dynamic catalogs are preserved, and add low-power Linux doctor hints for compile-cache placement and respawn tuning. (#30975) Thanks @vincentkoc. +- Docker/Compose gateway targeting: run `openclaw-cli` in the `openclaw-gateway` service network namespace, require gateway startup ordering, pin Docker setup to `gateway.mode=local`, sync `gateway.bind` from `OPENCLAW_GATEWAY_BIND`, default optional `CLAUDE_*` compose vars to empty values to reduce automation warning noise, and harden `openclaw-cli` with `cap_drop` (`NET_RAW`, `NET_ADMIN`) + `no-new-privileges`. Docs now call out the shared trust boundary explicitly. (#12504) Thanks @bvanderdrift and @vincentkoc. - Telegram/Outbound API proxy env: keep the Node 22 `autoSelectFamily` global-dispatcher workaround while restoring env-proxy support by using `EnvHttpProxyAgent` so `HTTP_PROXY`/`HTTPS_PROXY` continue to apply to outbound requests. (#26207) Thanks @qsysbio-cjw for reporting and @rylena and @vincentkoc for work. - Browser/Security: fail closed on browser-control auth bootstrap errors; if auto-auth setup fails and no explicit token/password exists, browser control server startup now aborts instead of starting unauthenticated. This ships in the next npm release. Thanks @ijxpwastaken. - Sandbox/noVNC hardening: increase observer password entropy, shorten observer token lifetime, and replace noVNC token redirect with a bootstrap page that keeps credentials out of `Location` query strings and adds strict no-cache/no-referrer headers. diff --git a/docker-compose.yml b/docker-compose.yml index 614a1f8d533..843abb48f50 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -5,9 +5,9 @@ services: HOME: /home/node TERM: xterm-256color OPENCLAW_GATEWAY_TOKEN: ${OPENCLAW_GATEWAY_TOKEN} - CLAUDE_AI_SESSION_KEY: ${CLAUDE_AI_SESSION_KEY} - CLAUDE_WEB_SESSION_KEY: ${CLAUDE_WEB_SESSION_KEY} - CLAUDE_WEB_COOKIE: ${CLAUDE_WEB_COOKIE} + CLAUDE_AI_SESSION_KEY: ${CLAUDE_AI_SESSION_KEY:-} + CLAUDE_WEB_SESSION_KEY: ${CLAUDE_WEB_SESSION_KEY:-} + CLAUDE_WEB_COOKIE: ${CLAUDE_WEB_COOKIE:-} volumes: - ${OPENCLAW_CONFIG_DIR}:/home/node/.openclaw - ${OPENCLAW_WORKSPACE_DIR}:/home/node/.openclaw/workspace @@ -29,14 +29,20 @@ services: openclaw-cli: image: ${OPENCLAW_IMAGE:-openclaw:local} + network_mode: "service:openclaw-gateway" + cap_drop: + - NET_RAW + - NET_ADMIN + security_opt: + - no-new-privileges:true environment: HOME: /home/node TERM: xterm-256color OPENCLAW_GATEWAY_TOKEN: ${OPENCLAW_GATEWAY_TOKEN} BROWSER: echo - CLAUDE_AI_SESSION_KEY: ${CLAUDE_AI_SESSION_KEY} - CLAUDE_WEB_SESSION_KEY: ${CLAUDE_WEB_SESSION_KEY} - CLAUDE_WEB_COOKIE: ${CLAUDE_WEB_COOKIE} + CLAUDE_AI_SESSION_KEY: ${CLAUDE_AI_SESSION_KEY:-} + CLAUDE_WEB_SESSION_KEY: ${CLAUDE_WEB_SESSION_KEY:-} + CLAUDE_WEB_COOKIE: ${CLAUDE_WEB_COOKIE:-} volumes: - ${OPENCLAW_CONFIG_DIR}:/home/node/.openclaw - ${OPENCLAW_WORKSPACE_DIR}:/home/node/.openclaw/workspace @@ -44,3 +50,5 @@ services: tty: true init: true entrypoint: ["node", "dist/index.js"] + depends_on: + - openclaw-gateway diff --git a/docker-setup.sh b/docker-setup.sh index 1f6e51cd75d..a19573b84bc 100755 --- a/docker-setup.sh +++ b/docker-setup.sh @@ -92,6 +92,14 @@ ensure_control_ui_allowed_origins() { echo "Set gateway.controlUi.allowedOrigins to $allowed_origin_json for non-loopback bind." } +sync_gateway_mode_and_bind() { + docker compose "${COMPOSE_ARGS[@]}" run --rm openclaw-cli \ + config set gateway.mode local >/dev/null + docker compose "${COMPOSE_ARGS[@]}" run --rm openclaw-cli \ + config set gateway.bind "$OPENCLAW_GATEWAY_BIND" >/dev/null + echo "Pinned gateway.mode=local and gateway.bind=$OPENCLAW_GATEWAY_BIND for Docker setup." +} + contains_disallowed_chars() { local value="$1" [[ "$value" == *$'\n'* || "$value" == *$'\r'* || "$value" == *$'\t'* ]] @@ -340,14 +348,18 @@ fi echo "" echo "==> Onboarding (interactive)" -echo "When prompted:" -echo " - Gateway bind: lan" -echo " - Gateway auth: token" -echo " - Gateway token: $OPENCLAW_GATEWAY_TOKEN" -echo " - Tailscale exposure: Off" -echo " - Install Gateway daemon: No" +echo "Docker setup pins Gateway mode to local." +echo "Gateway runtime bind comes from OPENCLAW_GATEWAY_BIND (default: lan)." +echo "Current runtime bind: $OPENCLAW_GATEWAY_BIND" +echo "Gateway token: $OPENCLAW_GATEWAY_TOKEN" +echo "Tailscale exposure: Off (use host-level tailnet/Tailscale setup separately)." +echo "Install Gateway daemon: No (managed by Docker Compose)" echo "" -docker compose "${COMPOSE_ARGS[@]}" run --rm openclaw-cli onboard --no-install-daemon +docker compose "${COMPOSE_ARGS[@]}" run --rm openclaw-cli onboard --mode local --no-install-daemon + +echo "" +echo "==> Docker gateway defaults" +sync_gateway_mode_and_bind echo "" echo "==> Control UI origin allowlist" diff --git a/docs/install/docker.md b/docs/install/docker.md index 80ca9441568..ddc77b6ad80 100644 --- a/docs/install/docker.md +++ b/docs/install/docker.md @@ -59,6 +59,31 @@ After it finishes: - Paste the token into the Control UI (Settings → token). - Need the URL again? Run `docker compose run --rm openclaw-cli dashboard --no-open`. +### Automation/CI (non-interactive, no TTY noise) + +For scripts and CI, disable Compose pseudo-TTY allocation with `-T`: + +```bash +docker compose run -T --rm openclaw-cli gateway probe +docker compose run -T --rm openclaw-cli devices list --json +``` + +If your automation exports no Claude session vars, leaving them unset now resolves to +empty values by default in `docker-compose.yml` to avoid repeated "variable is not set" +warnings. + +### Shared-network security note (CLI + gateway) + +`openclaw-cli` uses `network_mode: "service:openclaw-gateway"` so CLI commands can +reliably reach the gateway over `127.0.0.1` in Docker. + +Treat this as a shared trust boundary: loopback binding is not isolation between these two +containers. If you need stronger separation, run commands from a separate container/host +network path instead of the bundled `openclaw-cli` service. + +To reduce impact if the CLI process is compromised, the compose config drops +`NET_RAW`/`NET_ADMIN` and enables `no-new-privileges` on `openclaw-cli`. + It writes config/workspace on the host: - `~/.openclaw/` @@ -322,9 +347,30 @@ scripts/e2e/onboard-docker.sh pnpm test:docker:qr ``` +### LAN vs loopback (Docker Compose) + +`docker-setup.sh` defaults `OPENCLAW_GATEWAY_BIND=lan` so host access to +`http://127.0.0.1:18789` works with Docker port publishing. + +- `lan` (default): host browser + host CLI can reach the published gateway port. +- `loopback`: only processes inside the container network namespace can reach + the gateway directly; host-published port access may fail. + +The setup script also pins `gateway.mode=local` after onboarding so Docker CLI +commands default to local loopback targeting. + +If you see `Gateway target: ws://172.x.x.x:18789` or repeated `pairing required` +errors from Docker CLI commands, run: + +```bash +docker compose run --rm openclaw-cli config set gateway.mode local +docker compose run --rm openclaw-cli config set gateway.bind lan +docker compose run --rm openclaw-cli devices list --url ws://127.0.0.1:18789 +``` + ### Notes -- Gateway bind defaults to `lan` for container use. +- Gateway bind defaults to `lan` for container use (`OPENCLAW_GATEWAY_BIND`). - Dockerfile CMD uses `--allow-unconfigured`; mounted config with `gateway.mode` not `local` will still start. Override CMD to enforce the guard. - The gateway container is the source of truth for sessions (`~/.openclaw/agents//sessions/`). diff --git a/src/docker-setup.test.ts b/src/docker-setup.test.ts index 8737ff5a793..f09f7c7b4e0 100644 --- a/src/docker-setup.test.ts +++ b/src/docker-setup.test.ts @@ -151,6 +151,9 @@ describe("docker-setup.sh", () => { expect(extraCompose).toContain("openclaw-home:"); const log = await readFile(activeSandbox.logPath, "utf8"); expect(log).toContain("--build-arg OPENCLAW_DOCKER_APT_PACKAGES=ffmpeg build-essential"); + expect(log).toContain("run --rm openclaw-cli onboard --mode local --no-install-daemon"); + expect(log).toContain("run --rm openclaw-cli config set gateway.mode local"); + expect(log).toContain("run --rm openclaw-cli config set gateway.bind lan"); }); it("precreates config identity dir for CLI device auth writes", async () => { @@ -253,4 +256,10 @@ describe("docker-setup.sh", () => { expect(compose).not.toContain("gateway-daemon"); expect(compose).toContain('"gateway"'); }); + + it("keeps docker-compose CLI network namespace settings in sync", async () => { + const compose = await readFile(join(repoRoot, "docker-compose.yml"), "utf8"); + expect(compose).toContain('network_mode: "service:openclaw-gateway"'); + expect(compose).toContain("depends_on:\n - openclaw-gateway"); + }); });