Files
openclaw/docs/gateway/security/audit-checks.md
Peter Steinberger bdcc6836e0 refactor(gateway): retire the Control UI device-auth migration window (#124667)
* refactor(gateway): delete the retired Control UI device-auth migration window

The one-time remediation window for the retired
gateway.controlUi.dangerouslyDisableDeviceAuth break-glass threaded a pending
state machine through the connect pipeline, startup bootstrap, lifecycle,
request context, device management, security audit, the hello frame, and a
Control UI banner flow — steady-state runtime carrying a retired-shape shim
the architecture reserves for doctor. The window is closed: the retired key
is now fully inert, doctor still detects and removes it, and a browser that
never completed the migration pairs through the normal device flow (one
approval, no data loss).

Deleted with it: the config-machine-state import of the legacy flag, the
hello deviceAuthMigration field (optional; clients validate envelopes only,
so older gateways emitting it stay compatible), the migration-bound device
management authz states, the requireNoPairingCapableOperator approval mode,
the effective-operator pairing event emitter whose only subscriber was the
migration completion, and the Control UI banner, loader, overlay wiring,
i18n strings, and e2e scenario support. Swift and Kotlin protocol models
regenerated.

Live-verified on an isolated gateway with the retired key present in config:
clean boot with no migration warning, silent CLI pairing and silent local
scope widening unaffected, and doctor reporting the key as retired-and-inert.

Net -1111 production LOC.

* fix(ci): finish migration-window cleanup surfaced by the gates

The doctor migration for the retired key kept its "Preserved for remediation"
change text and describe; both now state plain removal, with the retired
tests updated. peekStoredDeviceIdentityId lost its only production consumer
with the deleted banner loader and is removed with its test mock. Also fixes
the unrelated no-unnecessary-boolean-literal-compare lint break that #124636
landed on main in scripts/check-changed.mts (truthiness is equivalent for the
boolean-or-undefined TTY probe).

* fix(ui): drop the retired-key device-auth reader from the security summary

Review findings on the migration-window removal: the Control UI security
summary still derived its "Device auth" row from the retired
dangerouslyDisableDeviceAuth key, rendering device auth as disabled on
configurations that merely retain the inert key. Device auth is now
unconditionally enforced, so the row and its derivation are removed rather
than pinned to a constant. The build-admission test also tracks and removes
its temporary device-identity databases after each run.
2026-08-16 09:39:15 -07:00

153 lines
36 KiB
Markdown

---
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 (desktop input/camera/screen/contacts/calendar/SMS) | `gateway.nodes.commands.allow` | no |
| `gateway.nodes.deny_commands_ineffective` | warn | Pattern-like deny entries do not match shell text or groups | `gateway.nodes.commands.deny` | 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.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 |
| `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 |
| `browser.extension_relay_legacy_auth` | warn | Legacy extension relay bearer/Basic/token authentication remains enabled | `browser.extensionRelay.allowLegacyAuth` | 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.entries.*.tools.exec.host`, `agents.entries.*.sandbox.mode` | no |
| `tools.exec.security_full_configured` | warn/critical | Host exec is running with `security="full"` | `tools.exec.security`, `agents.entries.*.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.entries.*.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.entries.*.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.entries.*.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.entries.*.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.entries.*.tools.exec.safeBins` | no |
| `tools.exec.safe_bin_trusted_dirs_risky` | warn | `safeBinTrustedDirs` includes mutable or risky directories | `tools.exec.safeBinTrustedDirs`, `agents.entries.*.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 |
| `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 |
| `channels.discord.allowlisted_groups.broad_members` | warn | Allowlisted Discord guild/channel targets have no member or role restriction | `channels.discord.guilds.*.users/roles`, per-channel `users/roles` | no |
| `security.exposure.open_channels_with_exec` | warn/critical | Shared/public rooms can reach exec-enabled agents | `channels.*.dmPolicy`, `channels.*.groupPolicy`, `tools.exec.*`, `agents.entries.*.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.exposure.open_groups_with_control_plane_tools` | critical | Open DMs/groups can reach gateway/cron control-plane tools | DM/group policy paths, `tools.allow`, `tools.alsoAllow`, `tools.profile`, `gateway`, `cron` | 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.entries.*.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)