# Security + Exec Approval

GoClaw implements deep application-level security across multiple layers: path sandboxing, shell/HTTP allowlists, SSRF protection, PBKDF2 encryption, skill security scanning, SAST analysis, ingress IP allowlisting, and gateway auth.

## Security Architecture

```mermaid
flowchart TD
    subgraph SEC["GoClaw Security Layers"]
        GW["<b>Gateway</b><br/>Auth + Rate limit<br/>trustProxy · IP allowlist"]
        TOOL["<b>Tool-level</b><br/>Sandboxing<br/>Path / Shell · HTTP policy"]
        DATA["<b>Data-at-rest</b><br/>Encryption<br/>AES-256-GCM · PBKDF2 (600K)"]

        NET["<b>Network</b><br/>SSRF block<br/>Domain WL · Private IP"]
        CODE["<b>Code</b><br/>SAST scan<br/>Linter · gosec / ruff"]
        CONT["<b>Content</b><br/>Skill scanner<br/>Rule-based · Prompt inject"]

        GW --> NET
        TOOL --> CODE
        DATA --> CONT
    end
```

## Security Features

| # | Feature | OpenClaw | GoClaw | Status |
|---|---------|----------|---------|--------|
| 7.1 | **Path sandboxing** | Workspace isolation | `resolveFilePath()` — path traversal blocked, all paths resolved within workspace | Done |
| 7.2 | **Shell command allowlist** | Permission system | `tools.shell.allow` / `tools.shell.deny` in config. Empty allow = all blocked. Deny checked first. Entries match by prefix (`git` also matches `gitk`). A command containing a shell metacharacter (`;` `&` `\|` `$` `>` `<` `` ` `` `\` `!` `(` `)` `{` `}` `[` `]` `=` `#` `^` `%` `@` or a newline) is refused even with `allow: ["*"]`. Hardcoded deny patterns, matched anywhere in the command and case-insensitively: destructive commands (`rm -rf /`, `dd if=`, `mkfs.`, `format `), `shutdown`/`reboot`, Ansible, `make deploy`/`install`/`update`/`restart` and similar targets, service management (`systemctl`, `launchctl`, `service`), `ssh`/`scp`, system package installs, user management, Windows registry and firewall changes | Done |
| 7.3 | **DM pairing system** | Device pairing (web) | Pairing code for Discord/Teams DMs; approve on the Web UI's Pairing tab. A CLI approval made while the gateway runs reaches it only after a restart | Done |
| 7.4 | **DM policy modes** | Per-channel config | `dm_policy: pairing` (default), `allowlist`, `open` | Done |
| 7.5 | **Guild channel isolation** | Channel allowlists | Bot only responds to @mentions or direct replies in Discord guilds | Done |
| 7.6 | **Read-only database queries** | — | `db_query` allows only SELECT/SHOW/DESCRIBE/EXPLAIN. INSERT/UPDATE/DELETE blocked. Writes go through `db_write`, only on datasources with `allow_write: true`, only for admins and SOP/cron runs (not webhook-triggered or delegated runs), capped at `tools.max_db_write_rows` rows per call. `metrics_store` writes its own `agent_metrics` table on a datasource only with `allow_write`, for an admin or admin-authored SOP/cron run (not webhook-triggered or delegated runs); its first write creates the table, and its writes are not row-capped | Done |
| 7.7 | **Query timeout + row limits** | — | 30s timeout, which the database enforces too: about 29s on PostgreSQL and MySQL/MariaDB, and on ClickHouse where the account lets goclaw set `max_execution_time` (otherwise the profile's own); max 10,000 rows (`tools.max_db_query_rows`), max 1000 chars per cell | Done |
| 7.7a | **Read-only on the database** | — | `db_query` reads in a read-only PostgreSQL transaction; on MySQL/MariaDB with the connection's session set to `transaction_read_only=1` for the read (a read-only transaction alone still runs DDL); on ClickHouse with `readonly=1` where the account allows it (writes and `SET` refused; `url()`/`file()`/`remote()` stay refused by goclaw's checks, since `DESCRIBE TABLE url(…)` still connects out). `db_query`'s SQL never runs on PostgreSQL's simple protocol, which would run several statements from one query | Done |
| 7.8 | **HTTP response size limit** | — | `http_request` caps response body at 100KB | Done |
| 7.9 | **Tool loop safety** | — | `tools.max_tool_iterations` (default 20) prevents infinite LLM tool loops | Done |
| 7.10 | **Dangerous command hardcodes** | — | Fork bombs, disk wipes, shutdown commands blocked regardless of config | Done |
| 7.11 | **Git write gating** | — | Read ops always allowed, write ops require config flag, dangerous ops always blocked | Done |
| 7.12 | **File edit safety** | — | `file_edit` requires unique string match — no mass replacements | Done |
| 7.13 | **Bootstrap file protection** | Read-only MEMORY.md | For anyone but an admin or a SOP/cron run, `file_write`/`file_edit` block writes to MEMORY.md, SOUL.md, TOOLS.md, AGENTS.md, IDENTITY.md, USER.md, subagent profiles (`*.agent.md`) and instructions (`*.instructions.md`). No file tool writes inside a `.git` directory | Done |
| 7.14 | **Gateway auth (token)** | OAuth / device code | `X-GoClaw-Token` header, `Bearer` auth, health probes exempt | Done |
| 7.15 | **Rate limiting** | — | Per-IP token bucket middleware, configurable `gateway.rate_limit.per_minute` | Done |
| 7.16 | **Secrets encryption** | Credential store | AES-256-GCM vault + **PBKDF2 key derivation** (600K iterations) | Done |
| 7.17 | **Trust proxy** | — | `gateway.ingress.trust_proxy` (and `gateway.rate_limit.trust_proxy`) — only reads `X-Forwarded-For` when trusted, and then its last entry, the one the proxy wrote. Used by the ingress allowlist, the rate limits (gateway and App API) and the audit log. Prevents IP spoofing | Done |
| 7.18 | **HTTP default-deny policy** | — | nginx-style: empty allow list blocks all external HTTP. `default_policy: deny\|allow` | **Done (GoClaw only)** |
| 7.19 | **SSRF protection** | — | DNS-level private IP blocking (0.x, 10.x, 100.64-127.x, 172.16-31.x, 192.168.x, loopback, link-local, ULA IPv6) | **Done (GoClaw only)** |
| 7.20 | **Domain whitelist/blacklist** | — | `tools.http.allow` / `tools.http.deny` with wildcard (`*.mn`), CIDR, exact match | **Done (GoClaw only)** |
| 7.21 | **Internal hostname bypass** | — | Allowlisted hostnames that resolve to private IPs are permitted (e.g. `monitor.example.mn` → 10.0.2.7). A wildcard entry counts: see [Bypass Mechanisms](#bypass-mechanisms) | **Done (GoClaw only)** |
| 7.22 | **Per-agent HTTP scope** | — | Agent-level `tools.http.allow/deny` overrides global policy via `runtimeScope` | **Done (GoClaw only)** |
| 7.22a | **Hosts per run** | — | `tools.http.max_hosts_per_run` (default 100): one agent run, its subagents included, reaches at most that many different hosts through `SafeHTTPClient`; the next new host is refused and logged. A start-up warning names `shell.allow: ["*"]` or an open `default_policy: allow` on a gateway other machines can reach | **Done (GoClaw only)** |
| 7.23 | **Pre-flight URL validation** | — | `ValidateURL()` — domain policy + SSRF check before the browser tools navigate and before `notify` posts to a webhook URL. Other tools' requests are checked when they connect (`SafeHTTPClient`) | **Done (GoClaw only)** |
| 7.24 | **Skill Security Scanner** | — | Rules for prompt injection in skill text and dangerous code patterns. Blocks critical findings on install | **Done (GoClaw only)** |
| 7.25 | **SAST Scanner** | — | `run_sast_scanner` tool — runs gosec (Go) or bandit (Python) on workspace code; the scanner must be installed on the host | **Done (GoClaw only)** |
| 7.26 | **Code Linter** | — | `run_code_linter` tool — runs golangci-lint (Go), ruff (Python) or `dart analyze` (Dart/Flutter); the linter must be installed on the host | **Done (GoClaw only)** |
| 7.27 | **Ingress IP allowlist** | — | Webhook ingress: M365 preset auto-fetch or explicit IPs/CIDRs + access logging (see [below](#ingress-ip-allowlist-ms-teams)) | **Done (GoClaw only)** |
| 7.28 | **Memory write RBAC** | — | `memory_write`: `personal` scope for any identified (paired) user, never from SOP/cron/webhook runs; `agent_notes` and `agent_config` for admins and the roles/users listed under `memory.write_access` (SOP/cron/webhook runs may write `agent_notes` only, never `global` or `agent_config`); `global`: while `memory.write_access` is enabled (the default, with `allowed_roles: [system]`), only the roles and users it lists; with it disabled, admins. `memory_consolidate`: admins and SOP/cron runs. `update_core_instruction`: a human admin only | Done |
| 7.29 | **isHumanAdmin Check** | — | Critical actions (altering identity, persona) require a real human admin — automated SOP/Cron runs are blocked. | **Done (GoClaw only)** |
| 7.30 | **Rollback Safety** | — | SOP rollback commands are validated against the shell allowlist to prevent privilege escalation. | **Done (GoClaw only)** |
| 7.31 | Docker sandboxing | Docker-based isolation | — | **TODO** |


## SSRF Protection Detail (safe_client.go)

GoClaw's `SafeHTTPClient` implements DNS-level SSRF protection at the transport layer:

```mermaid
flowchart LR
    REQ["HTTP Request"] --> DOM["<b>Domain Filter</b>"]
    DOM -->|"Pass"| DNS["<b>DNS Resolve</b>"]
    DNS --> IP["<b>Private IP Check</b>"]
    IP -->|"Public / Allowed"| CONN["<b>Connect</b>"]

    DOM -->|"Deny list / Not in allow /<br/>default_policy=deny + empty allow"| B1["<b>BLOCK</b>"]
    IP -->|"10.x / 172.16-31.x / 192.168.x /<br/>loopback / link-local / IPv6 ULA"| B2["<b>BLOCK</b>"]
```

### Private IP Ranges Blocked
| Range | Description |
|-------|------------|
| `0.0.0.0/8` | "This host" (a connect to `0.0.0.0` reaches loopback) |
| `10.0.0.0/8` | Class A private |
| `100.64.0.0/10` | Carrier-grade NAT |
| `172.16.0.0/12` | Class B private |
| `192.168.0.0/16` | Class C private |
| `127.0.0.0/8`, `::1` | Loopback |
| `169.254.0.0/16`, `fe80::/10` | Link-local |
| `fc00::/7` | IPv6 Unique Local Address |

### Bypass Mechanisms
- Explicit IP/CIDR in `tools.http.allow` list. IP and CIDR entries match only a URL whose host is an IP address: a hostname that resolves into an allowed CIDR is still refused unless the hostname itself is allowed.
- Hostname in allow list that resolves to private IP (e.g. `monitor.example.mn` → `10.0.2.7`)
- A wildcard entry allows private addresses for every hostname it matches: with `*.mn`, any `.mn` name that resolves to a private address is reached, and `*` turns private-address blocking off for every host. Allow internal hosts by their exact name, and keep wildcards for public domains.
- An integration's configured host: `gitlab`, `sonarqube`, the observability tools, Doctor and the Integrations page's Test reach the host of the instance's own `url` (or Uptrace `dsn`) as if it were on allow (`WithConfiguredHost`), for their own requests only. The address is the admin's, not the model's; `http_request` and the browser are not affected, and every other host such a request reaches (a redirect) is checked as usual.
- The browser tools check the URL before navigating (`ValidateURL()`) by the same rules, and every request a page makes against the policy of the agent using the browser.
- Not bypassed: deny list always takes priority. An IP or CIDR on `tools.http.deny` is refused whichever host name resolves to it, a name on the allow list included.
- Keep-alive connections are pooled per HTTP policy (a configured integration host included), so a connection opened under one agent's policy, for an integration, or before a config reload changed the lists, is not reused by a request the current policy would refuse.

## HTTP Default-Deny Policy

```yaml
tools:
  http:
    default_policy: "deny"   # "deny" (default) or "allow"
    allow:
      - "*.googleapis.com"
      - "api.github.com"
      - "monitor.example.mn"           # internal hostname → private IP allowed
      - "10.0.2.0/24"      # explicit CIDR allowlist (URLs with an IP host only)
    deny:
      - "127.0.0.1"
      - "*.internal"
```

### Domain Matching
| Pattern | Matches |
|---------|---------|
| `*` | All domains, private addresses included |
| `*.mn` | `example.mn`, `sub.example.mn`, including names that resolve to private addresses |
| `api.github.com` | Exact match only |
| `10.0.2.0/24` | CIDR range, for URLs whose host is an IP address |
| `192.168.1.1` | Exact IP, for URLs whose host is that IP |

## Skill Security Scanner

| Layer | Rules | Detects |
|-------|:-----:|--------|
| **Prompt Injection** | `prompt-override`, `role-injection`, `jailbreak-attempt`, `shell-danger-in-skill`, `secret-extraction`, `hidden-instruction`, `invisible-unicode`, `data-exfil-url` | LLM manipulation, `curl\|bash`, credential theft, instructions hidden in HTML comments or zero-width characters |
| **Code Patterns** | `dangerous-exec`, `dynamic-code-exec`, `crypto-mining`, `reverse-shell`, `env-harvesting`, `potential-exfiltration`, `obfuscated-hex`, `obfuscated-base64`, `suspicious-network` | Shell injection, mining, exfiltration |

- Scans each skill installed with `goclaw skills install` or `goclaw skills add`, from any source
- Critical findings = **blocked + auto-removed**; warnings are printed and the skill stays installed
- `goclaw skills scan` / `goclaw skills scan <name>`

## Ingress IP Allowlist (MS Teams)

| Feature | Description |
|---------|-------------|
| M365 preset | `gateway.ingress.allow_preset: microsoft-teams` fetches the Microsoft 365 Skype/Teams IPv4 ranges from `endpoints.office.com` at startup. If the fetch fails, the gateway does not start |
| Explicit IPs | Direct IP addresses in `gateway.ingress.allow_ips`. A non-empty `allow_ips` replaces the preset: the two are not combined |
| CIDR ranges | Network ranges (e.g. `52.112.0.0/14`) |
| Scope | Without `gateway.ingress.port` the webhooks share the main port, and the allowlist then applies to the whole gateway, Web UI included. Set `gateway.ingress.port` to keep it to the webhook listener |
| Loopback | Requests from `127.0.0.0/8` and `::1` are always let through (CLI, local health probes) |
| Access logging | Every request logged via `runtimeinfo.RecordIngressAccess()` |
| Denied tracking | `runtimeinfo.RecordIngressDenied()` for monitoring |
| Web UI display | Gateway tab shows access logs table |

---

## Exec Approval (Section 19)

GoClaw's interactive approval covers SOP steps only. A step marked `requires_approval: true` waits for an approval made through the gateway API; everything else is governed by the allow/deny lists.

| # | Feature | OpenClaw | GoClaw | Status |
|---|---------|----------|---------|--------|
| 19.1 | Approval policy | `security` (deny/allowlist/full) | Shell allowlist + HTTP domain policy + per-agent scopes | Done |
| 19.2 | **Control UI approval** | Visual approve/deny in web panel | No dashboard control. A SOP step marked `requires_approval: true` is approved or rejected with `POST /api/sop/approve` (`{"job_id": "…", "action": "approve"}` or `"reject"` with a `"reason"`) and the gateway token. A step still waiting after 1 hour fails. | Partial |
| 19.3 | macOS app approval | Native dialog | — | **TODO** |
| 19.4 | **Channel approval** | `/approve <id>` | — (no chat or CLI command approves a step) | **TODO** |
| 19.5 | Allowlist persistence | `exec-approvals.json` | `config.yaml` based allow/deny lists with hot-reload support. | Done |
| 19.6 | Safe bins auto-allow | Known safe commands pre-approved | Hardcoded deny patterns + skill bin-check | Partial |
| 19.7 | Skill CLI auto-allow | Skills register their CLIs as safe | — | **TODO** |
| 19.8 | Approval forwarding | Route approval prompts to channels | A step waiting for approval sends a notice to the channel and target set in `sop.alerts` (else only to the log). The notice carries the job ID to pass to the API; the `sop approve <id>` it suggests is not a command. | **Done (GoClaw only)** |

---

## OpenClaw vs GoClaw Security Comparison

| Aspect | OpenClaw | GoClaw |
|--------|----------|---------|
| **Sandboxing model** | Docker container isolation | Application-level (path, shell, HTTP, IP) |
| **HTTP policy** | No default-deny | **nginx-style default-deny** |
| **SSRF protection** | — | **DNS-level private IP blocking** |
| **Domain filtering** | — | **Wildcard + CIDR + hostname matching** |
| **Secret encryption** | Credential store | **AES-256-GCM + PBKDF2 (600K iterations)** |
| **Skill scanning** | — | **Rule-based security scanner** |
| **Code analysis** | — | **SAST scanner + code linter (gosec, bandit, golangci-lint, ruff)** |
| **Ingress security** | — | **M365 IP preset + CIDR allowlist + access logs** |
| **Memory RBAC** | — | **Admin-only write for core tools** |
| **Interactive approval** | Multi-surface (web, macOS, channel) | SOP step approval through the API; otherwise shell/HTTP allowlists |
| **Per-agent scopes** | — | **Agent-level HTTP policy overrides** |

**GoClaw security philosophy:** Defense in depth at the application layer — no Docker dependency for security. Every tool has its own safety boundary (path traversal, SQL injection, SSRF, shell injection) with configurable allow/deny policies.

**Coverage:** the security features above are in place except Docker sandboxing; exec approval is limited to SOP steps approved through the API.
