docs/gateway/security/index.md
OpenClaw security guidance assumes a personal assistant deployment: one trusted operator boundary, potentially many agents.
This page explains hardening within that model. It does not claim hostile multi-tenant isolation on one shared gateway.
openclaw security auditSee also: Formal Verification (Security Models)
Run this regularly (especially after changing config or exposing network surfaces):
openclaw security audit
openclaw security audit --deep
openclaw security audit --fix
openclaw security audit --json
security audit --fix stays intentionally narrow: it flips common open group
policies to allowlists, restores logging.redactSensitive: "tools", tightens
state/config/include-file permissions, and uses Windows ACL resets instead of
POSIX chmod when running on Windows.
It flags common footguns (Gateway auth exposure, browser control exposure, elevated allowlists, filesystem permissions, permissive exec approvals, and open-channel tool exposure).
OpenClaw is both a product and an experiment: you’re wiring frontier-model behavior into real messaging surfaces and real tools. There is no “perfectly secure” setup. The goal is to be deliberate about:
Start with the smallest access that still works, then widen it as you gain confidence.
OpenClaw assumes the host and config boundary are trusted:
~/.openclaw, including openclaw.json), treat them as a trusted operator.sessionKey, session IDs, labels) are routing selectors, not authorization tokens.If "everyone in Slack can message the bot," the core risk is delegated tool authority:
exec, browser, network/file tools) within the agent's policy;Use separate agents/gateways with minimal tools for team workflows; keep personal-data agents private.
This is acceptable when everyone using that agent is in the same trust boundary (for example one company team) and the agent is strictly business-scoped.
If you mix personal and company identities on the same runtime, you collapse the separation and increase personal-data exposure risk.
Treat Gateway and node as one operator trust domain, with different roles:
gateway.auth, tool policy, routing).sessionKey is routing/context selection, not per-user auth.gateway/node is allowed without approval prompts (security="full", ask="off" unless you tighten it). That default is intentional UX, not a vulnerability by itself.If you need hostile-user isolation, split trust boundaries by OS user/host and run separate gateways.
Use this as the quick model when triaging risk:
| Boundary or control | What it means | Common misread |
|---|---|---|
gateway.auth (token/password/trusted-proxy/device auth) | Authenticates callers to gateway APIs | "Needs per-message signatures on every frame to be secure" |
sessionKey | Routing key for context/session selection | "Session key is a user auth boundary" |
| Prompt/content guardrails | Reduce model abuse risk | "Prompt injection alone proves auth bypass" |
canvas.eval / browser evaluate | Intentional operator capability when enabled | "Any JS eval primitive is automatically a vuln in this trust model" |
Local TUI ! shell | Explicit operator-triggered local execution | "Local shell convenience command is remote injection" |
| Node pairing and node commands | Operator-level remote execution on paired devices | "Remote device control should be treated as untrusted user access by default" |
gateway.nodes.pairing.autoApproveCidrs | Opt-in trusted-network node enrollment policy | "A disabled-by-default allowlist is an automatic pairing vulnerability" |
These patterns get reported often and are usually closed as no-action unless a real boundary bypass is demonstrated:
sessions.list / sessions.preview / chat.history) as IDOR in a
shared-gateway setup.system.run, when the real execution boundary is still
the gateway's global node command policy plus the node's own exec
approvals.gateway.nodes.pairing.autoApproveCidrs as a
vulnerability by itself. This setting is disabled by default, requires
explicit CIDR/IP entries, only applies to first-time role: node pairing with
no requested scopes, and does not auto-approve operator/browser/Control UI,
WebChat, role upgrades, scope upgrades, metadata changes, public-key changes,
or same-host loopback trusted-proxy header paths unless loopback trusted-proxy auth was explicitly enabled.sessionKey as an
auth token.Use this baseline first, then selectively re-enable tools per trusted agent:
{
gateway: {
mode: "local",
bind: "loopback",
auth: { mode: "token", token: "replace-with-long-random-token" },
},
session: {
dmScope: "per-channel-peer",
},
tools: {
profile: "messaging",
deny: ["group:automation", "group:runtime", "group:fs", "sessions_spawn", "sessions_send"],
fs: { workspaceOnly: true },
exec: { security: "deny", ask: "always" },
elevated: { enabled: false },
},
channels: {
whatsapp: { dmPolicy: "pairing", groups: { "*": { requireMention: true } } },
},
}
This keeps the Gateway local-only, isolates DMs, and disables control-plane/runtime tools by default.
If more than one person can DM your bot:
session.dmScope: "per-channel-peer" (or "per-account-channel-peer" for multi-account channels).dmPolicy: "pairing" or strict allowlists.OpenClaw separates two concepts:
dmPolicy, groupPolicy, allowlists, mention gates).Allowlists gate triggers and command authorization. The contextVisibility setting controls how supplemental context (quoted replies, thread roots, fetched history) is filtered:
contextVisibility: "all" (default) keeps supplemental context as received.contextVisibility: "allowlist" filters supplemental context to senders allowed by the active allowlist checks.contextVisibility: "allowlist_quote" behaves like allowlist, but still keeps one explicit quoted reply.Set contextVisibility per channel or per room/conversation. See Group Chats for setup details.
Advisory triage guidance:
contextVisibility, not auth or sandbox boundary bypasses by themselves.security=full, autoAllowSkills, interpreter allowlists without strictInlineEval): are host-exec guardrails still doing what you think they are?
security="full" is a broad posture warning, not proof of a bug. It is the chosen default for trusted personal-assistant setups; tighten it only when your threat model needs approval or allowlist guardrails.gateway.nodes.denyCommands patterns because matching is exact command-name only (for example system.run) and does not inspect shell text; dangerous gateway.nodes.allowCommands entries; global tools.profile="minimal" overridden by per-agent profiles; plugin-owned tools reachable under permissive tool policy).sandbox when tools.exec.host now defaults to auto, or explicitly setting tools.exec.host="sandbox" while sandbox mode is off).If you run --deep, OpenClaw also attempts a best-effort live Gateway probe.
Use this when auditing access or deciding what to back up:
~/.openclaw/credentials/whatsapp/<accountId>/creds.jsonchannels.telegram.tokenFile (regular file only; symlinks rejected)channels.slack.*)~/.openclaw/credentials/<channel>-allowFrom.json (default account)~/.openclaw/credentials/<channel>-<accountId>-allowFrom.json (non-default accounts)~/.openclaw/agents/<agentId>/agent/auth-profiles.json~/.openclaw/agents/<agentId>/agent/codex-home/~/.openclaw/secrets.json~/.openclaw/credentials/oauth.jsonWhen the audit prints findings, treat this as a priority order:
Each audit finding is keyed by a structured checkId (for example
gateway.bind_no_auth or tools.exec.security_full_configured). Common
critical severity classes:
fs.* — filesystem permissions on state, config, credentials, auth profiles.gateway.* — bind mode, auth, Tailscale, Control UI, trusted-proxy setup.hooks.*, browser.*, sandbox.*, tools.exec.* — per-surface hardening.plugins.*, skills.* — plugin/skill supply chain and scan findings.security.exposure.* — cross-cutting checks where access policy meets tool blast radius.See the full catalog with severity levels, fix keys, and auto-fix support at Security audit checks.
The Control UI needs a secure context (HTTPS or localhost) to generate device
identity. gateway.controlUi.allowInsecureAuth is a local compatibility toggle:
Prefer HTTPS (Tailscale Serve) or open the UI on 127.0.0.1.
For break-glass scenarios only, gateway.controlUi.dangerouslyDisableDeviceAuth
disables device identity checks entirely. This is a severe security downgrade;
keep it off unless you are actively debugging and can revert quickly.
Separate from those dangerous flags, successful gateway.auth.mode: "trusted-proxy"
can admit operator Control UI sessions without device identity. That is an
intentional auth-mode behavior, not an allowInsecureAuth shortcut, and it still
does not extend to node-role Control UI sessions.
openclaw security audit warns when this setting is enabled.
openclaw security audit raises config.insecure_or_dangerous_flags when
known insecure/dangerous debug switches are enabled. Keep these unset in
production.
- `gateway.controlUi.dangerouslyAllowHostHeaderOriginFallback`
- `gateway.controlUi.dangerouslyDisableDeviceAuth`
- `browser.ssrfPolicy.dangerouslyAllowPrivateNetwork`
Channel name-matching (bundled and plugin channels; also available per
`accounts.<accountId>` where applicable):
- `channels.discord.dangerouslyAllowNameMatching`
- `channels.slack.dangerouslyAllowNameMatching`
- `channels.googlechat.dangerouslyAllowNameMatching`
- `channels.msteams.dangerouslyAllowNameMatching`
- `channels.synology-chat.dangerouslyAllowNameMatching` (plugin channel)
- `channels.synology-chat.dangerouslyAllowInheritedWebhookPath` (plugin channel)
- `channels.zalouser.dangerouslyAllowNameMatching` (plugin channel)
- `channels.irc.dangerouslyAllowNameMatching` (plugin channel)
- `channels.mattermost.dangerouslyAllowNameMatching` (plugin channel)
Network exposure:
- `channels.telegram.network.dangerouslyAllowPrivateNetwork` (also per account)
Sandbox Docker (defaults + per-agent):
- `agents.defaults.sandbox.docker.dangerouslyAllowReservedContainerTargets`
- `agents.defaults.sandbox.docker.dangerouslyAllowExternalBindSources`
- `agents.defaults.sandbox.docker.dangerouslyAllowContainerNamespaceJoin`
If you run the Gateway behind a reverse proxy (nginx, Caddy, Traefik, etc.), configure
gateway.trustedProxies for proper forwarded-client IP handling.
When the Gateway detects proxy headers from an address that is not in trustedProxies, it will not treat connections as local clients. If gateway auth is disabled, those connections are rejected. This prevents authentication bypass where proxied connections would otherwise appear to come from localhost and receive automatic trust.
gateway.trustedProxies also feeds gateway.auth.mode: "trusted-proxy", but that auth mode is stricter:
gateway.trustedProxies for local-client detection and forwarded IP handlinggateway.auth.mode: "trusted-proxy" only when gateway.auth.trustedProxy.allowLoopback = true; otherwise use token/password authgateway:
trustedProxies:
- "10.0.0.1" # reverse proxy IP
# Optional. Default false.
# Only enable if your proxy cannot provide X-Forwarded-For.
allowRealIpFallback: false
auth:
mode: password
password: ${OPENCLAW_GATEWAY_PASSWORD}
When trustedProxies is configured, the Gateway uses X-Forwarded-For to determine the client IP. X-Real-IP is ignored by default unless gateway.allowRealIpFallback: true is explicitly set.
Trusted proxy headers do not make node device pairing automatically trusted.
gateway.nodes.pairing.autoApproveCidrs is a separate, disabled-by-default
operator policy. Even when enabled, loopback-source trusted-proxy header paths
are excluded from node auto-approval because local callers can forge those
headers, including when loopback trusted-proxy auth is explicitly enabled.
Good reverse proxy behavior (overwrite incoming forwarding headers):
proxy_set_header X-Forwarded-For $remote_addr;
proxy_set_header X-Real-IP $remote_addr;
Bad reverse proxy behavior (append/preserve untrusted forwarding headers):
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
gateway.http.securityHeaders.strictTransportSecurity to emit the HSTS header from OpenClaw responses.gateway.controlUi.allowedOrigins is required by default.gateway.controlUi.allowedOrigins: ["*"] is an explicit allow-all browser-origin policy, not a hardened default. Avoid it outside tightly controlled local testing.Origin value instead of one shared localhost bucket.gateway.controlUi.dangerouslyAllowHostHeaderOriginFallback=true enables Host-header origin fallback mode; treat it as a dangerous operator-selected policy.trustedProxies tight and avoid exposing the gateway directly to the public internet.OpenClaw stores session transcripts on disk under ~/.openclaw/agents/<agentId>/sessions/*.jsonl.
This is required for session continuity and (optionally) session memory indexing, but it also means
any process/user with filesystem access can read those logs. Treat disk access as the trust
boundary and lock down permissions on ~/.openclaw (see the audit section below). If you need
stronger isolation between agents, run them under separate OS users or separate hosts.
If a macOS node is paired, the Gateway can invoke system.run on that node. This is remote code execution on the Mac:
gateway.nodes.allowCommands / denyCommands.system.run policy is the node's own exec approvals file (exec.approvals.node.*), which can be stricter or looser than the gateway's global command-ID policy.security="full" and ask="off" is following the default trusted-operator model. Treat that as expected behavior unless your deployment explicitly requires a tighter approval or allowlist stance.host=node, approval-backed runs also store a canonical prepared
systemRunPlan; later approved forwards reuse that stored plan, and gateway
validation rejects caller edits to command/cwd/session context after the
approval request was created.This distinction matters for triage:
OpenClaw can refresh the skills list mid-session:
SKILL.md can update the skills snapshot on the next agent turn.Treat skill folders as trusted code and restrict who can modify them.
Your AI assistant can:
People who message you can:
Most failures here are not fancy exploits — they’re “someone messaged the bot and the bot did what they asked.”
OpenClaw’s stance:
Slash commands and directives are only honored for authorized senders. Authorization is derived from
channel allowlists/pairing plus commands.useAccessGroups (see Configuration
and Slash commands). If a channel allowlist is empty or includes "*",
commands are effectively open for that channel.
/exec is a session-only convenience for authorized operators. It does not write config or
change other sessions.
Two built-in tools can make persistent control-plane changes:
gateway can inspect config with config.schema.lookup / config.get, and can make persistent changes with config.apply, config.patch, and update.run.cron can create scheduled jobs that keep running after the original chat/task ends.The owner-only gateway runtime tool still refuses to rewrite
tools.exec.ask or tools.exec.security; legacy tools.bash.* aliases are
normalized to the same protected exec paths before the write.
Agent-driven gateway config.apply and gateway config.patch edits are
fail-closed by default: only a narrow set of prompt, model, and mention-gating
paths are agent-tunable. New sensitive config trees are therefore protected
unless they are deliberately added to the allowlist.
For any agent/surface that handles untrusted content, deny these by default:
{
tools: {
deny: ["gateway", "cron", "sessions_spawn", "sessions_send"],
},
}
commands.restart=false only blocks restart actions. It does not disable gateway config/update actions.
Plugins run in-process with the Gateway. Treat them as trusted code:
plugins.allow allowlists.openclaw plugins install <package>, openclaw plugins update <id>), treat it like running untrusted code:
critical findings block by default.npm install.@scope/[email protected]), and inspect the unpacked code on disk before enabling.--dangerously-force-unsafe-install is break-glass only for built-in scan false positives on plugin install/update flows. It does not bypass plugin before_install hook policy blocks and does not bypass scan failures.critical findings block unless the caller explicitly sets dangerouslyForceUnsafeInstall, while suspicious findings still warn only. openclaw skills install remains the separate ClawHub skill download/install flow.Details: Plugins
All current DM-capable channels support a DM policy (dmPolicy or *.dm.policy) that gates inbound DMs before the message is processed:
pairing (default): unknown senders receive a short pairing code and the bot ignores their message until approved. Codes expire after 1 hour; repeated DMs won’t resend a code until a new request is created. Pending requests are capped at 3 per channel by default.allowlist: unknown senders are blocked (no pairing handshake).open: allow anyone to DM (public). Requires the channel allowlist to include "*" (explicit opt-in).disabled: ignore inbound DMs entirely.Approve via CLI:
openclaw pairing list <channel>
openclaw pairing approve <channel> <code>
Details + files on disk: Pairing
By default, OpenClaw routes all DMs into the main session so your assistant has continuity across devices and channels. If multiple people can DM the bot (open DMs or a multi-person allowlist), consider isolating DM sessions:
{
session: { dmScope: "per-channel-peer" },
}
This prevents cross-user context leakage while keeping group chats isolated.
This is a messaging-context boundary, not a host-admin boundary. If users are mutually adversarial and share the same Gateway host/config, run separate gateways per trust boundary instead.
Treat the snippet above as secure DM mode:
session.dmScope: "main" (all DMs share one session for continuity).session.dmScope: "per-channel-peer" when unset (keeps existing explicit values).session.dmScope: "per-channel-peer" (each channel+sender pair gets an isolated DM context).session.dmScope: "per-peer" (each sender gets one session across all channels of the same type).If you run multiple accounts on the same channel, use per-account-channel-peer instead. If the same person contacts you on multiple channels, use session.identityLinks to collapse those DM sessions into one canonical identity. See Session Management and Configuration.
OpenClaw has two separate “who can trigger me?” layers:
allowFrom / channels.discord.allowFrom / channels.slack.allowFrom; legacy: channels.discord.dm.allowFrom, channels.slack.dm.allowFrom): who is allowed to talk to the bot in direct messages.
dmPolicy="pairing", approvals are written to the account-scoped pairing allowlist store under ~/.openclaw/credentials/ (<channel>-allowFrom.json for default account, <channel>-<accountId>-allowFrom.json for non-default accounts), merged with config allowlists.channels.whatsapp.groups, channels.telegram.groups, channels.imessage.groups: per-group defaults like requireMention; when set, it also acts as a group allowlist (include "*" to keep allow-all behavior).groupPolicy="allowlist" + groupAllowFrom: restrict who can trigger the bot inside a group session (WhatsApp/Telegram/Signal/iMessage/Microsoft Teams).channels.discord.guilds / channels.slack.channels: per-surface allowlists + mention defaults.groupPolicy/group allowlists first, mention/reply activation second.groupAllowFrom.dmPolicy="open" and groupPolicy="open" as last-resort settings. They should be barely used; prefer pairing + allowlists unless you fully trust every member of the room.Details: Configuration and Groups
Prompt injection is when an attacker crafts a message that manipulates the model into doing something unsafe (“ignore your instructions”, “dump your filesystem”, “follow this link and run commands”, etc.).
Even with strong system prompts, prompt injection is not solved. System prompt guardrails are soft guidance only; hard enforcement comes from tool policy, exec approvals, sandboxing, and channel allowlists (and operators can disable these by design). What helps in practice:
host=auto resolves to the gateway host. Explicit host=sandbox still fails closed because no sandbox runtime is available. Set host=gateway if you want that behavior to be explicit in config.exec, browser, web_fetch, web_search) to trusted agents or explicit allowlists.python, node, ruby, perl, php, lua, osascript), enable tools.exec.strictInlineEval so inline eval forms still need explicit approval.$VAR, $?, $$, $1, $@, ${…}) inside unquoted heredocs, so an allowlisted heredoc body cannot sneak shell expansion past allowlist review as plain text. Quote the heredoc terminator (for example <<'EOF') to opt into literal body semantics; unquoted heredocs that would have expanded variables are rejected.Red flags to treat as untrusted:
OpenClaw strips common self-hosted LLM chat-template special-token literals from wrapped external content and metadata before they reach the model. Covered marker families include Qwen/ChatML, Llama, Gemma, Mistral, Phi, and GPT-OSS role/turn tokens.
Why:
assistant or system role boundary and escape the wrapped-content guardrails.<tool_call>, <function_calls>, ``, <previous_response>, and similar internal runtime scaffolding from user-visible replies at the final channel delivery boundary. The external-content sanitizer is the inbound counterpart.This does not replace the other hardening on this page — dmPolicy, allowlists, exec approvals, sandboxing, and contextVisibility still do the primary work. It closes one specific tokenizer-layer bypass against self-hosted stacks that forward user text with special tokens intact.
OpenClaw includes explicit bypass flags that disable external-content safety wrapping:
hooks.mappings[].allowUnsafeExternalContenthooks.gmail.allowUnsafeExternalContentallowUnsafeExternalContentGuidance:
Hooks risk note:
tools.profile: "messaging" or stricter), plus sandboxing where possible.Even if only you can message the bot, prompt injection can still happen via any untrusted content the bot reads (web search/fetch results, browser pages, emails, docs, attachments, pasted logs/code). In other words: the sender is not the only threat surface; the content itself can carry adversarial instructions.
When tools are enabled, the typical risk is exfiltrating context or triggering tool calls. Reduce the blast radius by:
web_search / web_fetch / browser off for tool-enabled agents unless needed.input_file / input_image), set tight
gateway.http.endpoints.responses.files.urlAllowlist and
gateway.http.endpoints.responses.images.urlAllowlist, and keep maxUrlParts low.
Empty allowlists are treated as unset; use files.allowUrl: false / images.allowUrl: false
if you want to disable URL fetching entirely.input_file text is still injected as
untrusted external content. Do not rely on file text being trusted just because
the Gateway decoded it locally. The injected block still carries explicit
<<<EXTERNAL_UNTRUSTED_CONTENT ...>>> boundary markers plus Source: External
metadata, even though this path omits the longer SECURITY NOTICE: banner.OpenAI-compatible self-hosted backends such as vLLM, SGLang, TGI, LM Studio,
or custom Hugging Face tokenizer stacks can differ from hosted providers in how
chat-template special tokens are handled. If a backend tokenizes literal strings
such as <|im_start|>, <|start_header_id|>, or <start_of_turn> as
structural chat-template tokens inside user content, untrusted text can try to
forge role boundaries at the tokenizer layer.
OpenClaw strips common model-family special-token literals from wrapped external content before dispatching it to the model. Keep external-content wrapping enabled, and prefer backend settings that split or escape special tokens in user-provided content when available. Hosted providers such as OpenAI and Anthropic already apply their own request-side sanitization.
Prompt injection resistance is not uniform across model tiers. Smaller/cheaper models are generally more susceptible to tool misuse and instruction hijacking, especially under adversarial prompts.
<Warning> For tool-enabled agents or agents that read untrusted content, prompt-injection risk with older/smaller models is often too high. Do not run those workloads on weak model tiers. </Warning>Recommendations:
/reasoning, /verbose, and /trace can expose internal reasoning, tool
output, or plugin diagnostics that
was not meant for a public channel. In group settings, treat them as debug
only and keep them off unless you explicitly need them.
Guidance:
/reasoning, /verbose, and /trace disabled in public rooms.Keep config + state private on the gateway host:
~/.openclaw/openclaw.json: 600 (user read/write only)~/.openclaw: 700 (user only)openclaw doctor can warn and offer to tighten these permissions.
The Gateway multiplexes WebSocket + HTTP on a single port:
18789gateway.port, --port, OPENCLAW_GATEWAY_PORTThis HTTP surface includes the Control UI and the canvas host:
/)/__openclaw__/canvas/ and /__openclaw__/a2ui/ (arbitrary HTML/JS; treat as untrusted content)If you load canvas content in a normal browser, treat it like any other untrusted web page:
Bind mode controls where the Gateway listens:
gateway.bind: "loopback" (default): only local clients can connect."lan", "tailnet", "custom") expand the attack surface. Only use them with gateway auth (shared token/password or a correctly configured trusted proxy) and a real firewall.Rules of thumb:
0.0.0.0.If you run OpenClaw with Docker on a VPS, remember that published container ports
(-p HOST:CONTAINER or Compose ports:) are routed through Docker's forwarding
chains, not only host INPUT rules.
To keep Docker traffic aligned with your firewall policy, enforce rules in
DOCKER-USER (this chain is evaluated before Docker's own accept rules).
On many modern distros, iptables/ip6tables use the iptables-nft frontend
and still apply these rules to the nftables backend.
Minimal allowlist example (IPv4):
# /etc/ufw/after.rules (append as its own *filter section)
*filter
:DOCKER-USER - [0:0]
-A DOCKER-USER -m conntrack --ctstate ESTABLISHED,RELATED -j RETURN
-A DOCKER-USER -s 127.0.0.0/8 -j RETURN
-A DOCKER-USER -s 10.0.0.0/8 -j RETURN
-A DOCKER-USER -s 172.16.0.0/12 -j RETURN
-A DOCKER-USER -s 192.168.0.0/16 -j RETURN
-A DOCKER-USER -s 100.64.0.0/10 -j RETURN
-A DOCKER-USER -p tcp --dport 80 -j RETURN
-A DOCKER-USER -p tcp --dport 443 -j RETURN
-A DOCKER-USER -m conntrack --ctstate NEW -j DROP
-A DOCKER-USER -j RETURN
COMMIT
IPv6 has separate tables. Add a matching policy in /etc/ufw/after6.rules if
Docker IPv6 is enabled.
Avoid hardcoding interface names like eth0 in docs snippets. Interface names
vary across VPS images (ens3, enp*, etc.) and mismatches can accidentally
skip your deny rule.
Quick validation after reload:
ufw reload
iptables -S DOCKER-USER
ip6tables -S DOCKER-USER
nmap -sT -p 1-65535 <public-ip> --open
Expected external ports should be only what you intentionally expose (for most setups: SSH + your reverse proxy ports).
When the bundled bonjour plugin is enabled, the Gateway broadcasts its presence via mDNS (_openclaw-gw._tcp on port 5353) for local device discovery. In full mode, this includes TXT records that may expose operational details:
cliPath: full filesystem path to the CLI binary (reveals username and install location)sshPort: advertises SSH availability on the hostdisplayName, lanHost: hostname informationOperational security consideration: Broadcasting infrastructure details makes reconnaissance easier for anyone on the local network. Even "harmless" info like filesystem paths and SSH availability helps attackers map your environment.
Recommendations:
Keep Bonjour disabled unless LAN discovery is needed. Bonjour auto-starts on macOS hosts and is opt-in elsewhere; direct Gateway URLs, Tailnet, SSH, or wide-area DNS-SD avoid local multicast.
Minimal mode (default when Bonjour is enabled, recommended for exposed gateways): omit sensitive fields from mDNS broadcasts:
{
discovery: {
mdns: { mode: "minimal" },
},
}
Disable mDNS mode if you want to keep the plugin enabled but suppress local device discovery:
{
discovery: {
mdns: { mode: "off" },
},
}
Full mode (opt-in): include cliPath + sshPort in TXT records:
{
discovery: {
mdns: { mode: "full" },
},
}
Environment variable (alternative): set OPENCLAW_DISABLE_BONJOUR=1 to disable mDNS without config changes.
When Bonjour is enabled in minimal mode, the Gateway broadcasts enough for device discovery (role, gatewayPort, transport) but omits cliPath and sshPort. Apps that need CLI path information can fetch it via the authenticated WebSocket connection instead.
Gateway auth is required by default. If no valid gateway auth path is configured, the Gateway refuses WebSocket connections (fail‑closed).
Onboarding generates a token by default (even for loopback) so local clients must authenticate.
Set a token so all WS clients must authenticate:
{
gateway: {
auth: { mode: "token", token: "your-token" },
},
}
Doctor can generate one for you: openclaw doctor --generate-gateway-token.
Local device pairing:
Auth modes:
gateway.auth.mode: "token": shared bearer token (recommended for most setups).gateway.auth.mode: "password": password auth (prefer setting via env: OPENCLAW_GATEWAY_PASSWORD).gateway.auth.mode: "trusted-proxy": trust an identity-aware reverse proxy to authenticate users and pass identity via headers (see Trusted Proxy Auth).Rotation checklist (token/password):
gateway.auth.token or OPENCLAW_GATEWAY_PASSWORD).gateway.remote.token / .password on machines that call into the Gateway).When gateway.auth.allowTailscale is true (default for Serve), OpenClaw
accepts Tailscale Serve identity headers (tailscale-user-login) for Control
UI/WebSocket authentication. OpenClaw verifies the identity by resolving the
x-forwarded-for address through the local Tailscale daemon (tailscale whois)
and matching it to the header. This only triggers for requests that hit loopback
and include x-forwarded-for, x-forwarded-proto, and x-forwarded-host as
injected by Tailscale.
For this async identity check path, failed attempts for the same {scope, ip}
are serialized before the limiter records the failure. Concurrent bad retries
from one Serve client can therefore lock out the second attempt immediately
instead of racing through as two plain mismatches.
HTTP API endpoints (for example /v1/*, /tools/invoke, and /api/channels/*)
do not use Tailscale identity-header auth. They still follow the gateway's
configured HTTP auth mode.
Important boundary note:
/v1/chat/completions, /v1/responses, or /api/channels/* as full-access operator secrets for that gateway.operator.admin, operator.approvals, operator.pairing, operator.read, operator.talk.secrets, operator.write) and owner semantics for agent turns; narrower x-openclaw-scopes values do not reduce that shared-secret path.gateway.auth.mode="none" on a private ingress.x-openclaw-scopes falls back to the normal operator default scope set; send the header explicitly when you want a narrower scope set./tools/invoke follows the same shared-secret rule: token/password bearer auth is treated as full operator access there too, while identity-bearing modes still honor declared scopes.Trust assumption: tokenless Serve auth assumes the gateway host is trusted.
Do not treat this as protection against hostile same-host processes. If untrusted
local code may run on the gateway host, disable gateway.auth.allowTailscale
and require explicit shared-secret auth with gateway.auth.mode: "token" or
"password".
Security rule: do not forward these headers from your own reverse proxy. If
you terminate TLS or proxy in front of the gateway, disable
gateway.auth.allowTailscale and use shared-secret auth (gateway.auth.mode: "token" or "password") or Trusted Proxy Auth
instead.
Trusted proxies:
gateway.trustedProxies to your proxy IPs.x-forwarded-for (or x-real-ip) from those IPs to determine the client IP for local pairing checks and HTTP auth/local checks.x-forwarded-for and blocks direct access to the Gateway port.See Tailscale and Web overview.
If your Gateway is remote but the browser runs on another machine, run a node host on the browser machine and let the Gateway proxy browser actions (see Browser tool). Treat node pairing like admin access.
Recommended pattern:
Avoid:
Assume anything under ~/.openclaw/ (or $OPENCLAW_STATE_DIR/) may contain secrets or private data:
openclaw.json: config may include tokens (gateway, remote gateway), provider settings, and allowlists.credentials/**: channel credentials (example: WhatsApp creds), pairing allowlists, legacy OAuth imports.agents/<agentId>/agent/auth-profiles.json: API keys, token profiles, OAuth tokens, and optional keyRef/tokenRef.agents/<agentId>/agent/codex-home/**: per-agent Codex app-server account, config, skills, plugins, native thread state, and diagnostics.secrets.json (optional): file-backed secret payload used by file SecretRef providers (secrets.providers).agents/<agentId>/agent/auth.json: legacy compatibility file. Static api_key entries are scrubbed when discovered.agents/<agentId>/sessions/**: session transcripts (*.jsonl) + routing metadata (sessions.json) that can contain private messages and tool output.node_modules/).sandboxes/**: tool sandbox workspaces; can accumulate copies of files you read/write inside the sandbox.Hardening tips:
700 on dirs, 600 on files)..env filesOpenClaw loads workspace-local .env files for agents and tools, but never lets those files silently override gateway runtime controls.
OPENCLAW_* is blocked from untrusted workspace .env files..env overrides, so cloned workspaces cannot redirect bundled connector traffic through local endpoint config. Endpoint env keys (such as MATRIX_HOMESERVER, MATTERMOST_URL, IRC_HOST, SYNOLOGY_CHAT_INCOMING_URL) must come from the gateway process environment or env.shellEnv, not from a workspace-loaded .env..env; the key is ignored and the gateway keeps its own value..env file loading.Why: workspace .env files frequently live next to agent code, get committed by accident, or get written by tools. Blocking the whole OPENCLAW_* prefix means adding a new OPENCLAW_* flag later can never regress into silent inheritance from workspace state.
Logs and transcripts can leak sensitive info even when access controls are correct:
Recommendations:
logging.redactSensitive: "tools"; default).logging.redactPatterns (tokens, hostnames, internal URLs).openclaw status --all (pasteable, secrets redacted) over raw logs.Details: Logging
{
channels: { whatsapp: { dmPolicy: "pairing" } },
}
{
"channels": {
"whatsapp": {
"groups": {
"*": { "requireMention": true }
}
}
},
"agents": {
"list": [
{
"id": "main",
"groupChat": { "mentionPatterns": ["@openclaw", "@mybot"] }
}
]
}
}
In group chats, only respond when explicitly mentioned.
For phone-number-based channels, consider running your AI on a separate phone number from your personal one:
You can build a read-only profile by combining:
agents.defaults.sandbox.workspaceAccess: "ro" (or "none" for no workspace access)write, edit, apply_patch, exec, process, etc.Additional hardening options:
tools.exec.applyPatch.workspaceOnly: true (default): ensures apply_patch cannot write/delete outside the workspace directory even when sandboxing is off. Set to false only if you intentionally want apply_patch to touch files outside the workspace.tools.fs.workspaceOnly: true (optional): restricts read/write/edit/apply_patch paths and native prompt image auto-load paths to the workspace directory (useful if you allow absolute paths today and want a single guardrail).~/.openclaw) to filesystem tools.One “safe default” config that keeps the Gateway private, requires DM pairing, and avoids always-on group bots:
{
gateway: {
mode: "local",
bind: "loopback",
port: 18789,
auth: { mode: "token", token: "your-long-random-token" },
},
channels: {
whatsapp: {
dmPolicy: "pairing",
groups: { "*": { requireMention: true } },
},
},
}
If you want “safer by default” tool execution too, add a sandbox + deny dangerous tools for any non-owner agent (example below under “Per-agent access profiles”).
Built-in baseline for chat-driven agent turns: non-owner senders cannot use the cron or gateway tools.
Dedicated doc: Sandboxing
Two complementary approaches:
agents.defaults.sandbox, host gateway + sandbox-isolated tools; Docker is the default backend): SandboxingAlso consider agent workspace access inside the sandbox:
agents.defaults.sandbox.workspaceAccess: "none" (default) keeps the agent workspace off-limits; tools run against a sandbox workspace under ~/.openclaw/sandboxesagents.defaults.sandbox.workspaceAccess: "ro" mounts the agent workspace read-only at /agent (disables write/edit/apply_patch)agents.defaults.sandbox.workspaceAccess: "rw" mounts the agent workspace read/write at /workspacesandbox.docker.binds are validated against normalized and canonicalized source paths. Parent-symlink tricks and canonical home aliases still fail closed if they resolve into blocked roots such as /etc, /var/run, or credential directories under the OS home.If you allow session tools, treat delegated sub-agent runs as another boundary decision:
sessions_spawn unless the agent truly needs delegation.agents.defaults.subagents.allowAgents and any per-agent agents.list[].subagents.allowAgents overrides restricted to known-safe target agents.sessions_spawn with sandbox: "require" (default is inherit).sandbox: "require" fails fast when the target child runtime is not sandboxed.Enabling browser control gives the model the ability to drive a real browser. If that browser profile already contains logged-in sessions, the model can access those accounts and data. Treat browser profiles as sensitive state:
openclaw profile).gateway.nodes.browser.mode="off").OpenClaw’s browser navigation policy is strict by default: private/internal destinations stay blocked unless you explicitly opt in.
browser.ssrfPolicy.dangerouslyAllowPrivateNetwork is unset, so browser navigation keeps private/internal/special-use destinations blocked.browser.ssrfPolicy.allowPrivateNetwork is still accepted for compatibility.browser.ssrfPolicy.dangerouslyAllowPrivateNetwork: true to allow private/internal/special-use destinations.hostnameAllowlist (patterns like *.example.com) and allowedHostnames (exact host exceptions, including blocked names like localhost) for explicit exceptions.http(s) URL after navigation to reduce redirect-based pivots.Example strict policy:
{
browser: {
ssrfPolicy: {
dangerouslyAllowPrivateNetwork: false,
hostnameAllowlist: ["*.example.com", "example.com"],
allowedHostnames: ["localhost"],
},
},
}
With multi-agent routing, each agent can have its own sandbox + tool policy: use this to give full access, read-only, or no access per agent. See Multi-Agent Sandbox & Tools for full details and precedence rules.
Common use cases:
{
agents: {
list: [
{
id: "personal",
workspace: "~/.openclaw/workspace-personal",
sandbox: { mode: "off" },
},
],
},
}
{
agents: {
list: [
{
id: "family",
workspace: "~/.openclaw/workspace-family",
sandbox: {
mode: "all",
scope: "agent",
workspaceAccess: "ro",
},
tools: {
allow: ["read"],
deny: ["write", "edit", "apply_patch", "exec", "process", "browser"],
},
},
],
},
}
{
agents: {
list: [
{
id: "public",
workspace: "~/.openclaw/workspace-public",
sandbox: {
mode: "all",
scope: "agent",
workspaceAccess: "none",
},
// Session tools can reveal sensitive data from transcripts. By default OpenClaw limits these tools
// to the current session + spawned subagent sessions, but you can clamp further if needed.
// See `tools.sessions.visibility` in the configuration reference.
tools: {
sessions: { visibility: "tree" }, // self | tree | agent | all
allow: [
"sessions_list",
"sessions_history",
"sessions_send",
"sessions_spawn",
"session_status",
"whatsapp",
"telegram",
"slack",
"discord",
],
deny: [
"read",
"write",
"edit",
"apply_patch",
"exec",
"process",
"browser",
"canvas",
"nodes",
"cron",
"gateway",
"image",
],
},
},
],
},
}
If your AI does something bad:
openclaw gateway process.gateway.bind: "loopback" (or disable Tailscale Funnel/Serve) until you understand what happened.dmPolicy: "disabled" / require mentions, and remove "*" allow-all entries if you had them.gateway.auth.token / OPENCLAW_GATEWAY_PASSWORD) and restart.gateway.remote.token / .password) on any machine that can call the Gateway.auth-profiles.json, and encrypted secrets payload values when used)./tmp/openclaw/openclaw-YYYY-MM-DD.log (or logging.file).~/.openclaw/agents/<agentId>/sessions/*.jsonl.gateway.bind, gateway.auth, dm/group policies, tools.elevated, plugin changes).openclaw security audit --deep and confirm critical findings are resolved.CI runs the pre-commit detect-private-key hook over the repository. If it
fails, remove or rotate the committed key material, then reproduce locally:
pre-commit run --all-files detect-private-key
Found a vulnerability in OpenClaw? Please report responsibly: