docs/gateway/security/index.md
~/.openclaw, including openclaw.json), treat them as a trusted operator.sessionKey (session IDs, labels) is a routing selector, not an authorization token.Before changing remote access, DM policy, reverse proxy, or public exposure, run through the Gateway exposure runbook as a pre-flight/rollback checklist.
openclaw security auditRun this after any config change or before exposing network surfaces:
openclaw security audit
openclaw security audit --deep # attempts a live Gateway probe
openclaw security audit --fix # apply safe remediations
openclaw security audit --json
--fix is intentionally narrow: it flips open group policies to allowlists, restores logging.redactSensitive: "tools", tightens state/config/include-file permissions (600 files, 700 dirs), and on Windows uses ACL resets instead of POSIX chmod.
exec/process stay available without sandbox constraints.security="full", autoAllowSkills, interpreter allowlists without strictInlineEval. security="full" alone 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 entries that look effective but only match exact command IDs (for example system.run), not shell text inside the payload; dangerous gateway.nodes.allowCommands entries; global tools.profile="minimal" overridden per agent; plugin-owned tools reachable under a permissive policy.sandbox when tools.exec.host now defaults to auto, or setting tools.exec.host="sandbox" while sandbox mode is off.Each finding has a structured checkId (for example gateway.bind_no_auth, tools.exec.security_full_configured). Prefixes: fs.* (permissions), gateway.* (bind/auth/Tailscale/Control UI/trusted-proxy), hooks.*/browser.*/sandbox.*/tools.exec.* (per-surface hardening), plugins.*/skills.* (supply chain), security.exposure.* (access policy x tool blast radius). Full catalog with severity and auto-fix support: Security audit checks. See also Formal Verification.
{
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 } } },
},
}
Keeps the Gateway local-only, isolates DMs, and disables control-plane/runtime tools by default. Re-enable tools selectively per trusted agent from there.
Built-in baseline for chat-driven agent turns: non-owner senders cannot use the cron or gateway tools regardless of config.
Quick model for triaging risk reports:
| 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" |
sessions.list / sessions.preview / chat.history) classified as IDOR in a shared-gateway setup.system.run; the real execution boundary is the gateway's global node command policy plus the node's own exec approvals.gateway.nodes.pairing.autoApproveCidrs treated as a vulnerability by itself. It is disabled by default, requires explicit CIDR/IP entries, only applies to first-time role: node pairing with no requested scopes, and never auto-approves operator/browser/Control UI, WebChat, role/scope upgrades, metadata or public-key changes, or same-host loopback trusted-proxy header paths (even when loopback trusted-proxy auth is enabled).sessionKey as an auth token.Treat Gateway and node as one operator trust domain with different roles:
gateway.auth, tool policy, routing).gateway/node is allowed without approval prompts (security="full", ask="off"). That is intentional UX, not a vulnerability by itself.For hostile-user isolation, split trust boundaries by OS user/host and run separate gateways.
Your AI assistant can execute arbitrary shell commands, read/write files, access network services, and send messages to anyone (if given channel access). People who message it can try to trick it into doing bad things, social-engineer access to your data, or probe for infrastructure details.
Most failures here are not exotic exploits - they are "someone messaged the bot and the bot did what they asked." OpenClaw's stance, in order:
Every DM-capable channel supports dmPolicy (or *.dm.policy), which gates inbound DMs before the message is processed:
| Policy | Behavior |
|---|---|
pairing | Default. Unknown senders get a pairing code; bot ignores them until approved. Codes expire after 1 hour; repeated DMs do not resend a code until a new request is created. Pending requests capped at 3 per channel. |
allowlist | Unknown senders blocked, no pairing handshake. |
open | Anyone can DM (public). Requires the channel allowlist to include "*" (explicit opt-in). |
disabled | Inbound DMs ignored entirely. |
openclaw pairing list <channel>
openclaw pairing approve <channel> <code>
Details + files on disk: Pairing
Treat dmPolicy="open" and groupPolicy="open" as last-resort settings; prefer pairing + allowlists unless you fully trust every member of the room.
allowFrom / channels.discord.allowFrom / channels.slack.allowFrom; legacy: channels.discord.dm.allowFrom, channels.slack.dm.allowFrom): who can DM the bot. When dmPolicy="pairing", approvals write to ~/.openclaw/credentials/<channel>-allowFrom.json (default account) or <channel>-<accountId>-allowFrom.json (non-default accounts), merged with config allowlists.channels.whatsapp.groups, channels.telegram.groups, channels.imessage.groups: per-group defaults like requireMention; when set, also acts as a group allowlist (include "*" to keep allow-all behavior). Customize mention triggers with agents.list[].groupChat.mentionPatterns (for example ["@openclaw", "@mybot"]) so requireMention gates on your own bot names.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, then mention/reply activation. Replying to a bot message (implicit mention) does not bypass groupAllowFrom.Details: Configuration and Groups
By default, OpenClaw routes all DMs into the main session for cross-device continuity. If multiple people can DM the bot (open DMs or a multi-person allowlist), isolate DM sessions:
{ session: { dmScope: "per-channel-peer" } }
session.dmScope values:
| Value | Scope |
|---|---|
main (config default) | All DMs share one session. |
per-channel-peer | Each channel+sender pair gets an isolated DM context (secure DM mode). |
per-account-channel-peer | Like above, split further by account (multi-account channels). |
per-peer | Each sender gets one session across all channels of the same type. |
Local CLI onboarding writes session.dmScope: "per-channel-peer" when unset, and preserves any explicit existing value.
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.
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.
Two separate concepts:
dmPolicy, groupPolicy, allowlists, mention gates).contextVisibility controls the second:
"all" (default): supplemental context kept as received."allowlist": supplemental context filtered to senders allowed by active allowlist checks."allowlist_quote": like allowlist, but still keeps one explicit quoted reply.Set per channel or per room/conversation - see Groups. Reports that only show "model can see quoted/historical text from non-allowlisted senders" are hardening findings addressable with contextVisibility, not auth or sandbox bypasses by themselves; a security-impacting report still needs a demonstrated trust-boundary bypass.
An attacker crafts a message that manipulates the model into unsafe action ("ignore your instructions", "dump your filesystem", "follow this link and run commands"). Prompt injection is not solved by system prompt guardrails alone - those are soft guidance; hard enforcement comes from tool policy, exec approvals, sandboxing, and channel allowlists (which operators can still disable by design).
Prompt injection does not require public DMs: even if only you can message the bot, any untrusted content it reads (web search/fetch results, browser pages, emails, docs, attachments, pasted logs/code) can carry adversarial instructions. The content itself is a threat surface, not just the sender.
Red flags to treat as untrusted:
What helps in practice:
host=auto resolves to the gateway host, while explicit host=sandbox still fails closed (no sandbox runtime available). Set host=gateway to make that behavior 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 (-c, -e, and similar) still need explicit approval. In allowlist mode, any heredoc segment (<<) always requires reviewer or explicit approval, regardless of quoting - an allowlisted command cannot use a heredoc body to bypass allowlist review.web_search / web_fetch / browser off for tool-enabled agents unless needed.input_file / input_image), set a tight gateway.http.endpoints.responses.files.urlAllowlist / images.urlAllowlist and keep maxUrlParts low (empty allowlists count as unset). Use files.allowUrl: false / images.allowUrl: false to disable URL fetching entirely.Model choice matters. Prompt-injection resistance is not uniform across model tiers - smaller/cheaper models are more susceptible to tool misuse and instruction hijacking 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>web_search/web_fetch/browser unless inputs are tightly controlled.OpenResponses input_file text is still injected as untrusted external content even though the Gateway decodes it locally - the block carries <<<EXTERNAL_UNTRUSTED_CONTENT ...>>> boundary markers plus Source: External metadata (this path omits the longer SECURITY NOTICE: banner used elsewhere). The same marker-based wrapping applies when media-understanding extracts text from attached documents before appending it to the media prompt.
OpenClaw also strips common self-hosted LLM chat-template special-token literals (Qwen/ChatML, Llama, Gemma, Mistral, Phi, GPT-OSS role/turn tokens) from wrapped external content and metadata before they reach the model. Self-hosted OpenAI-compatible backends (vLLM, SGLang, TGI, LM Studio, custom Hugging Face tokenizer stacks) sometimes tokenize literal strings like <|im_start|> or <|start_header_id|> as structural chat-template tokens inside user content; without this sanitization, untrusted text in a fetched page, email body, or file-contents tool output could forge a synthetic assistant/system role boundary. Sanitization happens at the external-content wrapping layer, so it applies uniformly across fetch/read tools and inbound channel content. Hosted providers (OpenAI, Anthropic) already apply their own request-side sanitization; keep external-content wrapping enabled and prefer backend settings that split/escape special tokens when available.
Outbound model responses have a separate sanitizer that strips leaked <tool_call>, <function_calls>, ``, <previous_response>, and similar internal scaffolding from user-visible replies at the final channel delivery boundary.
This does not replace dmPolicy, allowlists, exec approvals, sandboxing, or contextVisibility - it closes one specific tokenizer-layer bypass.
hooks.mappings[].allowUnsafeExternalContenthooks.gmail.allowUnsafeExternalContentallowUnsafeExternalContentOnly enable temporarily for tightly scoped debugging; if enabled, isolate that agent (sandbox + minimal tools + dedicated session namespace).
Hook payloads are untrusted content even when delivery comes from systems you control (mail/docs/web content can carry prompt injection). Weak model tiers increase this risk - for hook-driven automation, prefer strong modern model tiers and keep tool policy tight (tools.profile: "messaging" or stricter), plus sandboxing where possible.
/reasoning, /verbose, and /trace can expose internal reasoning, tool output, or plugin diagnostics not meant for a public channel - they can include tool args, URLs, plugin diagnostics, and data the model saw. Keep them disabled in public rooms; enable only in trusted DMs or tightly controlled rooms.
Slash commands and directives are honored only for authorized senders, 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 changes:
gateway inspects config with config.schema.lookup / config.get, and mutates with config.apply, config.patch, and update.run.cron creates scheduled jobs that keep running after the original chat/task ends.gateway config.apply/config.patch are fail-closed by default: only a narrow allowlist of low-risk agent runtime tuning (agents.defaults.thinkingDefault, per-agent model/thinking/reasoning/fast-mode fields), mention-gating (channels.*.requireMention at several nesting depths), and visible-reply settings (messages.visibleReplies, messages.groupChat.visibleReplies, messages.groupChat.unmentionedInbound) are agent-tunable. Any other changed config path is rejected. Global model defaults and prompt overlays stay operator-controlled, and new sensitive config trees are protected unless deliberately added to that allowlist. The tool still refuses to rewrite tools.exec.ask or tools.exec.security; legacy tools.bash.* aliases normalize to the equivalent tools.exec.* path before the write is checked.
For any agent/surface handling 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.
system.run)If a macOS node is paired, the Gateway can invoke system.run on it - this is remote code execution on that Mac.
gateway.nodes.allowCommands / denyCommands. denyCommands matches exact node command names only (for example system.run), not shell text inside a command payload - a reconnecting node advertising a different command list is not, by itself, a vulnerability if the gateway global policy and the node's own exec approvals still enforce the boundary.system.run policy is the node's own exec approvals file (exec.approvals.node.*), controlled on the Mac via Settings -> Exec approvals (security + ask + allowlist); it can be stricter or looser than the gateway's global command-ID policy.security="full" and ask="off" follows the default trusted-operator model - expected behavior, not a bug, unless your deployment needs a tighter 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.deny and remove node pairing for that Mac.OpenClaw can refresh the skills list mid-session: the skills watcher updates the snapshot on the next agent turn when SKILL.md changes, and connecting a macOS node can make macOS-only skills eligible (based on bin probing). Treat skill folders as trusted code and restrict who can modify them.
Plugins run in-process with the Gateway - treat them as trusted code.
plugins.allow allowlists; review plugin config before enabling; restart the Gateway after plugin changes.openclaw plugins install <package>, openclaw plugins update <id>) runs untrusted code:
security.installPolicy for operator-owned local allow/block decisions and openclaw security audit --deep for diagnostic scanning.npm install.@scope/[email protected]) and inspect the unpacked code before enabling.--dangerously-force-unsafe-install is deprecated and no longer changes install/update behavior.security.installPolicy lets operators run a trusted local command to make host-specific allow/block decisions for skill and plugin installs. It runs after source material is staged but before install continues, applies to ClawHub skills too, and is not bypassed by deprecated unsafe flags.Details: Plugins
Dedicated doc: Sandboxing
Two complementary approaches:
agents.defaults.sandbox; host gateway + sandbox-isolated tools; Docker is the default backend): SandboxingAgent workspace access inside the sandbox (agents.defaults.sandbox.workspaceAccess):
"none" (default): tools see a sandbox workspace under ~/.openclaw/sandboxes; agent workspace is off-limits."ro": mounts the agent workspace read-only at /agent (disables write/edit/apply_patch)."rw": mounts the agent workspace read/write at /workspace.Extra sandbox.docker.binds are validated against normalized, canonicalized source paths. A blocked-path denylist covers /etc, /private/etc, /proc, /sys, /dev, /root, /boot, and directories that commonly contain or alias the Docker socket (/run, /var/run, and docker.sock under them), plus HOME credential subpaths (.aws, .cargo, .config, .docker, .gnupg, .netrc, .npm, .ssh). Parent-symlink tricks and canonical home aliases are resolved through existing ancestors and re-checked, so they still fail closed if they resolve into a blocked root.
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"); "require" fails fast when the target child runtime is not sandboxed.Build a read-only profile by combining agents.defaults.sandbox.workspaceAccess: "ro" (or "none" for no workspace access) with tool allow/deny lists that block write, edit, apply_patch, exec, process, etc.
tools.exec.applyPatch.workspaceOnly: true (default): keeps apply_patch from writing/deleting outside the workspace directory even with sandboxing off. Set 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.~/.openclaw) to filesystem tools.Each agent can have its own sandbox + tool policy: full access, read-only, or no access. See Multi-Agent Sandbox & Tools for precedence rules.
Common patterns: personal agent (full access, no sandbox), family/work agent (sandboxed + read-only tools), public agent (sandboxed + no filesystem/shell tools).
{
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" },
tools: {
// Session tools can reveal transcript data. Default scope is current session +
// spawned subagent sessions; clamp further with tools.sessions.visibility if needed.
sessions: { visibility: "tree" }, // self | tree | agent | all
allow: [
"sessions_list",
"sessions_history",
"sessions_send",
"sessions_spawn",
"session_status",
"discord",
"slack",
"telegram",
"whatsapp",
],
deny: [
"apply_patch",
"browser",
"canvas",
"cron",
"edit",
"exec",
"gateway",
"image",
"nodes",
"process",
"read",
"write",
],
},
},
],
},
}
Enabling browser control gives the model a real browser. If that profile already has logged-in sessions, the model can access those accounts and data - treat browser profiles as sensitive state.
openclaw profile); avoid your personal daily-driver profile.gateway.nodes.browser.mode="off").Private/internal destinations stay blocked unless you explicitly opt in.
browser.ssrfPolicy.dangerouslyAllowPrivateNetwork unset, so private/internal/special-use destinations stay blocked. Legacy alias allowPrivateNetwork still accepted.dangerouslyAllowPrivateNetwork: true to allow those destinations.hostnameAllowlist (patterns like *.example.com) and allowedHostnames (exact host exceptions, including otherwise-blocked names like localhost) for explicit exceptions.http(s) URL after navigation, to reduce redirect-based pivots.{
browser: {
ssrfPolicy: {
dangerouslyAllowPrivateNetwork: false,
hostnameAllowlist: ["*.example.com", "example.com"],
allowedHostnames: ["localhost"],
},
},
}
The Gateway multiplexes WebSocket + HTTP on one port (default 18789; config/flags/env: gateway.port, --port, OPENCLAW_GATEWAY_PORT). That HTTP surface includes the Control UI (SPA assets, default base path /) and the canvas host (/__openclaw__/canvas and /__openclaw__/a2ui - arbitrary HTML/JS; treat as untrusted content when loaded in a normal browser; do not expose it to untrusted networks/users or share an origin with privileged web surfaces).
gateway.bind controls where the Gateway listens:
"loopback" (default): only local clients can connect."lan", "tailnet", "custom": expand the attack surface. Only use with gateway auth (shared token/password, or a correctly configured trusted proxy) and a real firewall.Rules of thumb: prefer Tailscale Serve over LAN binds (Serve keeps the Gateway on loopback and Tailscale handles access); if you must bind to LAN, firewall the port to a tight source-IP allowlist rather than port-forwarding broadly; never expose the Gateway unauthenticated on 0.0.0.0.
Published container ports (-p HOST:CONTAINER or Compose ports:) route through Docker's forwarding chains, not only host INPUT rules. Enforce rules in DOCKER-USER (evaluated before Docker's own accept rules); most modern distros use the iptables-nft frontend, which still applies these rules to the nftables backend.
# /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 (eth0) since they vary across VPS images (ens3, enp*, etc.) and a mismatch can silently skip your deny rule.
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 + reverse proxy ports).
When the bundled bonjour plugin is enabled, the Gateway broadcasts presence via mDNS (_openclaw-gw._tcp, port 5353) for local device discovery. Full mode includes TXT records that expose operational details: cliPath (filesystem path revealing username and install location), sshPort (advertises SSH availability), displayName/lanHost (hostname info). Broadcasting infrastructure details makes LAN reconnaissance easier.
Keep Bonjour disabled unless LAN discovery is needed - it 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) omits sensitive fields:
{ discovery: { mdns: { mode: "minimal" } } }
Off suppresses local discovery while keeping the plugin enabled:
{ discovery: { mdns: { mode: "off" } } }
Full mode (opt-in) includes cliPath + sshPort:
{ discovery: { mdns: { mode: "full" } } }
Or set OPENCLAW_DISABLE_BONJOUR=1 to disable mDNS without config changes.
In minimal mode the Gateway broadcasts role, gatewayPort, transport but omits cliPath/sshPort; apps that need the CLI path can fetch it over the authenticated WebSocket connection instead.
Gateway auth is required by default - with no valid auth path configured, the Gateway refuses WebSocket connections (fail-closed). Onboarding generates a token by default (even for loopback) so local clients must authenticate.
{ gateway: { auth: { mode: "token", token: "your-token" } } }
openclaw doctor --generate-gateway-token can generate one for you.
Pin remote TLS with gateway.remote.tlsFingerprint when using wss://. Plaintext ws:// is accepted for loopback, private IP literals, .local, and Tailnet *.ts.net gateway URLs; for other trusted private-DNS names, set OPENCLAW_ALLOW_INSECURE_PRIVATE_WS=1 on the client process as break-glass (process environment only, not an openclaw.json key). Mobile pairing and Android manual/scanned gateway routes are stricter: cleartext only for loopback, while private-LAN, link-local, .local, and dotless hostnames must use TLS unless you explicitly opt into the trusted private-network cleartext path.
Device pairing is auto-approved for direct local loopback connects (plus a narrow backend/container-local self-connect path for trusted shared-secret helper flows); Tailnet and LAN connects, including same-host tailnet binds, are treated as remote and still need approval. Forwarded-header evidence on a loopback request disqualifies loopback locality; metadata-upgrade auto-approval is scoped narrowly. See Gateway pairing.
Auth modes:
"token": shared bearer token (recommended for most setups)."password": prefer setting via OPENCLAW_GATEWAY_PASSWORD."trusted-proxy": trust an identity-aware reverse proxy to authenticate users and pass identity via headers. See Trusted Proxy Auth.Rotation checklist (token/password): generate/set a new secret (gateway.auth.token or OPENCLAW_GATEWAY_PASSWORD); restart the Gateway (or the macOS app if it supervises the Gateway); update remote clients (gateway.remote.token/.password); verify the old credentials no longer work.
When gateway.auth.allowTailscale is true (default for Serve), OpenClaw accepts the Tailscale Serve identity header tailscale-user-login for Control UI/WebSocket authentication. It verifies 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 loopback requests carrying x-forwarded-for, x-forwarded-proto, and x-forwarded-host as injected by Tailscale. For this async check, failed attempts for the same {scope, ip} are serialized before the limiter records the failure, so concurrent bad retries from one Serve client can lock out the second attempt immediately.
HTTP API endpoints (/v1/*, /tools/invoke, /api/channels/*) do not use Tailscale identity-header auth - they follow the gateway's configured HTTP auth mode.
Gateway HTTP bearer auth is effectively all-or-nothing operator access. Credentials that can call /v1/chat/completions, /v1/responses, plugin routes such as /api/v1/admin/rpc, or /api/channels/* are full-access operator secrets for that gateway: shared-secret bearer auth restores the full default operator scopes (operator.admin, operator.approvals, operator.pairing, operator.read, operator.talk.secrets, operator.write) and owner semantics for agent turns, and narrower x-openclaw-scopes values do not reduce that shared-secret path. Per-request scope semantics only apply when the request comes from an identity-bearing mode (trusted proxy auth) or an explicitly no-auth private ingress; in those modes, omitting x-openclaw-scopes falls back to the normal operator default scope set, and owner-level headers like x-openclaw-model require operator.admin when scopes are narrowed. /tools/invoke and HTTP session history endpoints follow the same shared-secret rule. Do not share these credentials with untrusted callers; prefer separate gateways per trust boundary.
Tokenless Serve auth assumes the gateway host itself is trusted - it is not protection against hostile same-host processes. If untrusted local code may run on the gateway host, disable allowTailscale and require explicit shared-secret auth (token or password).
Do not forward these headers from your own reverse proxy. If you terminate TLS or proxy in front of the gateway, disable allowTailscale and use shared-secret auth or Trusted Proxy Auth instead.
See Tailscale and Web overview.
Set gateway.trustedProxies for proper forwarded-client IP handling behind nginx/Caddy/Traefik/etc. When the Gateway detects proxy headers from an address not in trustedProxies, it will not treat the connection as local; if gateway auth is disabled, that connection is rejected. This prevents proxied connections from appearing to come from localhost and receiving automatic trust.
trustedProxies also feeds gateway.auth.mode: "trusted-proxy", which is stricter: it fails closed on loopback-source proxies by default. Same-host loopback reverse proxies can use trustedProxies for local-client detection and forwarded-IP handling, but can only satisfy trusted-proxy auth mode when gateway.auth.trustedProxy.allowLoopback = true; otherwise use token/password auth.
gateway:
trustedProxies:
- "10.0.0.1" # reverse proxy IP
allowRealIpFallback: false # default false; only enable if your proxy cannot provide X-Forwarded-For
auth:
mode: password
password: ${OPENCLAW_GATEWAY_PASSWORD}
When trustedProxies is set, the Gateway uses X-Forwarded-For to determine client IP; X-Real-IP is ignored unless gateway.allowRealIpFallback: true is explicitly set. Ensure your proxy overwrites X-Forwarded-For/X-Real-IP rather than appending to them:
# good
proxy_set_header X-Forwarded-For $remote_addr;
proxy_set_header X-Real-IP $remote_addr;
# bad: preserves/appends untrusted client-supplied values
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
Trusted proxy headers do not make node device pairing automatically trusted - gateway.nodes.pairing.autoApproveCidrs is a separate, disabled-by-default operator policy, and loopback-source trusted-proxy header paths stay excluded from node auto-approval even when loopback trusted-proxy auth is enabled (because local callers can forge those headers).
gateway.http.securityHeaders.strictTransportSecurity emits the HSTS header from OpenClaw responses.gateway.controlUi.allowedOrigins by default; allowedOrigins: ["*"] is an explicit allow-all 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.The Control UI needs a secure context (HTTPS or localhost) to generate device identity.
gateway.controlUi.allowInsecureAuth: local compatibility toggle. On localhost, allows Control UI auth without device identity when the page loads over non-secure HTTP. Does not bypass pairing checks and does not relax remote (non-localhost) device identity requirements. Prefer HTTPS (Tailscale Serve) or open the UI on 127.0.0.1.gateway.controlUi.dangerouslyDisableDeviceAuth: break-glass only, disables device identity checks entirely. Severe security downgrade; keep off unless actively debugging and able to revert quickly.gateway.auth.mode: "trusted-proxy" can admit operator Control UI sessions without device identity - an intentional auth-mode behavior, not an allowInsecureAuth shortcut, and it does not extend to node-role Control UI sessions.openclaw security audit warns when allowInsecureAuth is enabled.
openclaw security audit raises config.insecure_or_dangerous_flags for each enabled known insecure/dangerous debug switch (one finding per flag). Keep these unset in production. If audit suppressions are configured, security.audit.suppressions.active stays in the active output even when matching findings move to suppressedFindings.
Channel name-matching (bundled and plugin channels; also per `accounts.<accountId>` where applicable):
- `channels.discord.dangerouslyAllowNameMatching`
- `channels.googlechat.dangerouslyAllowNameMatching`
- `channels.msteams.dangerouslyAllowNameMatching`
- `channels.slack.dangerouslyAllowNameMatching`
- `channels.irc.dangerouslyAllowNameMatching` (plugin channel)
- `channels.mattermost.dangerouslyAllowNameMatching` (plugin channel)
- `channels.synology-chat.dangerouslyAllowNameMatching` (plugin channel)
- `channels.synology-chat.dangerouslyAllowInheritedWebhookPath` (plugin channel)
- `channels.zalouser.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`
pnpm-lock.yaml; the published openclaw npm package and OpenClaw-owned npm plugin packages include npm-shrinkwrap.json so installs use the reviewed transitive dependency graph from the release instead of resolving a fresh graph at install time. This is a supply-chain hardening and release reproducibility boundary, not a sandbox - see npm shrinkwrap.@openclaw/fs-safe for root-bounded file access, atomic writes, archive extraction, temp workspaces, and secret-file helpers. The optional POSIX Python helper defaults off; set OPENCLAW_FS_SAFE_PYTHON_MODE=auto or require only when you want the extra fd-relative mutation hardening and can support a Python runtime. Details: Secure file operations.exec, browser, network/file tools) within the agent's policy, prompt/content injection from one sender can affect shared state/devices/outputs, and if the shared agent has sensitive credentials/files, any allowed sender can potentially drive exfiltration via tool usage. Use separate agents/gateways with minimal tools for team workflows; keep personal-data agents private.Assume anything under ~/.openclaw/ (or $OPENCLAW_STATE_DIR/) may contain secrets or private data:
| Path | Contents |
|---|---|
openclaw.json | Config may include tokens (gateway, remote gateway), provider settings, and allowlists. |
credentials/** | Channel credentials (for example WhatsApp creds), pairing allowlists, legacy OAuth imports. |
agents/<agentId>/agent/auth-profiles.json | API keys, token profiles, OAuth tokens, optional keyRef/tokenRef. |
agents/<agentId>/agent/codex-home/** | Per-agent Codex app-server account, config, skills, plugins, native thread state, diagnostics (default). |
$CODEX_HOME/** or ~/.codex/** | Opt-in shared Codex runtime state, only when plugins.entries.codex.config.appServer.homeScope is "user". Uses the native Codex account, config, plugins, and thread store; enable only for an owner-controlled local Gateway. See Codex harness. |
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. |
| bundled plugin packages | Installed plugins (plus their node_modules/). |
sandboxes/** | Tool sandbox workspaces; can accumulate copies of files read/written inside the sandbox. |
Also useful for backup decisions:
~/.openclaw/credentials/whatsapp/<accountId>/creds.jsonchannels.telegram.tokenFile (regular file only; symlinks rejected)channels.slack.*)~/.openclaw/credentials/<channel>-allowFrom.json (default account) / <channel>-<accountId>-allowFrom.json (non-default accounts)~/.openclaw/agents/<agentId>/agent/auth-profiles.json~/.openclaw/credentials/oauth.jsonHardening: keep permissions tight (700 on dirs, 600 on files); use full-disk encryption on the gateway host; prefer a dedicated OS user account if the host is shared.
~/.openclaw/openclaw.json: 600 (user read/write only)~/.openclaw: 700 (user only)openclaw doctor can warn and offer to tighten these.
.env filesOpenClaw loads workspace-local .env files for agents and tools, but never lets them silently override gateway runtime controls:
.env files - for example GEMINI_API_KEY, GOOGLE_API_KEY, XAI_API_KEY, MISTRAL_API_KEY, GROQ_API_KEY, DEEPSEEK_API_KEY, PERPLEXITY_API_KEY, BRAVE_API_KEY, TAVILY_API_KEY, EXA_API_KEY, FIRECRAWL_API_KEY, and provider auth keys declared by installed trusted plugins. Put provider credentials in the Gateway process environment, ~/.openclaw/.env ($OPENCLAW_STATE_DIR/.env), the config env block, or an optional login-shell import instead.OPENCLAW_ is blocked from untrusted workspace .env files, reserving the whole runtime namespace so a future OPENCLAW_* control is fail-closed by default rather than silently inheritable from checked-in or attacker-supplied .env content..env overrides (for example MATRIX_HOMESERVER, MATTERMOST_URL, IRC_HOST, SYNOLOGY_CHAT_INCOMING_URL), so a cloned workspace cannot redirect bundled connector traffic through local endpoint config. These must come from the gateway process environment or env.shellEnv.env, and enabled login-shell import still apply - this only constrains workspace .env file loading.Workspace .env files frequently live next to agent code, get committed by accident, or get written by tools; blocking provider credentials prevents a cloned workspace from substituting attacker-controlled provider accounts.
OpenClaw stores session transcripts on disk under ~/.openclaw/agents/<agentId>/sessions/*.jsonl for session continuity and optional memory indexing - any process/user with filesystem access can read them. Treat disk access as the trust boundary and lock down ~/.openclaw permissions; run agents under separate OS users or hosts for stronger isolation.
Gateway logs may include tool summaries, errors, and URLs; session transcripts can include pasted secrets, file contents, command output, and links.
logging.redactSensitive: "tools", default).logging.redactPatterns (tokens, hostnames, internal URLs).openclaw status --all (pasteable, secrets redacted) over raw logs.Details: Logging
{
gateway: {
mode: "local",
bind: "loopback",
port: 18789,
auth: { mode: "token", token: "your-long-random-token" },
},
channels: {
whatsapp: {
dmPolicy: "pairing",
groups: { "*": { requireMention: true } },
},
},
}
Keeps the Gateway private, requires DM pairing, and avoids always-on group bots. For safer tool execution too, add a sandbox + deny dangerous tools for any non-owner agent (see "Per-agent access profiles" above).
For phone-number-based channels, consider running the assistant on a separate number from your personal one, so personal conversations stay private and the bot number handles automation with its own boundaries.
openclaw gateway process.gateway.bind: "loopback" (or disable Tailscale Funnel/Serve) until you understand what happened.dmPolicy: "disabled" / require mentions, and remove any "*" allow-all entries.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? Report responsibly: