Secure OpenClaw — Gateway + SSH Sandbox
10K+
Combine OpenClaw with Security and Easiness! Run out of the box a secure docker based sandboxed OpenClaw, locally or in a cloud.
It has never been so easy to run a secure sandboxed pre-configured OpenClaw!:
.envnpm starthttp://localhost:18789/Target audience: Security aware developer with some basic docker know how. Everybody else: Keep your hands away from OpenClaw!
All features are listed in FEATURES.md, all tests in TESTS.md. The sandbox builds on mwaeckerlin/sandbox-base; docker-in-docker runs the rootless mwaeckerlin/dockindock, which needs no host configuration.
The primary security mechanism is strict isolation: The AI runs in a dedicated sandbox container that contains only its tools and workspace — no host secrets, no production data, no unrelated resources.
no-new-privileges, pids_limit: 256 against escalation and fork bombsnetworks.<network>.driver_opts.encrypted: "true", or add a service mesh)127.0.0.1 by default (override with OPENCLAW_GATEWAY_BIND_ADDRESS) for local testing only; internal ports stay internal. If you attach chat tool, such as Telegram, you can even close that port. You can then reach your OpenClaw through Telegram. Do not expose 18789 to the Internet without further protection. You may add e.g. Traefik service and an Authentik proxy-provider outpost in front of OpenClaw when you want to access it through the internet.Note: If networks are neither seggregated nor encrypted, the agent can sniff for secrets on the shared or unencrypted network. So network isolation is crucial, and encryption is highly recommended at least in production.
/var/secrets/secret-name and are then exported to the OpenClaw environment variables as SECRET_NAME.docker:dind) where the agent can run docker in an isolated installation, seggregated from your docker installation. Be aware that the agent can gain root, but only in tis isolated container. Just restart the container to restore in case of a break out. No data is in danger.openclaw CLI commands. It helps for self analysis and allows to setup cron jobs. Only the MCP server holds the gateway token, the sandbox has no access to the token.tools.fs.workspaceOnly: true) — File tools limited to the sandbox workspace.workspaceOnly setting restricts OpenClaw's file tools to the workspace. However, exec/shell commands can still read container system files (e.g. /etc/passwd, /proc). This is acceptable because the sandbox is an isolated container — there are no host secrets inside it.loopDetection) — Circuit breaker against tool/agent loops. That's more to prevent token over spending.strictHostKeyChecking: falseAcceptable in a controlled internal Docker network where DNS is managed by Docker. For production hardening, consider pinning host keys.
These defaults trade security for local out-of-the-box usability. All are overridable via environment variables; review them before any non-local deployment:
allowInsecureAuth, dangerouslyAllowHostHeaderOriginFallback and dangerouslyDisableDeviceAuth default to true (OPENCLAW_CONTROL_UI_ALLOW_INSECURE_AUTH, OPENCLAW_CONTROL_UI_ALLOW_HOST_HEADER_ORIGIN_FALLBACK, OPENCLAW_CONTROL_UI_DISABLE_DEVICE_AUTH). This lets the token-auth UI work over plain HTTP on loopback without device pairing. Behind a public reverse proxy, set all three to false and configure OPENCLAW_ALLOWED_ORIGINS_JSON.permissionMode: approve-all — the gateway-side GitHub/Gitea MCP servers auto-approve all tool calls; the effective permission boundary is the scope of the token you provide (OPENCLAW_GITHUB_TOKEN/OPENCLAW_GITEA_TOKEN). Use minimal-scope tokens.dmPolicy: pairing (unknown peers must be approved before the agent reacts); Telegram groups require an explicit mention by default. Loosening this (e.g. OPENCLAW_TELEGRAM_DM_POLICY=open) means anyone who finds your bot can drive the agent.DOCKER_TLS_CERTDIR: "") — the isolated Docker daemon listens unauthenticated, but only on the segregated sandbox-dind network where the sandbox is root-equivalent by design (see the DinD security warning below).
@startuml architecture
actor User as user
cloud docker {
node "mwaeckerlin/openclaw:gateway" as gw {
[Gateway] as ctrl
storage "openclaw-config" as cfg
ctrl - cfg
}
node "mwaeckerlin/openclaw-mcp-gateway" {
[MCP OpenClaw Server] as mcp
}
node "mwaeckerlin/openclaw:sandbox" as sb {
[Sandbox] as sshd
storage "openclaw-workspace" as ws
sshd -right- ws
}
node "openclaw-dind" as dind {
[Docker] as dd
storage "openclaw-docker" as dv
dd -left- dv
}
node "mwaeckerlin/mcp-github" {
[Github-Gateway] as gh
}
component "allow-write-access" as aw
}
user --> ctrl : "HTTP"
ctrl --> sshd : "SSH"
sshd --up--> mcp : openclaw\ncommands
mcp --up--> ctrl : forward\ncommands
sshd -left-> dd : docker
aw .up.> cfg : chown
sshd --> gh
gh ----> [GitHib]
@enduml
For local testing with docker compose and .env file.
Simplest use is with an OpenAI token that you store in OPENAI_API_KEY. All other secrets can just be randomly generated:
(umask 077
ssh-keygen -t ed25519 -f openclaw-key -N "" -C "openclaw-sandbox"
cat > .env <<EOF
OPENCLAW_GATEWAY_TOKEN=$(pwgen 40 1)
OPENCLAW_SANDBOX_SSH_PUBLIC_KEY=$(cat openclaw-key.pub)
OPENCLAW_SANDBOX_SSH_PRIVATE_KEY=$(sed -z 's/\n/\\n/g' openclaw-key)
OPENAI_API_KEY=sk-...[PLACE-TOKEN-HERE]
EOF
rm openclaw-key openclaw-key.pub)
The umask 077 keeps .env readable only by you — it contains all secrets.
If you use the MCP gateway (enabled by default), generate a device keypair for secure gateway-to-MCP communication:
node generate-device-pairing.mjs
This appends OPENCLAW_DEVICE_IDENTITY and OPENCLAW_DEVICE_PAIRING to .env. The MCP gateway uses the private key to authenticate, and the OpenClaw gateway pre-registers the public key so the device is trusted on first connect.
Use --stdout to print the values instead of writing to .env.
In foreground (see logs in real-time):
npm start
In background (daemon mode):
npm run start:daemon
Control UI: http://localhost:18789/
This is for local / trusted-network use only. The gateway token is transmitted unencrypted. The port is bound to 127.0.0.1 by default; do not expose it to the internet without a TLS reverse proxy.
npm test
Runs the unit tests for the configuration renderer (secret escaping, template defaults). Requires npm install once for the dev dependencies.
The gateway entrypoint iterates over all files in /run/secrets/ and exports each as an environment variable. The filename is uppercased and dashes are replaced by underscores, e.g.:
| Environment Variable | Secret Name | Alternative Secret Name |
|---|---|---|
OPENAI_API_KEY | openai_api_key | openai-api-key |
OPENCLAW_SANDBOX_SSH_PRIVATE_KEY | openclaw_sandbox_ssh_private_key | openclaw-sandbox-ssh-private-key |
| … | … | … |
The sandbox reads its public key directly from /run/secrets/openclaw_sandbox_ssh_public_key or alternatively /run/secrets/openclaw-sandbox-ssh-public-key (fallback when OPENCLAW_SANDBOX_SSH_PUBLIC_KEY is not set, - and _ are interchangable).
This means any Docker Secret is automatically available as an environment variable — no explicit mapping required. Secrets take precedence over environment variables.
| Variable | Required | Description |
|---|---|---|
OPENCLAW_GATEWAY_TOKEN | yes | Shared secret for Control UI |
OPENCLAW_SANDBOX_SSH_PUBLIC_KEY | yes | SSH public key (ed25519) for sandbox access |
OPENCLAW_SANDBOX_SSH_PRIVATE_KEY | yes | SSH private key, \n-encoded (gateway → sandbox) |
| Variable | Required | Description |
|---|---|---|
OPENAI_API_KEY | no | OpenAI API key; enables OpenAI provider, Whisper audio transcription, and is used as default model provider if LITELLM_MASTER_KEY is not set |
OPENCLAW_WHISPER_API_KEY | no | Whisper API key override; if unset and OPENAI_API_KEY is set, the rendered configuration uses OPENAI_API_KEY for Whisper |
OVERWRITE_CONFIG | no | Unset/true overwrites openclaw.json from the template on startup; set false to preserve manual edits |
OPENCLAW_CONFIG_DIR | no | Host path for config (default: Docker volume) |
OPENCLAW_STATE_DIR | no | OpenClaw state directory path inside the gateway container (defaults to ~/.openclaw) |
OPENCLAW_GATEWAY_PORT | no | Published host port of the gateway (default: 18789) |
OPENCLAW_GATEWAY_BIND_ADDRESS | no | Host address the gateway port is published on; default 127.0.0.1 (loopback only). Trade-off: the Control UI uses plain HTTP token auth, so the port is not exposed beyond the local machine by default — set 0.0.0.0 explicitly for LAN access, and put a TLS reverse proxy in front for anything non-local |
GITHUB_TOKEN | no | GitHub token for the separate mcp-github service (sandbox-side MCP); independent from OPENCLAW_GITHUB_TOKEN, which enables the gateway-side ACPX GitHub MCP server |
OPENCLAW_LOGGING_LEVEL | no | Gateway log level (default: info). Trade-off: debug logs request details and may leak sensitive data into logs — use it only temporarily for diagnosis |
OPENCLAW_ELEVENLABS_API_KEY | — | ElevenLabs API key; enables TTS via ElevenLabs (else Microsoft TTS) |
OPENCLAW_NOTION_API_KEY | — | Notion API key; enables Notion skill |
OPENCLAW_GITHUB_TOKEN | — | GitHub personal access token; enables GitHub MCP server via ACPX (token stays gateway-side, sandbox only sees MCP tools) |
MCP_GITHUB_URL | no (compose default) | MCP GitHub endpoint used from the sandbox. Default in this setup: http://mcp-github:4000. This value is written to /etc/environment by the sandbox entrypoint so the non-root SSH user can read it. |
OPENCLAW_GITEA_HOST | — | Gitea host URL for ACPX MCP server setup |
OPENCLAW_GITEA_TOKEN | — | Gitea personal access token; enables Gitea MCP server via ACPX |
OPENCLAW_GITEA_INSECURE | — | Optional Gitea MCP setting (GITEA_INSECURE) |
OPENCLAW_TRELLO_API_KEY | — | Trello API key; enables Trello skill |
OPENCLAW_TELEGRAM_BOT_TOKEN | — | Telegram bot token; enables Telegram channel |
OPENCLAW_DISCORD_BOT_TOKEN | — | Discord bot token; enables Discord channel |
OPENCLAW_SLACK_BOT_TOKEN | — | Slack bot token; enables Slack channel |
OPENCLAW_SLACK_APP_TOKEN | — | Slack app token for socket mode (channels.slack.appToken) |
OPENCLAW_BRAVE_API_KEY | — | Brave Search API key; enables Brave plugin (else DuckDuckGo) |
OPENCLAW_GOOGLECHAT_SERVICE_ACCOUNT_JSON | — | Google Chat service account JSON; enables Google Chat channel |
OPENCLAW_GOOGLECHAT_SERVICE_ACCOUNT_FILE | — | Path to Google Chat service account file |
OPENCLAW_MATTERMOST_BOT_TOKEN | — | Mattermost bot token; enables Mattermost channel |
OPENCLAW_MATTERMOST_BASE_URL | — | Mattermost base URL |
OPENCLAW_MATRIX_HOMESERVER | — | Matrix homeserver URL |
OPENCLAW_MATRIX_ACCESS_TOKEN | — | Matrix access token; enables Matrix channel |
OPENCLAW_MSTEAMS_APP_ID | — | Microsoft Teams app ID |
OPENCLAW_MSTEAMS_APP_PASSWORD | — | Microsoft Teams app password |
OPENCLAW_MSTEAMS_TENANT_ID | — | Microsoft Teams tenant ID |
OPENCLAW_BLUEBUBBLES_SERVER_URL | — | BlueBubbles server URL |
OPENCLAW_BLUEBUBBLES_PASSWORD | — | BlueBubbles password |
OPENCLAW_IRC_NICKSERV_PASSWORD | — | IRC NickServ password |
When LITELLM_MASTER_KEY is set, LiteLLM is enabled as model provider and the default model switches to litellm/openrouter/~moonshotai/kimi-latest. Without LiteLLM, OpenClaw uses openrouter/~moonshotai/kimi-latest when OPENROUTER_API_KEY is set, otherwise openai/gpt-4.6.
| Variable | Default | Description |
|---|---|---|
LITELLM_MASTER_KEY | — | Bearer token for LiteLLM API authentication; enables LiteLLM provider |
LITELLM_URL | — | Base URL of LiteLLM proxy for model discovery |
LITELLM_BASE_URL | http://litellm:4000 | Base URL for connecting to LiteLLM |
When configured, model lists are discovered dynamically from providers:
LITELLM_URL/v1/models → models.providers.litellm.models${OPENCLAW_OPENAI_BASE_URL:-https://api.openai.com/v1}/models → models.providers.openai.models (unless OPENCLAW_OPENAI_MODELS_JSON is explicitly set)| Variable | Default | Description |
|---|---|---|
OPENCLAW_PRIMARY_MODEL | (auto) | Default LLM model; auto-selects litellm/openrouter/~moonshotai/kimi-latest with LiteLLM, openrouter/~moonshotai/kimi-latest with OpenRouter, else openai/gpt-4.6 |
OPENCLAW_HEARTBEAT_INTERVAL | 0s | Duration for agent heartbeat (e.g. 30m, 2h, 0s = disabled) |
OPENCLAW_TIMEOUT_SECONDS | 300 | Agent execution timeout in seconds |
OPENCLAW_MAX_CONCURRENT | 5 | Maximum concurrent agents |
OPENCLAW_CRON_ENABLED | true | Enable cron scheduler support |
OPENCLAW_BASE_PATH | (empty) | Base path for Control UI (e.g. /openclaw behind reverse proxy) |
OPENCLAW_AGENT_SCOPE | agent | Sandbox scope for agent sessions; allowed: session, agent, shared |
OPENCLAW_DM_SCOPE | main | DM scope for session routing; allowed: main, per-peer, per-channel-peer, per-account-channel-peer |
OPENCLAW_SESSION_VISIBILITY | agent | Session visibility for tools; allowed: agent, global |
OPENCLAW_SESSION_TOOLS_VISIBILITY | all | Which tools are visible in sandbox sessions; allowed: all, none |
| Variable | Default | Description |
|---|---|---|
OPENCLAW_PLUGINS_JSON | — | Full plugins section as JSON |
OPENCLAW_PLUGIN_ENTRIES_JSON | — | Additional plugins.entries object merged into the generated config |
PLUGINS | — | Manual install spec passed to openclaw plugins install |
Example:
OPENCLAW_PLUGIN_ENTRIES_JSON='{"matrix":{"enabled":true,"config":{"homeserver":"https://matrix.example","accessToken":"${OPENCLAW_MATRIX_ACCESS_TOKEN}"}}}'
PLUGINS='@openclaw/matrix'
Each root section in files/openclaw.json.j2 is configurable via a section JSON variable:
OPENCLAW_<SECTION>_JSON
Example:
OPENCLAW_GATEWAY_JSON='{"mode":"local","bind":"lan","port":18789,"auth":{"mode":"token","token":"${OPENCLAW_GATEWAY_TOKEN}"},"trustedProxies":["10.0.0.0/8","172.16.0.0/12","192.168.0.0/16"]}'
Supported section variables (from official OpenClaw schema roots):
OPENCLAW_META_JSON, OPENCLAW_ENV_JSON, OPENCLAW_WIZARD_JSON, OPENCLAW_DIAGNOSTICS_JSON, OPENCLAW_LOGGING_JSON, OPENCLAW_CLI_JSON, OPENCLAW_UPDATE_JSON, OPENCLAW_BROWSER_JSON, OPENCLAW_UI_JSON, OPENCLAW_SECRETS_JSON, OPENCLAW_AUTH_JSON, OPENCLAW_ACP_JSON, OPENCLAW_MODELS_JSON, OPENCLAW_NODE_HOST_JSON, OPENCLAW_AGENTS_JSON, OPENCLAW_TOOLS_JSON, OPENCLAW_BINDINGS_JSON, OPENCLAW_BROADCAST_JSON, OPENCLAW_AUDIO_JSON, OPENCLAW_MEDIA_JSON, OPENCLAW_MESSAGES_JSON, OPENCLAW_COMMANDS_JSON, OPENCLAW_APPROVALS_JSON, OPENCLAW_SESSION_JSON, OPENCLAW_CRON_JSON, OPENCLAW_HOOKS_JSON, OPENCLAW_WEB_JSON, OPENCLAW_CHANNELS_JSON, OPENCLAW_DISCOVERY_JSON, OPENCLAW_CANVAS_HOST_JSON, OPENCLAW_TALK_JSON, OPENCLAW_GATEWAY_JSON, OPENCLAW_MEMORY_JSON, OPENCLAW_MCP_JSON, OPENCLAW_SKILLS_JSON, OPENCLAW_PLUGINS_JSON.
If OPENCLAW_<SECTION>_JSON is set, it replaces that full section from the template.
If not set, the template defaults and feature toggles apply.
Plugin configurations are supported in two modes:
OPENCLAW_PLUGINS_JSONOPENCLAW_PLUGIN_ENTRIES_JSONIn addition to section-level JSON overrides, common single settings can be overridden directly via environment variables.
Most useful groups:
OPENCLAW_MODELS_MODE, OPENCLAW_OPENAI_BASE_URL, OPENCLAW_OPENAI_MODELS_JSON, OPENCLAW_LITELLM_*, OPENCLAW_AGENT_MODELS_JSONOPENCLAW_AGENT_SANDBOX_MODE, OPENCLAW_AGENT_WORKSPACE_ACCESS, OPENCLAW_SUBAGENT_*OPENCLAW_TOOLS_FS_WORKSPACE_ONLY, OPENCLAW_LOOP_DETECTION_*, OPENCLAW_MEDIA_AUDIO_*, OPENCLAW_TTS_*OPENCLAW_MESSAGES_QUEUE_*, OPENCLAW_COMMANDS_*, OPENCLAW_HOOKS_*OPENCLAW_TELEGRAM_*, OPENCLAW_DISCORD_*, OPENCLAW_SLACK_*, OPENCLAW_WHATSAPP_*, OPENCLAW_GOOGLECHAT_*, OPENCLAW_MATTERMOST_*, OPENCLAW_SIGNAL_*, OPENCLAW_IRC_*OPENCLAW_GATEWAY_*, OPENCLAW_CONTROL_UI_*, OPENCLAW_ALLOWED_ORIGINS_JSON, OPENCLAW_TAILSCALE_*, OPENCLAW_TRUSTED_PROXIES_JSONOPENCLAW_PLUGIN_*, OPENCLAW_ACPX_*, OPENCLAW_GITHUB_TOKEN, OPENCLAW_GITEA_*, PLUGINSFor token/secret-based channels, there is intentionally no separate *_ENABLED toggle: the token/secret is the feature enabler.
Special case:
OPENCLAW_ALLOWED_ORIGINS_JSON sets gateway.controlUi.allowedOrigins.allowedOrigins; if not set, the field is not written.Model handling:
OPENCLAW_AGENT_MODELS_JSON.models.providers.*.models), including LiteLLM discovery via LITELLM_URL + LITELLM_MASTER_KEY.For a full technical variable reference, use the gateway service environment block in docker-compose.yml and the template defaults in files/openclaw.json.j2.
The openclaw-dind service provides an isolated Docker daemon for the sandbox. It is optional — simply remove the openclaw-dind service and the DOCKER_HOST environment variable from the sandbox to disable it.
Who needs this? Developers and DevOps engineers who want OpenClaw to autonomously build, run, and test containerized applications. For general use (writing, research, scripting), DinD is not needed.
Security warning: The AI has full root access inside the DinD daemon. It can mount the DinD container's root filesystem, destroy all images/containers, or exhaust disk space on the openclaw-docker volume. DinD is isolated from the host Docker, but within its own daemon the AI has unrestricted access. Only enable this if you accept that risk.
Docker Swarm does not support privileged: true in stack deploy files. Docker-in-Docker is therefore not supported in this Swarm setup.
docker secret, not environment variables--opt encrypted)read_only: true + tmpfs mounts if OpenClaw supports itContent type
Image
Digest
sha256:91dc00538…
Size
310.5 MB
Last updated
about 1 month ago
docker pull mwaeckerlin/openclaw:gateway