Vendor OpenClaw source as Adolf fork baseline
Some checks failed
ClawSweeper Dispatch / dispatch (push) Has been cancelled
CodeQL / Security High (actions) (push) Has been cancelled
CodeQL / Security High (channel-runtime-boundary) (push) Has been cancelled
CodeQL / Security High (core-auth-secrets) (push) Has been cancelled
CodeQL / Security High (mcp-process-tool-boundary) (push) Has been cancelled
CodeQL / Security High (network-ssrf-boundary) (push) Has been cancelled
CodeQL / Security High (plugin-trust-boundary) (push) Has been cancelled
CodeQL / Security High (process-exec-boundary) (push) Has been cancelled
Docs Sync Publish Repo / sync-publish-repo (push) Has been cancelled
Docs / docs (push) Has been cancelled
OpenClaw Stable Main Closeout / Resolve stable release closeout inputs (push) Has been cancelled
OpenClaw Stable Main Closeout / Verify stable main closeout (push) Has been cancelled
Workflow Sanity / no-tabs (push) Has been cancelled
Workflow Sanity / actionlint (push) Has been cancelled
Workflow Sanity / generated-doc-baselines (push) Has been cancelled
CI / runner-admission (push) Has been cancelled
CI / preflight (push) Has been cancelled
CI / security-fast (push) Has been cancelled
CI / pnpm-store-warmup (push) Has been cancelled
CI / build-artifacts (push) Has been cancelled
CI / native-i18n (push) Has been cancelled
CI / ${{ matrix.check_name }} (push) Has been cancelled
CI / ${{ matrix.checkName }} (push) Has been cancelled
CI / checks-node-compat-node22 (push) Has been cancelled
CI / check-bundled-channel-config-metadata (push) Has been cancelled
CI / check-dependencies (push) Has been cancelled
CI / check-guards (push) Has been cancelled
CI / check-lint (push) Has been cancelled
CI / check-prod-types (push) Has been cancelled
CI / check-shrinkwrap (push) Has been cancelled
CI / check-test-types (push) Has been cancelled
CI / check-additional-boundaries-a (push) Has been cancelled
CI / check-additional-boundaries-bcd (push) Has been cancelled
CI / check-additional-extension-bundled (push) Has been cancelled
CI / check-additional-extension-channels (push) Has been cancelled
CI / check-additional-extension-package-boundary (push) Has been cancelled
CI / check-additional-runtime-topology-architecture (push) Has been cancelled
CI / check-session-accessor-boundary (push) Has been cancelled
CI / check-session-transcript-reader-boundary (push) Has been cancelled
CI / check-docs (push) Has been cancelled
CI / skills-python (push) Has been cancelled
CI / macos-swift (push) Has been cancelled
CI / ios-build (push) Has been cancelled
CI / ci-timings-summary (push) Has been cancelled
Native App Locale Refresh / Refresh native fa (push) Has been cancelled
Native App Locale Refresh / Refresh native fr (push) Has been cancelled
Native App Locale Refresh / Refresh native hi (push) Has been cancelled
Native App Locale Refresh / Refresh native id (push) Has been cancelled
Native App Locale Refresh / Refresh native it (push) Has been cancelled
Native App Locale Refresh / Refresh native ja-JP (push) Has been cancelled
Control UI Locale Refresh / plan (push) Has been cancelled
Control UI Locale Refresh / Refresh ${{ matrix.locale }} (push) Has been cancelled
Control UI Locale Refresh / Commit control UI locale refresh (push) Has been cancelled
Live Media Runner Image / Build live media runner image (push) Has been cancelled
Native App Locale Refresh / Refresh native ar (push) Has been cancelled
Native App Locale Refresh / Refresh native de (push) Has been cancelled
Native App Locale Refresh / Refresh native es (push) Has been cancelled
Native App Locale Refresh / Refresh native ko (push) Has been cancelled
Native App Locale Refresh / Refresh native nl (push) Has been cancelled
Native App Locale Refresh / Refresh native pl (push) Has been cancelled
Native App Locale Refresh / Refresh native pt-BR (push) Has been cancelled
Native App Locale Refresh / Refresh native ru (push) Has been cancelled
Native App Locale Refresh / Refresh native sv (push) Has been cancelled
Native App Locale Refresh / Refresh native th (push) Has been cancelled
Native App Locale Refresh / Refresh native tr (push) Has been cancelled
Native App Locale Refresh / Refresh native uk (push) Has been cancelled
Native App Locale Refresh / Refresh native vi (push) Has been cancelled
Native App Locale Refresh / Refresh native zh-CN (push) Has been cancelled
Native App Locale Refresh / Refresh native zh-TW (push) Has been cancelled
Native App Locale Refresh / Commit native locale refresh (push) Has been cancelled
Plugin Init Scaffold Validation / Validate provider scaffold (push) Has been cancelled
Plugin NPM Release / preview_plugins_npm (push) Has been cancelled
Plugin NPM Release / Validate release publish approval (push) Has been cancelled
Plugin NPM Release / preview_plugin_pack (push) Has been cancelled
Plugin NPM Release / publish_plugins_npm (push) Has been cancelled
Sandbox Common Smoke / sandbox-common-smoke (push) Has been cancelled
Website Installer Sync / static (push) Has been cancelled
Website Installer Sync / linux-docker (push) Has been cancelled
Website Installer Sync / macos-installer (push) Has been cancelled
Website Installer Sync / windows-installer (push) Has been cancelled
Website Installer Sync / sync-website (push) Has been cancelled
Some checks failed
ClawSweeper Dispatch / dispatch (push) Has been cancelled
CodeQL / Security High (actions) (push) Has been cancelled
CodeQL / Security High (channel-runtime-boundary) (push) Has been cancelled
CodeQL / Security High (core-auth-secrets) (push) Has been cancelled
CodeQL / Security High (mcp-process-tool-boundary) (push) Has been cancelled
CodeQL / Security High (network-ssrf-boundary) (push) Has been cancelled
CodeQL / Security High (plugin-trust-boundary) (push) Has been cancelled
CodeQL / Security High (process-exec-boundary) (push) Has been cancelled
Docs Sync Publish Repo / sync-publish-repo (push) Has been cancelled
Docs / docs (push) Has been cancelled
OpenClaw Stable Main Closeout / Resolve stable release closeout inputs (push) Has been cancelled
OpenClaw Stable Main Closeout / Verify stable main closeout (push) Has been cancelled
Workflow Sanity / no-tabs (push) Has been cancelled
Workflow Sanity / actionlint (push) Has been cancelled
Workflow Sanity / generated-doc-baselines (push) Has been cancelled
CI / runner-admission (push) Has been cancelled
CI / preflight (push) Has been cancelled
CI / security-fast (push) Has been cancelled
CI / pnpm-store-warmup (push) Has been cancelled
CI / build-artifacts (push) Has been cancelled
CI / native-i18n (push) Has been cancelled
CI / ${{ matrix.check_name }} (push) Has been cancelled
CI / ${{ matrix.checkName }} (push) Has been cancelled
CI / checks-node-compat-node22 (push) Has been cancelled
CI / check-bundled-channel-config-metadata (push) Has been cancelled
CI / check-dependencies (push) Has been cancelled
CI / check-guards (push) Has been cancelled
CI / check-lint (push) Has been cancelled
CI / check-prod-types (push) Has been cancelled
CI / check-shrinkwrap (push) Has been cancelled
CI / check-test-types (push) Has been cancelled
CI / check-additional-boundaries-a (push) Has been cancelled
CI / check-additional-boundaries-bcd (push) Has been cancelled
CI / check-additional-extension-bundled (push) Has been cancelled
CI / check-additional-extension-channels (push) Has been cancelled
CI / check-additional-extension-package-boundary (push) Has been cancelled
CI / check-additional-runtime-topology-architecture (push) Has been cancelled
CI / check-session-accessor-boundary (push) Has been cancelled
CI / check-session-transcript-reader-boundary (push) Has been cancelled
CI / check-docs (push) Has been cancelled
CI / skills-python (push) Has been cancelled
CI / macos-swift (push) Has been cancelled
CI / ios-build (push) Has been cancelled
CI / ci-timings-summary (push) Has been cancelled
Native App Locale Refresh / Refresh native fa (push) Has been cancelled
Native App Locale Refresh / Refresh native fr (push) Has been cancelled
Native App Locale Refresh / Refresh native hi (push) Has been cancelled
Native App Locale Refresh / Refresh native id (push) Has been cancelled
Native App Locale Refresh / Refresh native it (push) Has been cancelled
Native App Locale Refresh / Refresh native ja-JP (push) Has been cancelled
Control UI Locale Refresh / plan (push) Has been cancelled
Control UI Locale Refresh / Refresh ${{ matrix.locale }} (push) Has been cancelled
Control UI Locale Refresh / Commit control UI locale refresh (push) Has been cancelled
Live Media Runner Image / Build live media runner image (push) Has been cancelled
Native App Locale Refresh / Refresh native ar (push) Has been cancelled
Native App Locale Refresh / Refresh native de (push) Has been cancelled
Native App Locale Refresh / Refresh native es (push) Has been cancelled
Native App Locale Refresh / Refresh native ko (push) Has been cancelled
Native App Locale Refresh / Refresh native nl (push) Has been cancelled
Native App Locale Refresh / Refresh native pl (push) Has been cancelled
Native App Locale Refresh / Refresh native pt-BR (push) Has been cancelled
Native App Locale Refresh / Refresh native ru (push) Has been cancelled
Native App Locale Refresh / Refresh native sv (push) Has been cancelled
Native App Locale Refresh / Refresh native th (push) Has been cancelled
Native App Locale Refresh / Refresh native tr (push) Has been cancelled
Native App Locale Refresh / Refresh native uk (push) Has been cancelled
Native App Locale Refresh / Refresh native vi (push) Has been cancelled
Native App Locale Refresh / Refresh native zh-CN (push) Has been cancelled
Native App Locale Refresh / Refresh native zh-TW (push) Has been cancelled
Native App Locale Refresh / Commit native locale refresh (push) Has been cancelled
Plugin Init Scaffold Validation / Validate provider scaffold (push) Has been cancelled
Plugin NPM Release / preview_plugins_npm (push) Has been cancelled
Plugin NPM Release / Validate release publish approval (push) Has been cancelled
Plugin NPM Release / preview_plugin_pack (push) Has been cancelled
Plugin NPM Release / publish_plugins_npm (push) Has been cancelled
Sandbox Common Smoke / sandbox-common-smoke (push) Has been cancelled
Website Installer Sync / static (push) Has been cancelled
Website Installer Sync / linux-docker (push) Has been cancelled
Website Installer Sync / macos-installer (push) Has been cancelled
Website Installer Sync / windows-installer (push) Has been cancelled
Website Installer Sync / sync-website (push) Has been cancelled
Adolf is a fork/vendored clone of github.com/openclaw/openclaw (v2026.6.11), free to diverge. Tree copied sans upstream .git; upstream remote added for future syncs. Node pinned to 24 (.nvmrc); engines already require >=22.19. Preserves docs/ARCHITECTURE.md. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01LeqyaxJF2nbRXJtae2kNB2
This commit is contained in:
153
docs/gateway/security/audit-checks.md
Normal file
153
docs/gateway/security/audit-checks.md
Normal file
@@ -0,0 +1,153 @@
|
||||
---
|
||||
summary: "Reference catalog of checkIds emitted by openclaw security audit"
|
||||
read_when:
|
||||
- You saw a specific `checkId` in `openclaw security audit` output and want to know what it means
|
||||
- You need the fix key/path for a given finding
|
||||
- You are triaging severity across a security audit run
|
||||
title: "Security audit checks"
|
||||
---
|
||||
|
||||
`openclaw security audit` emits structured findings keyed by `checkId`. This
|
||||
page is the reference catalog for those IDs. For the high-level threat model
|
||||
and hardening guidance, see [Security](/gateway/security).
|
||||
|
||||
Some checks only run with `openclaw security audit --deep`: plugin/skill code
|
||||
scans (`plugins.code_safety*`, `skills.code_safety*`) and live Gateway probe
|
||||
checks (`gateway.probe_*`). Everything else in this table runs on a plain
|
||||
`openclaw security audit`.
|
||||
|
||||
A severity like `warn/critical` means the same `checkId` can be emitted at
|
||||
either level depending on config (for example, whether the Gateway is remotely
|
||||
exposed). High-signal values you will most likely see in real deployments (not
|
||||
exhaustive):
|
||||
|
||||
| `checkId` | Severity | Why it matters | Primary fix key/path | Auto-fix |
|
||||
| --------------------------------------------------------------- | ------------------ | --------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------- | -------- |
|
||||
| `fs.state_dir.perms_world_writable` | critical | Other users/processes can modify full OpenClaw state | filesystem perms on `~/.openclaw` | yes |
|
||||
| `fs.state_dir.perms_group_writable` | warn | Group users can modify full OpenClaw state | filesystem perms on `~/.openclaw` | yes |
|
||||
| `fs.state_dir.perms_readable` | warn | State dir is readable by others | filesystem perms on `~/.openclaw` | yes |
|
||||
| `fs.state_dir.symlink` | warn | State dir target becomes another trust boundary | state dir filesystem layout | no |
|
||||
| `fs.config.perms_writable` | critical | Others can change auth/tool policy/config | filesystem perms on `~/.openclaw/openclaw.json` | yes |
|
||||
| `fs.config.symlink` | warn | Symlinked config files are unsupported for writes and add another trust boundary | replace with a regular config file or point `OPENCLAW_CONFIG_PATH` at the real file | no |
|
||||
| `fs.config.perms_group_readable` | warn | Group users can read config tokens/settings | filesystem perms on config file | yes |
|
||||
| `fs.config.perms_world_readable` | critical | Config can expose tokens/settings | filesystem perms on config file | yes |
|
||||
| `fs.config_include.perms_writable` | critical | Config include file can be modified by others | include-file perms referenced from `openclaw.json` | yes |
|
||||
| `fs.config_include.perms_group_readable` | warn | Group users can read included secrets/settings | include-file perms referenced from `openclaw.json` | yes |
|
||||
| `fs.config_include.perms_world_readable` | critical | Included secrets/settings are world-readable | include-file perms referenced from `openclaw.json` | yes |
|
||||
| `fs.auth_profiles.perms_writable` | critical | Others can inject or replace stored model credentials | `agents/<agentId>/agent/auth-profiles.json` perms | yes |
|
||||
| `fs.auth_profiles.perms_readable` | warn | Others can read API keys and OAuth tokens | `agents/<agentId>/agent/auth-profiles.json` perms | yes |
|
||||
| `fs.credentials_dir.perms_writable` | critical | Others can modify channel pairing/credential state | filesystem perms on `~/.openclaw/credentials` | yes |
|
||||
| `fs.credentials_dir.perms_readable` | warn | Others can read channel credential state | filesystem perms on `~/.openclaw/credentials` | yes |
|
||||
| `fs.sessions_store.perms_readable` | warn | Others can read session transcripts/metadata | session store perms | yes |
|
||||
| `fs.log_file.perms_readable` | warn | Others can read redacted-but-still-sensitive logs | gateway log file perms | yes |
|
||||
| `fs.synced_dir` | warn | State/config in iCloud/Dropbox/Drive broadens token/transcript exposure | move config/state off synced folders | no |
|
||||
| `gateway.bind_no_auth` | critical | Remote bind without shared secret | `gateway.bind`, `gateway.auth.*` | no |
|
||||
| `gateway.loopback_no_auth` | critical | Reverse-proxied loopback may become unauthenticated | `gateway.auth.*`, proxy setup | no |
|
||||
| `gateway.trusted_proxies_missing` | warn | Reverse-proxy headers are present but not trusted | `gateway.trustedProxies` | no |
|
||||
| `gateway.http.no_auth` | warn/critical | Gateway HTTP APIs reachable with `auth.mode="none"` | `gateway.auth.mode`, `gateway.http.endpoints.*`, `plugins.entries.admin-http-rpc` | no |
|
||||
| `gateway.http.session_key_override_enabled` | info | HTTP API callers can override `sessionKey` | `gateway.http.allowSessionKeyOverride` | no |
|
||||
| `gateway.tools_invoke_http.dangerous_allow` | warn/critical | Re-enables dangerous tools over HTTP API for owner/admin callers | `gateway.tools.allow` | no |
|
||||
| `gateway.nodes.allow_commands_dangerous` | warn/critical | Enables high-impact node commands (camera/screen/contacts/calendar/SMS) | `gateway.nodes.allowCommands` | no |
|
||||
| `gateway.nodes.deny_commands_ineffective` | warn | Pattern-like deny entries do not match shell text or groups | `gateway.nodes.denyCommands` | no |
|
||||
| `gateway.tailscale_funnel` | critical | Public internet exposure | `gateway.tailscale.mode` | no |
|
||||
| `gateway.tailscale_serve` | info | Tailnet exposure is enabled via Serve | `gateway.tailscale.mode` | no |
|
||||
| `gateway.control_ui.allowed_origins_required` | critical | Non-loopback Control UI without explicit browser-origin allowlist | `gateway.controlUi.allowedOrigins` | no |
|
||||
| `gateway.control_ui.allowed_origins_wildcard` | warn/critical | `allowedOrigins=["*"]` disables browser-origin allowlisting | `gateway.controlUi.allowedOrigins` | no |
|
||||
| `gateway.control_ui.host_header_origin_fallback` | warn/critical | Enables Host-header origin fallback (DNS rebinding hardening downgrade) | `gateway.controlUi.dangerouslyAllowHostHeaderOriginFallback` | no |
|
||||
| `gateway.control_ui.insecure_auth` | warn | Insecure-auth compatibility toggle enabled | `gateway.controlUi.allowInsecureAuth` | no |
|
||||
| `gateway.control_ui.device_auth_disabled` | critical | Disables device identity check | `gateway.controlUi.dangerouslyDisableDeviceAuth` | no |
|
||||
| `gateway.real_ip_fallback_enabled` | warn/critical | Trusting `X-Real-IP` fallback can enable source-IP spoofing via proxy misconfig | `gateway.allowRealIpFallback`, `gateway.trustedProxies` | no |
|
||||
| `gateway.token_too_short` | warn | Short shared token is easier to brute force | `gateway.auth.token` | no |
|
||||
| `gateway.auth_no_rate_limit` | warn | Exposed auth without rate limiting increases brute-force risk | `gateway.auth.rateLimit` | no |
|
||||
| `gateway.trusted_proxy_auth` | critical | Proxy identity now becomes the auth boundary | `gateway.auth.mode="trusted-proxy"` | no |
|
||||
| `gateway.trusted_proxy_no_proxies` | critical | Trusted-proxy auth without trusted proxy IPs is unsafe | `gateway.trustedProxies` | no |
|
||||
| `gateway.trusted_proxy_no_user_header` | critical | Trusted-proxy auth cannot resolve user identity safely | `gateway.auth.trustedProxy.userHeader` | no |
|
||||
| `gateway.trusted_proxy_no_allowlist` | warn | Trusted-proxy auth accepts any authenticated upstream user | `gateway.auth.trustedProxy.allowUsers` | no |
|
||||
| `gateway.trusted_proxy_allow_loopback` | warn | Trusted-proxy auth accepts explicitly allowed loopback proxy sources | `gateway.auth.trustedProxy.allowLoopback` | no |
|
||||
| `gateway.probe_auth_secretref_unavailable` | warn | Deep probe could not resolve auth SecretRefs in this command path | deep-probe auth source / SecretRef availability | no |
|
||||
| `gateway.probe_failed` | warn | Live Gateway probe failed (`--deep` only) | gateway reachability/auth | no |
|
||||
| `discovery.mdns_full_mode` | warn/critical | mDNS full mode advertises `cliPath`/`sshPort` metadata on local network | `discovery.mdns.mode`, `gateway.bind` | no |
|
||||
| `config.insecure_or_dangerous_flags` | warn | One insecure/dangerous debug flag is enabled | key named in finding detail | no |
|
||||
| `security.audit.suppressions.active` | info | Audit output has configured suppressions and may be filtered | `security.audit.suppressions` | no |
|
||||
| `config.secrets.gateway_password_in_config` | warn | Gateway password is stored directly in config | `gateway.auth.password` | no |
|
||||
| `config.secrets.hooks_token_in_config` | warn | Hook bearer token is stored directly in config | `hooks.token` | no |
|
||||
| `hooks.token_reuse_gateway_token` | critical | Hook ingress token also unlocks Gateway auth | `hooks.token`, `gateway.auth.token`, `gateway.auth.password` | no |
|
||||
| `hooks.token_too_short` | warn | Easier brute force on hook ingress | `hooks.token` | no |
|
||||
| `hooks.default_session_key_unset` | warn | Hook agent runs fan out into generated per-request sessions | `hooks.defaultSessionKey` | no |
|
||||
| `hooks.allowed_agent_ids_unrestricted` | warn/critical | Authenticated hook callers may route to any configured agent | `hooks.allowedAgentIds` | no |
|
||||
| `hooks.request_session_key_enabled` | warn/critical | External caller can choose sessionKey | `hooks.allowRequestSessionKey` | no |
|
||||
| `hooks.request_session_key_prefixes_missing` | warn/critical | No bound on external session key shapes | `hooks.allowedSessionKeyPrefixes` | no |
|
||||
| `hooks.path_root` | critical | Hook path is `/`, making ingress easier to collide or misroute | `hooks.path` | no |
|
||||
| `hooks.installs_unpinned_npm_specs` | warn | Hook install records are not pinned to immutable npm specs | hook install metadata | no |
|
||||
| `hooks.installs_missing_integrity` | warn | Hook install records lack integrity metadata | hook install metadata | no |
|
||||
| `hooks.installs_version_drift` | warn | Hook install records drift from installed packages | hook install metadata | no |
|
||||
| `logging.redact_off` | warn | Sensitive values leak to logs/status | `logging.redactSensitive` | yes |
|
||||
| `browser.control_invalid_config` | warn | Browser control config is invalid before runtime | `browser.*` | no |
|
||||
| `browser.control_no_auth` | critical | Browser control exposed without token/password auth | `gateway.auth.*` | no |
|
||||
| `browser.remote_cdp_http` | warn | Remote CDP over plain HTTP lacks transport encryption | browser profile `cdpUrl` | no |
|
||||
| `browser.remote_cdp_private_host` | warn | Remote CDP targets a private/internal host | browser profile `cdpUrl`, `browser.ssrfPolicy.*` | no |
|
||||
| `sandbox.docker_config_mode_off` | warn | Sandbox Docker config present but inactive | `agents.*.sandbox.mode` | no |
|
||||
| `sandbox.bind_mount_non_absolute` | warn | Relative bind mounts can resolve unpredictably | `agents.*.sandbox.docker.binds[]` | no |
|
||||
| `sandbox.dangerous_bind_mount` | critical | Sandbox bind mount targets blocked system, credential, or Docker socket paths | `agents.*.sandbox.docker.binds[]` | no |
|
||||
| `sandbox.dangerous_network_mode` | critical | Sandbox Docker network uses `host` or `container:*` namespace-join mode | `agents.*.sandbox.docker.network` | no |
|
||||
| `sandbox.dangerous_seccomp_profile` | critical | Sandbox seccomp profile weakens container isolation | `agents.*.sandbox.docker.securityOpt` | no |
|
||||
| `sandbox.dangerous_apparmor_profile` | critical | Sandbox AppArmor profile weakens container isolation | `agents.*.sandbox.docker.securityOpt` | no |
|
||||
| `sandbox.browser_cdp_bridge_unrestricted` | warn | Sandbox browser bridge is exposed without source-range restriction | `sandbox.browser.cdpSourceRange` | no |
|
||||
| `sandbox.browser_container.non_loopback_publish` | critical | Existing browser container publishes CDP on non-loopback interfaces | browser sandbox container publish config | no |
|
||||
| `sandbox.browser_container.hash_label_missing` | warn | Existing browser container predates current config-hash labels | `openclaw sandbox recreate --browser --all` | no |
|
||||
| `sandbox.browser_container.hash_epoch_stale` | warn | Existing browser container predates current browser config epoch | `openclaw sandbox recreate --browser --all` | no |
|
||||
| `sandbox.browser_container.docker_probe_timeout` | warn | Docker label probe for the browser container timed out | Docker daemon reachability | no |
|
||||
| `tools.exec.host_sandbox_no_sandbox_defaults` | warn | `exec host=sandbox` fails closed when sandbox is off | `tools.exec.host`, `agents.defaults.sandbox.mode` | no |
|
||||
| `tools.exec.host_sandbox_no_sandbox_agents` | warn | Per-agent `exec host=sandbox` fails closed when sandbox is off | `agents.list[].tools.exec.host`, `agents.list[].sandbox.mode` | no |
|
||||
| `tools.exec.security_full_configured` | warn/critical | Host exec is running with `security="full"` | `tools.exec.security`, `agents.list[].tools.exec.security` | no |
|
||||
| `tools.exec.agent_skill_mcp_boundary_drift` | warn | Agent skill allowlists are present while host exec can reach MCP clients/registries | `agents.list[].tools.exec.*`, sandbox/OS isolation, MCP server credentials | no |
|
||||
| `tools.exec.fs_tools_disabled_but_exec_enabled` | warn | Filesystem tool policy does not make shell execution read-only | `tools.deny`, `agents.list[].tools.deny`, `agents.*.sandbox.workspaceAccess` | no |
|
||||
| `tools.exec.auto_allow_skills_enabled` | warn | Exec approvals trust skill bins implicitly | host approvals file | no |
|
||||
| `tools.exec.allowlist_interpreter_without_strict_inline_eval` | warn | Interpreter allowlists permit inline eval without forced reapproval | `tools.exec.strictInlineEval`, `agents.list[].tools.exec.strictInlineEval`, exec approvals allowlist | no |
|
||||
| `tools.exec.safe_bins_interpreter_unprofiled` | warn | Interpreter/runtime bins in `safeBins` without explicit profiles broaden exec risk | `tools.exec.safeBins`, `tools.exec.safeBinProfiles`, `agents.list[].tools.exec.*` | no |
|
||||
| `tools.exec.safe_bins_broad_behavior` | warn | Broad-behavior tools in `safeBins` weaken the low-risk stdin-filter trust model | `tools.exec.safeBins`, `agents.list[].tools.exec.safeBins` | no |
|
||||
| `tools.exec.safe_bin_trusted_dirs_risky` | warn | `safeBinTrustedDirs` includes mutable or risky directories | `tools.exec.safeBinTrustedDirs`, `agents.list[].tools.exec.safeBinTrustedDirs` | no |
|
||||
| `tools.elevated.allowFrom.<provider>.wildcard` | critical | `tools.elevated.allowFrom.<provider>` includes `"*"`, approving every sender | `tools.elevated.allowFrom.<provider>` | no |
|
||||
| `tools.elevated.allowFrom.<provider>.large` | warn | Elevated allowlist for `<provider>` has more than 25 entries | `tools.elevated.allowFrom.<provider>` | no |
|
||||
| `agents.claude_cli.permission_mode_overridden_by_yolo` | warn | Claude CLI `--permission-mode` is ignored because OpenClaw exec is fully unattended | `tools.exec.security`, `tools.exec.ask`, `cliBackends.claude-cli` args | no |
|
||||
| `skills.workspace.symlink_escape` | warn | Workspace `skills/**/SKILL.md` resolves outside workspace root (symlink-chain drift) | workspace `skills/**` filesystem state | no |
|
||||
| `skills.workspace.scan_truncated` | warn | Workspace skill scan hit its directory-visit cap before finishing | flatten/simplify the workspace `skills/` directory tree | no |
|
||||
| `plugins.extensions_no_allowlist` | warn | Plugins are installed without an explicit plugin allowlist | `plugins.allowlist` | no |
|
||||
| `plugins.allow_phantom_entries` | warn | `plugins.allow` lists an ID with no matching installed plugin | `plugins.allow` | no |
|
||||
| `plugins.installs_unpinned_npm_specs` | warn | Plugin index records are not pinned to immutable npm specs | plugin install metadata | no |
|
||||
| `plugins.installs_missing_integrity` | warn | Plugin index records lack integrity metadata | plugin install metadata | no |
|
||||
| `plugins.installs_version_drift` | warn | Plugin index records drift from installed packages | plugin install metadata | no |
|
||||
| `plugins.code_safety` | warn/critical | Plugin code scan found suspicious or dangerous patterns (`--deep` only) | plugin code / install source | no |
|
||||
| `plugins.code_safety.entry_path` | warn | Plugin entry path points into hidden or `node_modules` locations | plugin manifest `entry` | no |
|
||||
| `plugins.code_safety.entry_escape` | critical | Plugin entry escapes the plugin directory | plugin manifest `entry` | no |
|
||||
| `plugins.code_safety.manifest_parse_error` | warn | Plugin manifest could not be parsed during the code-safety scan | plugin manifest file | no |
|
||||
| `plugins.code_safety.scan_failed` | warn | Plugin code scan could not complete (`--deep` only) | plugin path / scan environment | no |
|
||||
| `plugins.<pluginId>.security_audit_failed` | warn | A plugin-owned security audit collector threw an error | that plugin's security-audit collector | no |
|
||||
| `skills.code_safety` | warn/critical | Skill installer metadata/code contains suspicious or dangerous patterns (`--deep` only) | skill install source | no |
|
||||
| `skills.code_safety.scan_failed` | warn | Skill code scan could not complete (`--deep` only) | skill scan environment | no |
|
||||
| `security.exposure.open_channels_with_exec` | warn/critical | Shared/public rooms can reach exec-enabled agents | `channels.*.dmPolicy`, `channels.*.groupPolicy`, `tools.exec.*`, `agents.list[].tools.exec.*` | no |
|
||||
| `security.exposure.open_groups_with_elevated` | critical | Open DMs/groups + elevated tools create high-impact prompt-injection paths | top-level or nested DM policy paths, account overrides, `channels.*.groupPolicy` | no |
|
||||
| `security.exposure.open_groups_with_runtime_or_fs` | critical/warn | Open DMs/groups can reach command/file tools without sandbox/workspace guards | DM/group policy paths, `tools.profile/deny`, `tools.fs.workspaceOnly`, `agents.*.sandbox.mode` | no |
|
||||
| `security.trust_model.multi_user_heuristic` | warn | Config looks multi-user while gateway trust model is personal-assistant | split trust boundaries, or shared-user hardening (`sandbox.mode`, tool deny/workspace scoping) | no |
|
||||
| `tools.profile_minimal_overridden` | warn | Agent overrides bypass global minimal profile | `agents.list[].tools.profile` | no |
|
||||
| `plugins.tools_reachable_permissive_policy` | warn | Extension tools reachable in permissive contexts | `tools.profile` + tool allow/deny | no |
|
||||
| `models.legacy` | warn | Legacy model families are still configured | model selection | no |
|
||||
| `models.weak_tier` | warn | Configured models are below current recommended tiers | model selection | no |
|
||||
| `models.small_params` | critical/info | Small models + unsafe tool surfaces raise injection risk | model choice + sandbox/tool policy | no |
|
||||
| `channels.<provider>.dm.open` | critical | `<provider>` DM policy is `"open"`; anyone can DM the bot | `channels.<provider>.dmPolicy`, `.allowFrom` | no |
|
||||
| `channels.<provider>.dm.open_invalid` | warn | `dmPolicy="open"` without `"*"` in `allowFrom` is inconsistent | `channels.<provider>.allowFrom` | no |
|
||||
| `channels.<provider>.dm.scope_main_multiuser` | warn | Multiple DM senders currently share the main session | `session.dmScope` | no |
|
||||
| `channels.<provider>.allowFrom.dangerous_name_matching_enabled` | info | `dangerouslyAllowNameMatching` re-enables mutable name/email/tag sender matching | disable `dangerouslyAllowNameMatching`, use stable sender IDs | no |
|
||||
| `channels.<provider>.account.read_only_resolution` | warn | A channel account could not be fully resolved for audit (missing secret/gateway) | ensure referenced secrets are resolvable, or run against a live gateway snapshot | no |
|
||||
| `channels.<provider>.warning.<n>` | info/warn/critical | Provider-specific security warning, classified from free-form plugin text | see finding detail | no |
|
||||
| `summary.attack_surface` | info | Roll-up summary of auth, channel, tool, and exposure posture | multiple keys (see finding detail) | no |
|
||||
|
||||
`channels.<provider>.*` and `tools.elevated.allowFrom.<provider>.*` checkIds are
|
||||
generated per configured channel/provider, so `<provider>` is a real channel id
|
||||
(for example `telegram`, `discord`) in actual output, not a literal string.
|
||||
|
||||
## Related
|
||||
|
||||
- [Security](/gateway/security)
|
||||
- [Configuration](/gateway/configuration)
|
||||
- [Trusted proxy auth](/gateway/trusted-proxy-auth)
|
||||
223
docs/gateway/security/exposure-runbook.md
Normal file
223
docs/gateway/security/exposure-runbook.md
Normal file
@@ -0,0 +1,223 @@
|
||||
---
|
||||
summary: "Pre-flight and rollback checklist before exposing an OpenClaw Gateway beyond loopback"
|
||||
title: "Gateway exposure runbook"
|
||||
sidebarTitle: "Exposure runbook"
|
||||
read_when:
|
||||
- Exposing the Gateway over LAN, tailnet, Tailscale Serve, Funnel, or a reverse proxy
|
||||
- Reviewing a deployment before allowing real messaging users
|
||||
- Rolling back a risky remote access or DM configuration
|
||||
---
|
||||
|
||||
<Warning>
|
||||
Expose the Gateway only after you can explain who can reach it, how they are
|
||||
authenticated, which agents they can trigger, and which tools those agents can
|
||||
use. When in doubt, return to loopback-only access and re-run the audit.
|
||||
</Warning>
|
||||
|
||||
This runbook turns the broader [Security](/gateway/security) guidance into an
|
||||
operator checklist for remote access and messaging exposure.
|
||||
|
||||
## Choose the exposure pattern
|
||||
|
||||
Prefer the narrowest pattern that satisfies the workflow.
|
||||
|
||||
| Pattern | Recommended when | Required controls |
|
||||
| -------------------------- | ----------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| Loopback + SSH tunnel | Personal use, admin access, debugging | Keep `gateway.bind: "loopback"` and tunnel `127.0.0.1:18789` |
|
||||
| Loopback + Tailscale Serve | Personal tailnet access to Control UI/WebSocket | Keep Gateway loopback-only; Tailscale identity headers only authenticate the Control UI WebSocket surface, not other auth paths |
|
||||
| Tailnet/LAN bind | Dedicated private network with known devices | Gateway auth, firewall allowlist, no public port-forward |
|
||||
| Trusted reverse proxy | Organization SSO/OIDC in front of Gateway | `trusted-proxy` auth, strict `trustedProxies`, header overwrite/strip rules, explicit allowed users |
|
||||
| Public internet | Rare, high-risk deployments | Identity-aware proxy, TLS, rate limits, strict allowlists, sandboxed non-main sessions |
|
||||
|
||||
Avoid direct public port-forwarding to the Gateway. If public access is
|
||||
required, put an identity-aware proxy in front of it and make the proxy the
|
||||
only network path to the Gateway.
|
||||
|
||||
## Pre-flight inventory
|
||||
|
||||
Record these before changing bind, proxy, Tailscale, or channel policy:
|
||||
|
||||
- Gateway host, OS user, and state directory (default `~/.openclaw`).
|
||||
- Gateway URL and bind mode (`gateway.bind`; default port `18789`).
|
||||
- Auth mode, token/password source, or trusted proxy identity source.
|
||||
- Every enabled channel and whether it accepts DMs, groups, or webhooks.
|
||||
- Agents reachable from non-local senders.
|
||||
- Tool profile, sandbox mode, and elevated tool policy for each reachable agent.
|
||||
- External credentials available to those agents.
|
||||
- Backup location for `~/.openclaw/openclaw.json` and credentials.
|
||||
|
||||
If more than one person can message the bot, treat this as shared delegated
|
||||
tool authority, not per-user host isolation.
|
||||
|
||||
## Baseline checks
|
||||
|
||||
Run before opening access:
|
||||
|
||||
```bash
|
||||
openclaw doctor
|
||||
openclaw security audit
|
||||
openclaw security audit --deep
|
||||
openclaw health
|
||||
```
|
||||
|
||||
Resolve critical findings first. Accept warnings only when intentional and
|
||||
documented for the deployment. See [Security audit checks](/gateway/security/audit-checks)
|
||||
for what each `checkId` means and its fix key.
|
||||
|
||||
For remote CLI validation, pass credentials explicitly:
|
||||
|
||||
```bash
|
||||
openclaw gateway probe --url ws://127.0.0.1:18789 --token "$OPENCLAW_GATEWAY_TOKEN"
|
||||
```
|
||||
|
||||
Do not assume local config credentials apply to an explicit remote URL.
|
||||
|
||||
## Minimum safe baseline
|
||||
|
||||
Use this shape as the starting point for exposed deployments:
|
||||
|
||||
```json5
|
||||
{
|
||||
gateway: {
|
||||
bind: "loopback",
|
||||
auth: {
|
||||
mode: "token",
|
||||
token: "replace-with-a-long-random-token",
|
||||
},
|
||||
},
|
||||
session: {
|
||||
dmScope: "per-channel-peer",
|
||||
},
|
||||
agents: {
|
||||
defaults: {
|
||||
sandbox: { mode: "non-main" },
|
||||
},
|
||||
},
|
||||
tools: {
|
||||
profile: "messaging",
|
||||
exec: { security: "deny", ask: "always" },
|
||||
elevated: { enabled: false },
|
||||
},
|
||||
}
|
||||
```
|
||||
|
||||
Widen one control at a time: add a specific channel allowlist before enabling
|
||||
write-capable tools, or enable a reverse proxy before accepting remote Control
|
||||
UI traffic.
|
||||
|
||||
`tools.exec.security: "deny"` blocks all exec calls, including benign
|
||||
diagnostics. If diagnostics or low-risk commands are required, relax this only
|
||||
after choosing the specific senders, agents, commands, and approval mode that
|
||||
match your threat model.
|
||||
|
||||
## DM and group exposure
|
||||
|
||||
Messaging channels are untrusted input surfaces. Before allowing DMs or
|
||||
groups:
|
||||
|
||||
- Prefer `dmPolicy: "pairing"` or a strict `allowFrom` list over `dmPolicy: "open"`.
|
||||
- Do not combine `"*"` allowlists with broad tool access.
|
||||
- Require mentions in groups unless the room is tightly controlled.
|
||||
- Set `session.dmScope: "per-channel-peer"` (or `"per-account-channel-peer"` for
|
||||
multi-account channels) when multiple people can DM the bot, so DM sessions
|
||||
don't share context.
|
||||
- Route shared channels to agents with minimal tools and no personal
|
||||
credentials.
|
||||
|
||||
Pairing approves the sender to trigger the bot. It does not make that sender a
|
||||
separate host security boundary.
|
||||
|
||||
## Reverse proxy checks
|
||||
|
||||
For identity-aware proxies:
|
||||
|
||||
- The proxy must authenticate users before forwarding to the Gateway.
|
||||
- Firewall or network policy must block direct access to the Gateway port.
|
||||
- `gateway.trustedProxies` must list only the proxy source IPs.
|
||||
- The proxy must strip or overwrite client-supplied identity and forwarding
|
||||
headers.
|
||||
- Set `gateway.auth.trustedProxy.allowUsers` when the proxy serves more than
|
||||
one audience.
|
||||
- Use `gateway.auth.trustedProxy.allowLoopback` only for a same-host proxy
|
||||
where local processes are trusted and the proxy owns the identity headers.
|
||||
|
||||
Run `openclaw security audit --deep` after proxy changes. Trusted-proxy
|
||||
findings are high-signal because the proxy becomes the authentication
|
||||
boundary.
|
||||
|
||||
## Tool and sandbox review
|
||||
|
||||
Before exposing an agent to remote senders:
|
||||
|
||||
- Confirm which sessions run on host versus sandbox.
|
||||
- Deny or require approval for host exec.
|
||||
- Keep elevated tools disabled unless a specific, trusted sender needs them.
|
||||
- Avoid browser, canvas, node, cron, gateway, and session-spawn tools for open
|
||||
or semi-open messaging surfaces.
|
||||
- Keep bind mounts narrow; avoid credential, home, Docker socket, and system
|
||||
paths.
|
||||
- Use separate gateways, OS users, or hosts for materially different trust
|
||||
boundaries.
|
||||
|
||||
If remote users are not fully trusted, isolation must come from separate
|
||||
deployments, not only from prompts or session labels.
|
||||
|
||||
## Post-change validation
|
||||
|
||||
After each exposure change:
|
||||
|
||||
1. Re-run `openclaw security audit --deep`.
|
||||
2. Confirm a successful authorized connection succeeds.
|
||||
3. Confirm an unauthorized sender or browser session is denied.
|
||||
4. Confirm logs redact secrets.
|
||||
5. Confirm DM/group routing reaches only the intended agent.
|
||||
6. Confirm high-impact tools ask for approval or are denied.
|
||||
7. Document the accepted residual warnings.
|
||||
|
||||
Do not proceed to the next exposure change until the current one is
|
||||
understood.
|
||||
|
||||
## Rollback plan
|
||||
|
||||
If the Gateway may be overexposed:
|
||||
|
||||
```json5
|
||||
{
|
||||
gateway: {
|
||||
bind: "loopback",
|
||||
},
|
||||
channels: {
|
||||
whatsapp: { dmPolicy: "disabled" },
|
||||
telegram: { dmPolicy: "disabled" },
|
||||
discord: { dmPolicy: "disabled" },
|
||||
slack: { dmPolicy: "disabled" },
|
||||
},
|
||||
tools: {
|
||||
exec: { security: "deny", ask: "always" },
|
||||
elevated: { enabled: false },
|
||||
},
|
||||
}
|
||||
```
|
||||
|
||||
Then:
|
||||
|
||||
1. Stop public forwarding, Tailscale Funnel, or reverse proxy routes.
|
||||
2. Rotate Gateway tokens/passwords and affected integration credentials.
|
||||
3. Remove `"*"` and unexpected senders from allowlists.
|
||||
4. Review recent audit logs, run history, tool calls, and config changes.
|
||||
5. Re-run `openclaw security audit --deep`.
|
||||
6. Re-enable access with the narrowest pattern that satisfies the workflow.
|
||||
|
||||
## Review checklist
|
||||
|
||||
- Gateway remains loopback-only unless there is a documented reason.
|
||||
- Non-loopback access has auth, firewalling, and no public direct route.
|
||||
- Trusted-proxy deployments have strict proxy IPs and header controls.
|
||||
- DMs use pairing or allowlists, not open access by default.
|
||||
- Groups require mentions or explicit allowlists.
|
||||
- Shared channels do not reach personal credentials.
|
||||
- Non-main sessions run in sandbox mode.
|
||||
- Host exec and elevated tools are denied or approval-gated.
|
||||
- Logs redact secrets.
|
||||
- Critical audit findings are resolved.
|
||||
- Rollback steps are tested and documented.
|
||||
839
docs/gateway/security/index.md
Normal file
839
docs/gateway/security/index.md
Normal file
@@ -0,0 +1,839 @@
|
||||
---
|
||||
summary: "Security considerations and threat model for running an AI gateway with shell access"
|
||||
read_when:
|
||||
- Adding features that widen access or automation
|
||||
title: "Security"
|
||||
---
|
||||
|
||||
<Warning>
|
||||
**Personal assistant trust model.** This guidance assumes one trusted
|
||||
operator boundary per gateway (single-user, personal-assistant model).
|
||||
OpenClaw is **not** a hostile multi-tenant security boundary for multiple
|
||||
adversarial users sharing one agent or gateway. For mixed-trust or
|
||||
adversarial-user operation, split trust boundaries: separate gateway +
|
||||
credentials, ideally separate OS users or hosts.
|
||||
</Warning>
|
||||
|
||||
## Scope: personal assistant security model
|
||||
|
||||
- Supported: one user/trust boundary per gateway (prefer one OS user/host/VPS per boundary).
|
||||
- Not supported: one shared gateway/agent used by mutually untrusted or adversarial users.
|
||||
- Adversarial-user isolation needs separate gateways (and ideally separate OS users/hosts).
|
||||
- If several untrusted users can message one tool-enabled agent, they share that agent's delegated tool authority.
|
||||
- If someone can modify Gateway host state/config (`~/.openclaw`, including `openclaw.json`), treat them as a trusted operator.
|
||||
- Inside one Gateway, authenticated operator access is a trusted control-plane role, not a per-user tenant role.
|
||||
- `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](/gateway/security/exposure-runbook) as a pre-flight/rollback checklist.
|
||||
|
||||
## `openclaw security audit`
|
||||
|
||||
Run this after any config change or before exposing network surfaces:
|
||||
|
||||
```bash
|
||||
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`.
|
||||
|
||||
### What the audit checks (high level)
|
||||
|
||||
- **Inbound access** - DM/group policies, allowlists: can strangers trigger the bot?
|
||||
- **Tool blast radius** - elevated tools + open rooms: could prompt injection become shell/file/network actions?
|
||||
- **Exec filesystem drift** - mutating filesystem tools denied while `exec`/`process` stay available without sandbox constraints.
|
||||
- **Exec approval drift** - `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.
|
||||
- **Network exposure** - Gateway bind/auth, Tailscale Serve/Funnel, weak/short auth tokens.
|
||||
- **Browser control exposure** - remote nodes, relay ports, remote CDP endpoints.
|
||||
- **Local disk hygiene** - permissions, symlinks, config includes, synced-folder paths.
|
||||
- **Plugins** - loading without an explicit allowlist.
|
||||
- **Policy drift** - sandbox Docker settings configured but sandbox mode off; `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.
|
||||
- **Runtime expectation drift** - assuming implicit exec still means `sandbox` when `tools.exec.host` now defaults to `auto`, or setting `tools.exec.host="sandbox"` while sandbox mode is off.
|
||||
- **Model hygiene** - warns on legacy configured models (soft warning, not a hard block).
|
||||
|
||||
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](/gateway/security/audit-checks). See also [Formal Verification](/security/formal-verification).
|
||||
|
||||
### Priority order when triaging findings
|
||||
|
||||
1. Anything "open" + tools enabled: lock down DMs/groups first (pairing/allowlists), then tighten tool policy/sandboxing.
|
||||
2. Public network exposure (LAN bind, Funnel, missing auth): fix immediately.
|
||||
3. Browser control remote exposure: treat like operator access (tailnet-only, pair nodes deliberately, no public exposure).
|
||||
4. Permissions: state/config/credentials/auth must not be group/world-readable.
|
||||
5. Plugins: load only what you explicitly trust.
|
||||
6. Model choice: prefer modern, instruction-hardened models for any bot with tools.
|
||||
|
||||
## Hardened baseline in 60 seconds
|
||||
|
||||
```json5
|
||||
{
|
||||
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.
|
||||
|
||||
## Trust boundary matrix
|
||||
|
||||
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" |
|
||||
|
||||
## Not vulnerabilities by design
|
||||
|
||||
<Accordion title="Common findings closed as no-action">
|
||||
|
||||
- Prompt-injection-only chains without a policy, auth, or sandbox bypass.
|
||||
- Claims that assume hostile multi-tenant operation on one shared host or config.
|
||||
- Normal operator read-path access (for example `sessions.list` / `sessions.preview` / `chat.history`) classified as IDOR in a shared-gateway setup.
|
||||
- Localhost-only deployment findings (for example missing HSTS on a loopback-only gateway).
|
||||
- Discord inbound webhook signature findings for inbound paths that do not exist in this repo.
|
||||
- Node pairing metadata treated as a hidden second per-command approval layer for `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).
|
||||
- "Missing per-user authorization" findings that treat `sessionKey` as an auth token.
|
||||
|
||||
</Accordion>
|
||||
|
||||
## Gateway and node trust
|
||||
|
||||
Treat Gateway and node as one operator trust domain with different roles:
|
||||
|
||||
- **Gateway**: control plane and policy surface (`gateway.auth`, tool policy, routing).
|
||||
- **Node**: remote execution surface paired to that Gateway (commands, device actions, host-local capabilities).
|
||||
- A caller authenticated to the Gateway is trusted at Gateway scope; after pairing, node actions are trusted operator actions on that node. See [Operator scopes](/gateway/operator-scopes).
|
||||
- Direct loopback backend clients authenticated with the shared gateway token/password can make internal control-plane RPCs without presenting a user device identity. This is not a remote or browser pairing bypass - network clients, node clients, device-token clients, and explicit device identities still go through pairing and scope-upgrade enforcement.
|
||||
- Exec approvals (allowlist + ask) are guardrails for operator intent, not hostile multi-tenant isolation. They bind exact request context and best-effort direct local file operands; they do not semantically model every runtime/interpreter loader path. Use sandboxing and host isolation for strong boundaries.
|
||||
- Trusted single-operator default: host exec on `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.
|
||||
|
||||
## Threat model
|
||||
|
||||
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:
|
||||
|
||||
1. **Identity first** - decide who can talk to the bot (DM pairing / allowlists / explicit "open").
|
||||
2. **Scope next** - decide where the bot can act (group allowlists + mention gating, tools, sandboxing, device permissions).
|
||||
3. **Model last** - assume the model can be manipulated; design so manipulation has limited blast radius.
|
||||
|
||||
## DM access: pairing, allowlist, open, disabled
|
||||
|
||||
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. |
|
||||
|
||||
```bash
|
||||
openclaw pairing list <channel>
|
||||
openclaw pairing approve <channel> <code>
|
||||
```
|
||||
|
||||
Details + files on disk: [Pairing](/channels/pairing)
|
||||
|
||||
Treat `dmPolicy="open"` and `groupPolicy="open"` as last-resort settings; prefer pairing + allowlists unless you fully trust every member of the room.
|
||||
|
||||
### Allowlists (two layers)
|
||||
|
||||
- **DM allowlist** (`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.
|
||||
- **Group allowlist** (channel-specific): which groups/channels/guilds the bot accepts at all.
|
||||
- `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.
|
||||
- Check order: `groupPolicy`/group allowlists first, then mention/reply activation. Replying to a bot message (implicit mention) does **not** bypass `groupAllowFrom`.
|
||||
|
||||
Details: [Configuration](/gateway/configuration) and [Groups](/channels/groups)
|
||||
|
||||
### DM session isolation (multi-user mode)
|
||||
|
||||
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:
|
||||
|
||||
```json5
|
||||
{ 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](/concepts/session) and [Configuration](/gateway/configuration).
|
||||
|
||||
## Context visibility vs trigger authorization
|
||||
|
||||
Two separate concepts:
|
||||
|
||||
- **Trigger authorization**: who can trigger the agent (`dmPolicy`, `groupPolicy`, allowlists, mention gates).
|
||||
- **Context visibility**: what supplemental context reaches the model (reply body, quoted text, thread history, forwarded metadata).
|
||||
|
||||
`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](/channels/groups#context-visibility-and-allowlists). 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.
|
||||
|
||||
## Prompt injection
|
||||
|
||||
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:
|
||||
|
||||
- "Read this file/URL and do exactly what it says."
|
||||
- "Ignore your system prompt or safety rules."
|
||||
- "Reveal your hidden instructions or tool outputs."
|
||||
- "Paste the full contents of ~/.openclaw or your logs."
|
||||
|
||||
What helps in practice:
|
||||
|
||||
- Keep inbound DMs locked down (pairing/allowlists); prefer mention gating in groups; avoid always-on bots in public rooms.
|
||||
- Treat links, attachments, and pasted instructions as hostile by default.
|
||||
- Run sensitive tool execution in a sandbox; keep secrets out of the agent's reachable filesystem. Sandboxing is opt-in: if sandbox mode is off, implicit `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.
|
||||
- Limit high-risk tools (`exec`, `browser`, `web_fetch`, `web_search`) to trusted agents or explicit allowlists.
|
||||
- If you allowlist interpreters (`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.
|
||||
- Reduce blast radius by using a read-only or tool-disabled **reader agent** to summarize untrusted content, then pass the summary to your main agent.
|
||||
- Keep `web_search` / `web_fetch` / `browser` off for tool-enabled agents unless needed.
|
||||
- For OpenResponses URL inputs (`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.
|
||||
- Keep secrets out of prompts; pass them via env/config on the gateway host instead.
|
||||
|
||||
**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>
|
||||
|
||||
- Use the latest-generation, best-tier model for any bot that can run tools or touch files/networks.
|
||||
- Do not use older/weaker/smaller tiers for tool-enabled agents or untrusted inboxes.
|
||||
- If you must use a smaller model, reduce blast radius: read-only tools, strong sandboxing, minimal filesystem access, strict allowlists. Enable sandboxing for all sessions and disable `web_search`/`web_fetch`/`browser` unless inputs are tightly controlled.
|
||||
- For chat-only personal assistants with trusted input and no tools, smaller models are usually fine.
|
||||
|
||||
### External content and untrusted-input wrapping
|
||||
|
||||
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>`, `<system-reminder>`, `<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.
|
||||
|
||||
### Bypass flags (keep off in production)
|
||||
|
||||
- `hooks.mappings[].allowUnsafeExternalContent`
|
||||
- `hooks.gmail.allowUnsafeExternalContent`
|
||||
- Cron payload field `allowUnsafeExternalContent`
|
||||
|
||||
Only 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 and verbose output in groups
|
||||
|
||||
`/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.
|
||||
|
||||
## Command authorization
|
||||
|
||||
Slash commands and directives are honored only for authorized senders, derived from channel allowlists/pairing plus `commands.useAccessGroups` (see [Configuration](/gateway/configuration) and [Slash commands](/tools/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.
|
||||
|
||||
## Control plane tools
|
||||
|
||||
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:
|
||||
|
||||
```json5
|
||||
{
|
||||
tools: {
|
||||
deny: ["gateway", "cron", "sessions_spawn", "sessions_send"],
|
||||
},
|
||||
}
|
||||
```
|
||||
|
||||
`commands.restart=false` only blocks restart actions - it does not disable `gateway` config/update actions.
|
||||
|
||||
## Node execution (`system.run`)
|
||||
|
||||
If a macOS node is paired, the Gateway can invoke `system.run` on it - this is remote code execution on that Mac.
|
||||
|
||||
- Requires node pairing (approval + token). Pairing establishes node identity/trust and token issuance; it is not a per-command approval surface.
|
||||
- The Gateway applies a coarse global node command policy via `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.
|
||||
- The per-node `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.
|
||||
- A node running `security="full"` and `ask="off"` follows the default trusted-operator model - expected behavior, not a bug, unless your deployment needs a tighter stance.
|
||||
- Approval mode binds exact request context and, when possible, one concrete local script/file operand. If OpenClaw cannot identify exactly one direct local file for an interpreter/runtime command, approval-backed execution is denied rather than promising full semantic coverage.
|
||||
- For `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.
|
||||
- To disable remote execution entirely: set security to `deny` and remove node pairing for that Mac.
|
||||
|
||||
## Dynamic skills (watcher / remote nodes)
|
||||
|
||||
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
|
||||
|
||||
Plugins run in-process with the Gateway - treat them as trusted code.
|
||||
|
||||
- Only install from sources you trust; prefer explicit `plugins.allow` allowlists; review plugin config before enabling; restart the Gateway after plugin changes.
|
||||
- Installing/updating (`openclaw plugins install <package>`, `openclaw plugins update <id>`) runs untrusted code:
|
||||
- The install path is the per-plugin directory under the active plugin install root.
|
||||
- OpenClaw does not run built-in local dangerous-code blocking during install/update. Use `security.installPolicy` for operator-owned local allow/block decisions and `openclaw security audit --deep` for diagnostic scanning.
|
||||
- npm and git plugin installs run package-manager dependency convergence only during the explicit install/update flow. Local paths and archives are treated as self-contained packages; OpenClaw copies/references them without running `npm install`.
|
||||
- Prefer pinned exact versions (`@scope/pkg@1.2.3`) 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](/tools/plugin)
|
||||
|
||||
## Sandboxing
|
||||
|
||||
Dedicated doc: [Sandboxing](/gateway/sandboxing)
|
||||
|
||||
Two complementary approaches:
|
||||
|
||||
- **Full Gateway in Docker** (container boundary): [Docker](/install/docker)
|
||||
- **Tool sandbox** (`agents.defaults.sandbox`; host gateway + sandbox-isolated tools; Docker is the default backend): [Sandboxing](/gateway/sandboxing)
|
||||
|
||||
<Note>
|
||||
To prevent cross-agent access, keep `agents.defaults.sandbox.scope` at `"agent"` (default) or use `"session"` for stricter per-session isolation. `scope: "shared"` uses a single container or workspace.
|
||||
</Note>
|
||||
|
||||
Agent 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.
|
||||
|
||||
<Warning>
|
||||
`tools.elevated` is the global baseline escape hatch that runs exec outside the sandbox. The effective host is `gateway` by default, or `node` when the exec target is configured to `node`. Keep `tools.elevated.allowFrom` tight and do not enable it for strangers. Further restrict per agent via `agents.list[].tools.elevated`. See [Elevated mode](/tools/elevated).
|
||||
</Warning>
|
||||
|
||||
### Sub-agent delegation guardrail
|
||||
|
||||
If you allow session tools, treat delegated sub-agent runs as another boundary decision:
|
||||
|
||||
- Deny `sessions_spawn` unless the agent truly needs delegation.
|
||||
- Keep `agents.defaults.subagents.allowAgents` and any per-agent `agents.list[].subagents.allowAgents` overrides restricted to known-safe target agents.
|
||||
- For workflows that must remain sandboxed, call `sessions_spawn` with `sandbox: "require"` (default is `"inherit"`); `"require"` fails fast when the target child runtime is not sandboxed.
|
||||
|
||||
### Read-only mode
|
||||
|
||||
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.
|
||||
- Keep filesystem roots narrow - avoid broad roots like your home directory for agent/sandbox workspaces, which can expose sensitive local files (for example state/config under `~/.openclaw`) to filesystem tools.
|
||||
|
||||
## Per-agent access profiles (multi-agent)
|
||||
|
||||
Each agent can have its own sandbox + tool policy: full access, read-only, or no access. See [Multi-Agent Sandbox & Tools](/tools/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).
|
||||
|
||||
### Full access (no sandbox)
|
||||
|
||||
```json5
|
||||
{
|
||||
agents: {
|
||||
list: [
|
||||
{ id: "personal", workspace: "~/.openclaw/workspace-personal", sandbox: { mode: "off" } },
|
||||
],
|
||||
},
|
||||
}
|
||||
```
|
||||
|
||||
### Read-only tools + read-only workspace
|
||||
|
||||
```json5
|
||||
{
|
||||
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"],
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
}
|
||||
```
|
||||
|
||||
### No filesystem/shell access (provider messaging allowed)
|
||||
|
||||
```json5
|
||||
{
|
||||
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",
|
||||
],
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
}
|
||||
```
|
||||
|
||||
## Browser control risks
|
||||
|
||||
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.
|
||||
|
||||
- Prefer a dedicated profile for the agent (the default `openclaw` profile); avoid your personal daily-driver profile.
|
||||
- Keep host browser control disabled for sandboxed agents unless you trust them.
|
||||
- The standalone loopback browser control API only honors shared-secret auth (gateway token bearer auth or gateway password) - it does not consume trusted-proxy or Tailscale Serve identity headers.
|
||||
- Treat browser downloads as untrusted input; prefer an isolated downloads directory.
|
||||
- Disable browser sync/password managers in the agent profile if possible.
|
||||
- For remote gateways, "browser control" is equivalent to "operator access" to whatever that profile can reach.
|
||||
- Keep Gateway and node hosts tailnet-only; avoid exposing browser control ports to LAN or public internet.
|
||||
- Disable browser proxy routing when not needed (`gateway.nodes.browser.mode="off"`).
|
||||
- Chrome MCP existing-session mode is not "safer" - it can act as you in whatever that host Chrome profile can reach.
|
||||
- Run a **node host** on the browser machine and let the Gateway proxy browser actions when the Gateway is remote from the browser (see [Browser tool](/tools/browser)); treat node pairing like admin access, keep Gateway and node host on the same tailnet, and avoid exposing relay/control ports over LAN, public internet, or Tailscale Funnel.
|
||||
|
||||
### Browser SSRF policy (strict by default)
|
||||
|
||||
Private/internal destinations stay blocked unless you explicitly opt in.
|
||||
|
||||
- Default: `browser.ssrfPolicy.dangerouslyAllowPrivateNetwork` unset, so private/internal/special-use destinations stay blocked. Legacy alias `allowPrivateNetwork` still accepted.
|
||||
- Opt-in: set `dangerouslyAllowPrivateNetwork: true` to allow those destinations.
|
||||
- In strict mode, use `hostnameAllowlist` (patterns like `*.example.com`) and `allowedHostnames` (exact host exceptions, including otherwise-blocked names like `localhost`) for explicit exceptions.
|
||||
- Navigation is checked before the request and best-effort re-checked on the final `http(s)` URL after navigation, to reduce redirect-based pivots.
|
||||
|
||||
```json5
|
||||
{
|
||||
browser: {
|
||||
ssrfPolicy: {
|
||||
dangerouslyAllowPrivateNetwork: false,
|
||||
hostnameAllowlist: ["*.example.com", "example.com"],
|
||||
allowedHostnames: ["localhost"],
|
||||
},
|
||||
},
|
||||
}
|
||||
```
|
||||
|
||||
## Network exposure
|
||||
|
||||
### Bind, port, firewall
|
||||
|
||||
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`.
|
||||
|
||||
### Docker port publishing with UFW
|
||||
|
||||
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.
|
||||
|
||||
```bash
|
||||
# /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.
|
||||
|
||||
```bash
|
||||
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).
|
||||
|
||||
### mDNS/Bonjour discovery
|
||||
|
||||
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:
|
||||
|
||||
```json5
|
||||
{ discovery: { mdns: { mode: "minimal" } } }
|
||||
```
|
||||
|
||||
- **Off** suppresses local discovery while keeping the plugin enabled:
|
||||
|
||||
```json5
|
||||
{ discovery: { mdns: { mode: "off" } } }
|
||||
```
|
||||
|
||||
- **Full mode** (opt-in) includes `cliPath` + `sshPort`:
|
||||
|
||||
```json5
|
||||
{ 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 WebSocket auth
|
||||
|
||||
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.
|
||||
|
||||
```json5
|
||||
{ gateway: { auth: { mode: "token", token: "your-token" } } }
|
||||
```
|
||||
|
||||
`openclaw doctor --generate-gateway-token` can generate one for you.
|
||||
|
||||
<Note>
|
||||
`gateway.remote.token` and `gateway.remote.password` are client credential sources - they do not protect local WS access by themselves. Local call paths use `gateway.remote.*` only as fallback when `gateway.auth.*` is unset. If `gateway.auth.token` or `gateway.auth.password` is explicitly configured via SecretRef and unresolved, resolution fails closed (no remote-fallback masking).
|
||||
</Note>
|
||||
|
||||
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](/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](/gateway/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.
|
||||
|
||||
### Tailscale Serve identity headers
|
||||
|
||||
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](/gateway/trusted-proxy-auth) instead.
|
||||
|
||||
See [Tailscale](/gateway/tailscale) and [Web overview](/web).
|
||||
|
||||
### Reverse proxy configuration
|
||||
|
||||
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.
|
||||
|
||||
```yaml
|
||||
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:
|
||||
|
||||
```nginx
|
||||
# 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).
|
||||
|
||||
### HSTS and origin notes
|
||||
|
||||
- OpenClaw's gateway is local/loopback first. If you terminate TLS at a reverse proxy, set HSTS there.
|
||||
- If the gateway itself terminates HTTPS, `gateway.http.securityHeaders.strictTransportSecurity` emits the HSTS header from OpenClaw responses.
|
||||
- Non-loopback Control UI deployments require `gateway.controlUi.allowedOrigins` by default; `allowedOrigins: ["*"]` is an explicit allow-all policy, not a hardened default - avoid it outside tightly controlled local testing.
|
||||
- Browser-origin auth failures on loopback are still rate-limited even with the general loopback exemption enabled, but the lockout key is scoped per normalized `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.
|
||||
- Treat DNS rebinding and proxy-host header behavior as deployment hardening concerns; keep `trustedProxies` tight and avoid exposing the gateway directly to the public internet.
|
||||
- Detailed deployment guidance: [Trusted Proxy Auth](/gateway/trusted-proxy-auth#tls-termination-and-hsts).
|
||||
|
||||
### Control UI over HTTP
|
||||
|
||||
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.
|
||||
- Separate from those flags, a successful `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.
|
||||
|
||||
### Insecure/dangerous flags
|
||||
|
||||
`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`.
|
||||
|
||||
<AccordionGroup>
|
||||
<Accordion title="Flags tracked by the audit today">
|
||||
- `gateway.controlUi.allowInsecureAuth=true`
|
||||
- `gateway.controlUi.dangerouslyAllowHostHeaderOriginFallback=true`
|
||||
- `gateway.controlUi.dangerouslyDisableDeviceAuth=true`
|
||||
- `security.audit.suppressions configured (<count>)`
|
||||
- `hooks.gmail.allowUnsafeExternalContent=true`
|
||||
- `hooks.mappings[<index>].allowUnsafeExternalContent=true`
|
||||
- `tools.exec.applyPatch.workspaceOnly=false`
|
||||
- `plugins.entries.acpx.config.permissionMode=approve-all`
|
||||
|
||||
</Accordion>
|
||||
|
||||
<Accordion title="All dangerous*/dangerously* keys in the config schema">
|
||||
Control UI and browser:
|
||||
- `gateway.controlUi.dangerouslyAllowHostHeaderOriginFallback`
|
||||
- `gateway.controlUi.dangerouslyDisableDeviceAuth`
|
||||
- `browser.ssrfPolicy.dangerouslyAllowPrivateNetwork`
|
||||
|
||||
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`
|
||||
|
||||
</Accordion>
|
||||
</AccordionGroup>
|
||||
|
||||
## Deployment and host trust
|
||||
|
||||
- Full-disk encryption on the gateway host; prefer a dedicated OS user account for the Gateway if the host is shared.
|
||||
- Published package dependency lock: source checkouts use `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](/gateway/security/shrinkwrap).
|
||||
- Secure file operations: OpenClaw uses `@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](/gateway/security/secure-file-operations).
|
||||
- Shared Slack workspace risk: if everyone in Slack can message the bot, the core risk is delegated tool authority - any allowed sender can induce tool calls (`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.
|
||||
- Company-shared agent (acceptable pattern): fine when everyone using the agent is in the same trust boundary (for example one company team) and the agent is strictly business-scoped. Run it on a dedicated machine/VM/container, use a dedicated OS user + dedicated browser/profile/accounts, and do not sign that runtime into personal Apple/Google accounts or personal password-manager/browser profiles. Mixing personal and company identities on the same runtime collapses the separation and increases personal-data exposure risk.
|
||||
|
||||
## Secrets on disk
|
||||
|
||||
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](/plugins/codex-harness#share-threads-with-codex-desktop-and-cli). |
|
||||
| `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. |
|
||||
|
||||
### Credential storage map
|
||||
|
||||
Also useful for backup decisions:
|
||||
|
||||
- WhatsApp: `~/.openclaw/credentials/whatsapp/<accountId>/creds.json`
|
||||
- Telegram bot token: config/env or `channels.telegram.tokenFile` (regular file only; symlinks rejected)
|
||||
- Discord bot token: config/env or SecretRef (env/file/exec providers)
|
||||
- Slack tokens: config/env (`channels.slack.*`)
|
||||
- Pairing allowlists: `~/.openclaw/credentials/<channel>-allowFrom.json` (default account) / `<channel>-<accountId>-allowFrom.json` (non-default accounts)
|
||||
- Model auth profiles: `~/.openclaw/agents/<agentId>/agent/auth-profiles.json`
|
||||
- Legacy OAuth import: `~/.openclaw/credentials/oauth.json`
|
||||
|
||||
Hardening: 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.
|
||||
|
||||
### File permissions
|
||||
|
||||
- `~/.openclaw/openclaw.json`: `600` (user read/write only)
|
||||
- `~/.openclaw`: `700` (user only)
|
||||
|
||||
`openclaw doctor` can warn and offer to tighten these.
|
||||
|
||||
### Workspace `.env` files
|
||||
|
||||
OpenClaw loads workspace-local `.env` files for agents and tools, but never lets them silently override gateway runtime controls:
|
||||
|
||||
- Provider credential environment variables are blocked from untrusted workspace `.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.
|
||||
- Any key starting with `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.
|
||||
- Channel endpoint settings for Matrix, Mattermost, IRC, and Synology Chat are also blocked from workspace `.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`.
|
||||
- Trusted process/OS environment variables, global runtime dotenv, config `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.
|
||||
|
||||
### Logs and transcripts
|
||||
|
||||
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.
|
||||
|
||||
- Keep log/transcript redaction on (`logging.redactSensitive: "tools"`, default).
|
||||
- Add custom patterns for your environment via `logging.redactPatterns` (tokens, hostnames, internal URLs).
|
||||
- When sharing diagnostics, prefer `openclaw status --all` (pasteable, secrets redacted) over raw logs.
|
||||
- Prune old session transcripts and log files if you do not need long retention.
|
||||
|
||||
Details: [Logging](/gateway/logging)
|
||||
|
||||
## Secure baseline (copy/paste)
|
||||
|
||||
```json5
|
||||
{
|
||||
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).
|
||||
|
||||
### Separate numbers (WhatsApp, Signal, Telegram)
|
||||
|
||||
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.
|
||||
|
||||
## Incident response
|
||||
|
||||
### Contain
|
||||
|
||||
1. Stop it: stop the macOS app (if it supervises the Gateway) or terminate your `openclaw gateway` process.
|
||||
2. Close exposure: set `gateway.bind: "loopback"` (or disable Tailscale Funnel/Serve) until you understand what happened.
|
||||
3. Freeze access: switch risky DMs/groups to `dmPolicy: "disabled"` / require mentions, and remove any `"*"` allow-all entries.
|
||||
|
||||
### Rotate (assume compromise if secrets leaked)
|
||||
|
||||
1. Rotate Gateway auth (`gateway.auth.token` / `OPENCLAW_GATEWAY_PASSWORD`) and restart.
|
||||
2. Rotate remote client secrets (`gateway.remote.token` / `.password`) on any machine that can call the Gateway.
|
||||
3. Rotate provider/API credentials (WhatsApp creds, Slack/Discord tokens, model/API keys in `auth-profiles.json`, and encrypted secrets payload values when used).
|
||||
|
||||
### Audit
|
||||
|
||||
1. Check Gateway logs: `/tmp/openclaw/openclaw-YYYY-MM-DD.log` (or `logging.file`).
|
||||
2. Review the relevant transcript(s): `~/.openclaw/agents/<agentId>/sessions/*.jsonl`.
|
||||
3. Review recent config changes that could have widened access: `gateway.bind`, `gateway.auth`, DM/group policies, `tools.elevated`, plugin changes.
|
||||
4. Re-run `openclaw security audit --deep` and confirm critical findings are resolved.
|
||||
|
||||
### Collect for a report
|
||||
|
||||
- Timestamp, gateway host OS + OpenClaw version.
|
||||
- The session transcript(s) + a short log tail (after redacting).
|
||||
- What the attacker sent and what the agent did.
|
||||
- Whether the Gateway was exposed beyond loopback (LAN/Tailscale Funnel/Serve).
|
||||
|
||||
## Secret scanning
|
||||
|
||||
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:
|
||||
|
||||
```bash
|
||||
pre-commit run --all-files detect-private-key
|
||||
```
|
||||
|
||||
## Reporting security issues
|
||||
|
||||
Found a vulnerability in OpenClaw? Report responsibly:
|
||||
|
||||
1. Email: [security@openclaw.ai](mailto:security@openclaw.ai)
|
||||
2. Do not post publicly until fixed.
|
||||
3. We will credit you (unless you prefer anonymity).
|
||||
74
docs/gateway/security/secure-file-operations.md
Normal file
74
docs/gateway/security/secure-file-operations.md
Normal file
@@ -0,0 +1,74 @@
|
||||
---
|
||||
summary: "How OpenClaw handles local file access safely, and why the optional fs-safe Python helper is off by default"
|
||||
read_when:
|
||||
- Changing file access, archive extraction, workspace storage, or plugin filesystem helpers
|
||||
title: "Secure file operations"
|
||||
---
|
||||
|
||||
OpenClaw uses [`@openclaw/fs-safe`](https://github.com/openclaw/fs-safe) for security-sensitive local file operations: root-bounded reads/writes, atomic replacement, archive extraction, temp workspaces, JSON state, and secret-file handling.
|
||||
|
||||
It is a **library guardrail** for trusted OpenClaw code that receives untrusted path names, not a sandbox. Host filesystem permissions, OS users, containers, and the agent/tool policy still define the real blast radius.
|
||||
|
||||
## Default: no Python helper
|
||||
|
||||
OpenClaw sets the fs-safe POSIX Python helper to **off** by default:
|
||||
|
||||
- the gateway should not spawn a persistent Python sidecar unless an operator opts in;
|
||||
- most installs do not need the extra parent-directory mutation hardening;
|
||||
- disabling Python keeps runtime behavior predictable across desktop, Docker, CI, and bundled-app environments.
|
||||
|
||||
OpenClaw only changes the _default_. An explicit setting always wins:
|
||||
|
||||
```bash
|
||||
# Default OpenClaw behavior: Node-only fs-safe fallbacks.
|
||||
OPENCLAW_FS_SAFE_PYTHON_MODE=off
|
||||
|
||||
# Opt into the helper when available, falling back if unavailable.
|
||||
OPENCLAW_FS_SAFE_PYTHON_MODE=auto
|
||||
|
||||
# Fail closed if the helper cannot start.
|
||||
OPENCLAW_FS_SAFE_PYTHON_MODE=require
|
||||
|
||||
# Optional explicit interpreter path.
|
||||
OPENCLAW_FS_SAFE_PYTHON=/usr/bin/python3
|
||||
```
|
||||
|
||||
The generic fs-safe env names also work: `FS_SAFE_PYTHON_MODE` and `FS_SAFE_PYTHON`.
|
||||
|
||||
Use `require` (not `auto`) when the helper is part of your security posture; `auto` silently falls back to Node-only behavior if the helper cannot start.
|
||||
|
||||
## What stays protected without Python
|
||||
|
||||
With the helper off, OpenClaw still gets fs-safe's Node-only guardrails:
|
||||
|
||||
- rejects relative-path escapes (`..`), absolute paths, and path separators where only bare names are allowed;
|
||||
- resolves operations through a trusted root handle instead of ad-hoc `path.resolve(...).startsWith(...)` checks;
|
||||
- refuses symlink and hardlink patterns on APIs that require that policy;
|
||||
- opens files with identity checks where the API returns or consumes file contents;
|
||||
- writes state/config files via atomic sibling-temp + rename;
|
||||
- enforces byte limits for reads and archive extraction;
|
||||
- applies private file modes for secrets and state files where the API requires them.
|
||||
|
||||
This covers OpenClaw's normal threat model: trusted gateway code handling untrusted model/plugin/channel path input inside a single trusted operator boundary.
|
||||
|
||||
## What Python adds
|
||||
|
||||
On POSIX, the optional helper keeps one persistent Python process and uses fd-relative filesystem operations for parent-directory mutations: rename, remove, mkdir, stat/list, and some write paths.
|
||||
|
||||
That narrows same-UID race windows where another process swaps a parent directory between validation and mutation — defense in depth on hosts where untrusted local processes can modify the same directories OpenClaw operates in.
|
||||
|
||||
If your deployment has that risk and Python is guaranteed to exist, set:
|
||||
|
||||
```bash
|
||||
OPENCLAW_FS_SAFE_PYTHON_MODE=require
|
||||
```
|
||||
|
||||
## Plugin and core guidance
|
||||
|
||||
- Plugin-facing file access should go through `openclaw/plugin-sdk/*` helpers, not raw `fs`, when a path comes from a message, model output, config, or plugin input.
|
||||
- Core code should use the fs-safe wrappers under `src/infra/*` so OpenClaw's process policy applies consistently.
|
||||
- Archive extraction should use the fs-safe archive helpers with explicit size, entry-count, link, and destination limits.
|
||||
- Secrets should use OpenClaw secret helpers or fs-safe secret/private-state helpers; do not hand-roll mode checks around `fs.writeFile`.
|
||||
- For hostile local-user isolation, do not rely on fs-safe alone. Run separate gateways under separate OS users/hosts, or use sandboxing.
|
||||
|
||||
Related: [Security](/gateway/security), [Sandboxing](/gateway/sandboxing), [Exec approvals](/tools/exec-approvals), [Secrets](/gateway/secrets).
|
||||
79
docs/gateway/security/shrinkwrap.md
Normal file
79
docs/gateway/security/shrinkwrap.md
Normal file
@@ -0,0 +1,79 @@
|
||||
---
|
||||
summary: "Plain-English and technical explanation of npm shrinkwrap in OpenClaw releases"
|
||||
read_when:
|
||||
- You want to know what npm shrinkwrap means in an OpenClaw release
|
||||
- You are reviewing package lockfiles, dependency changes, or supply-chain risk
|
||||
- You are validating root or plugin npm packages before publishing
|
||||
title: "npm shrinkwrap"
|
||||
---
|
||||
|
||||
OpenClaw source checkouts use `pnpm-lock.yaml`. Published OpenClaw npm packages use `npm-shrinkwrap.json`, npm's publishable dependency lockfile, so package installs use the dependency graph reviewed during release.
|
||||
|
||||
## Why it matters
|
||||
|
||||
Shrinkwrap is a receipt for the dependency tree that ships with an npm package: it tells npm which exact transitive versions to install.
|
||||
|
||||
| File | Where it matters | What it means |
|
||||
| --------------------- | ------------------------ | --------------------------------- |
|
||||
| `pnpm-lock.yaml` | OpenClaw source checkout | Maintainer dependency graph |
|
||||
| `npm-shrinkwrap.json` | Published npm package | npm install graph for users |
|
||||
| `package-lock.json` | Local npm apps | Not the OpenClaw publish contract |
|
||||
|
||||
For OpenClaw releases this means:
|
||||
|
||||
- the published package does not ask npm to invent a fresh dependency graph at install time;
|
||||
- dependency changes are reviewable because they land in a lockfile diff;
|
||||
- release validation tests the same graph users will install;
|
||||
- package-size or native-dependency surprises surface before publishing.
|
||||
|
||||
Shrinkwrap is not a sandbox. It does not make a dependency safe by itself, and it does not replace host isolation, `openclaw security audit`, package provenance, or install smoke tests.
|
||||
|
||||
OpenClaw is a gateway, plugin host, model router, and agent runtime, so a default install affects startup time, disk use, native package downloads, and supply-chain exposure. Shrinkwrap gives release review a stable boundary: reviewers see transitive dependency movement, validators reject unexpected lockfile drift, and plugin packages carry their own locked dependency graph instead of relying on the root package.
|
||||
|
||||
## Generating and checking
|
||||
|
||||
The root `openclaw` npm package, OpenClaw-owned npm plugin packages (for example `@openclaw/discord`), and publishable workspace packages such as [`@openclaw/ai`](/reference/openclaw-ai) include `npm-shrinkwrap.json` when they publish. Workspace dependencies are omitted from the root shrinkwrap because they publish beside the root package; each publishable workspace package pins its own transitive tree instead. Suitable plugin packages can also publish with explicit `bundledDependencies`, carrying their runtime dependency files in the plugin tarball instead of relying only on install-time resolution.
|
||||
|
||||
```bash
|
||||
# All shrinkwrap-managed packages (root + publishable plugins)
|
||||
pnpm deps:shrinkwrap:generate
|
||||
pnpm deps:shrinkwrap:check
|
||||
|
||||
# Root package only
|
||||
pnpm deps:shrinkwrap:root:generate
|
||||
pnpm deps:shrinkwrap:root:check
|
||||
|
||||
# Only packages affected by the current changeset
|
||||
pnpm deps:shrinkwrap:changed:generate
|
||||
pnpm deps:shrinkwrap:changed:check
|
||||
```
|
||||
|
||||
The generator resolves npm's publishable lock format but rejects generated package versions that are not already present in `pnpm-lock.yaml`. That keeps the pnpm dependency age, override, and patch-review boundary intact.
|
||||
|
||||
Review these as security-sensitive:
|
||||
|
||||
- `pnpm-lock.yaml`
|
||||
- `npm-shrinkwrap.json`
|
||||
- bundled plugin dependency payloads
|
||||
- any `package-lock.json` diff
|
||||
|
||||
OpenClaw package validators require shrinkwrap in new root package tarballs and reject `package-lock.json` for published packages. The plugin npm publish path checks plugin-local shrinkwrap, installs package-local bundled dependencies, then packs or publishes.
|
||||
|
||||
## Inspecting a published package
|
||||
|
||||
Root package:
|
||||
|
||||
```bash
|
||||
npm pack openclaw@<version> --json --pack-destination /tmp/openclaw-pack
|
||||
tar -tf /tmp/openclaw-pack/openclaw-<version>.tgz | grep '^package/npm-shrinkwrap.json$'
|
||||
```
|
||||
|
||||
Plugin package:
|
||||
|
||||
```bash
|
||||
npm pack @openclaw/discord@<version> --json --pack-destination /tmp/openclaw-plugin-pack
|
||||
tar -tf /tmp/openclaw-plugin-pack/openclaw-discord-<version>.tgz | grep '^package/npm-shrinkwrap.json$'
|
||||
tar -tf /tmp/openclaw-plugin-pack/openclaw-discord-<version>.tgz | grep '^package/node_modules/'
|
||||
```
|
||||
|
||||
Background: [npm-shrinkwrap.json](https://docs.npmjs.com/cli/v11/configuring-npm/npm-shrinkwrap-json).
|
||||
Reference in New Issue
Block a user