#Openclaw localhost ssrf how to resolve it

1 messages · Page 1 of 1 (latest)

wooden falcon
#

Openclaw localhost ssrf how to resolve it I dont have any public domain and Public IP I want to use localhost or internal ip what is the way around

stark totemBOT
#

To help others find answers, you can mark your question as solved via Right click solution message -> Apps -> âś… Mark Solution

broken slate
#

@magic tide

magic tideBOT
#

This depends which thing is hitting SSRF, but the safe answer is: don’t disable it globally unless you trust that network.

If this is the browser tool trying to open localhost / LAN IP, add a narrow browser SSRF exception in ~/.openclaw/openclaw.json:

{
  browser: {
    ssrfPolicy: {
      allowedHostnames: ["localhost", "127.0.0.1"]
      // hostnameAllowlist: ["my-internal-host.local"] // for named internal hosts
    }
  }
}

For a LAN/internal IP where hostname allowlisting isn’t enough, the broader opt-in is:

#
{
  browser: {
    ssrfPolicy: {
      dangerouslyAllowPrivateNetwork: true
    }
  }
}

Use that only on a trusted personal machine/network, then restart the gateway. Docs: https://docs.openclaw.ai/tools/browser#configuration

If you mean accessing the OpenClaw dashboard/gateway remotely without public IP/domain, use an SSH tunnel or Tailscale instead of exposing it publicly:

ssh -N -L 18789:127.0.0.1:18789 user@host
wooden falcon
#

i am using claude api and vllm for LLM and local model tool call need to updated the file

magic tideBOT
#

Yep — for Claude API as the main model + vLLM local model/tool-calling, the file to update is usually:

~/.openclaw/openclaw.json

First, please paste:

openclaw --version

Then check your vLLM baseUrl. If it is 127.0.0.1 / localhost, OpenClaw normally trusts loopback model endpoints automatically. If it is LAN/Tailscale/private DNS like 192.168.x.x, 10.x.x.x, *.local, or a tailnet hostname, add request.allowPrivateNetwork: true under that model provider.

Example shape:

#
{
  models: {
    mode: "merge",
    providers: {
      vllm: {
        baseUrl: "http://127.0.0.1:8000/v1", // or your LAN/Tailscale URL
        apiKey: "sk-local",
        api: "openai-completions",
        request: { allowPrivateNetwork: true },
        timeoutSeconds: 300,
        models: [
          {
            id: "your-vllm-model-id",
            name: "Local vLLM Model",
            reasoning: false,
#
            input: ["text"],
            cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
            contextWindow: 128000,
            maxTokens: 8192
          }
        ]
      }
    }
  },
  agents: {
    defaults: {
      model: {
        primary: "anthropic/claude-...",
        small: "vllm/your-vllm-model-id"
      }
#

If you paste your redacted models + agents.defaults config and the exact SSRF error line, I can point to the exact key to change.

wooden falcon
#
  "agents": {
    "defaults": {
      "workspace": "~/.openclaw/wazuh-autopilot/workspace",
      "model": {
        "primary": "anthropic/claude-sonnet-4-5",
        "fallbacks": [
          "anthropic/claude-haiku-4-5"
        ]
      },

      // Model aliases for quick switching via /model command
      "models": {
        "anthropic/claude-sonnet-4-5": { "alias": "sonnet" },
        "anthropic/claude-haiku-4-5": { "alias": "haiku" }
      },

      "sandbox": {
        "mode": "all",
        "scope": "session"
        },
      // Heartbeat: periodic health/sweep tasks using cheap model
      "heartbeat": {
        "every": "30m",
        "model": "anthropic/claude-haiku-4-5"
      },

      // Memory search using embeddings
      "memorySearch": {
        "sources": ["memory", "sessions"],
        "provider": "local",
        "model": "/root/.cache/node-llama-cpp/models/embeddinggemma-300M.gguf"
      },

      "maxConcurrent": 3,
      "timeoutSeconds": 600
    },

    "list": [
      // =====================================================================
      // TRIAGE AGENT — First responder for alert analysis
      // =====================================================================
      {
        "id": "wazuh-triage",
        "default": true,
        "workspace": "~/.openclaw/wazuh-autopilot/agents/triage",
        "agentDir": "~/.openclaw/wazuh-autopilot/agents/triage",
        "tools": {
          "profile": "minimal",
          "alsoAllow": ["read", "edit", "web_fetch", "sessions_list", "sessions_history", "sessions_send"],
          "deny": ["write", "exec", "delete", "browser"]
        },
        "heartbeat": { "every": "10m" }
}
magic tideBOT
#

You’re close — the missing part is a top-level models.providers.vllm block. Your pasted config only has agents, so OpenClaw knows about Claude aliases, but not your local vLLM provider/model.

Add this as a sibling to agents, not inside agents.defaults:

{
  models: {
    mode: "merge",
    providers: {
      vllm: {
        baseUrl: "http://127.0.0.1:8000/v1", // change if your vLLM is elsewhere
        apiKey: "sk-local",
        api: "openai-completions",
        request: { allowPrivateNetwork: true }, // needed for LAN/Tailscale/private endpoints; ok for local too
        timeoutSeconds: 300,
        models: [
#
          {
            id: "YOUR_VLLM_MODEL_ID",
            name: "Local vLLM Model",
            reasoning: false,
            input: ["text"],
            cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
            contextWindow: 128000,
            maxTokens: 8192
          }
        ]
      }
    }
  },

  agents: {
#
    defaults: {
      model: {
        primary: "anthropic/claude-sonnet-4-5",
        fallbacks: ["anthropic/claude-haiku-4-5"]
      },

      models: {
        "anthropic/claude-sonnet-4-5": { "alias": "sonnet" },
        "anthropic/claude-haiku-4-5": { "alias": "haiku" },
        "vllm/YOUR_VLLM_MODEL_ID": { "alias": "local" }
      }
    }
  }
}
#

Key details:

  • Replace YOUR_VLLM_MODEL_ID with exactly what vLLM returns from GET /v1/models / openclaw models list --provider vllm.
  • In models.providers.vllm.models[].id, do not include vllm/.
  • In agents.defaults.models, do include vllm/, because that’s the selectable model ref.
  • Since you already set agents.defaults.models, it acts as an allowlist — so the vLLM model must be added there or /model local won’t work.

Then restart/check:

openclaw gateway restart
openclaw models list --provider vllm
openclaw status --all

Also paste openclaw --version if it still errors. Docs: https://docs.openclaw.ai/providers/vllm

wooden falcon
#

i mean in openclaw agent when i use web_fetch here i need to used public ip or domain here i want to use localhost or internal ip

magic tideBOT
#

Ah — got it. For web_fetch specifically, localhost/internal IPs are blocked by design. There isn’t a normal tools.web.fetch.allowPrivateNetwork: true equivalent for web_fetch.

The config you may have seen here:

tools: {
  web: {
    fetch: {
      ssrfPolicy: {
        allowRfc2544BenchmarkRange: true,
        allowIpv6UniqueLocalRange: true
      }
    }
  }
}
#

is only for special fake-IP proxy ranges (198.18.0.0/15, fc00::/7). It does not allow localhost, 127.0.0.1, 10.x, 192.168.x, etc.

For your use case, use the browser tool instead, because browser has the private-network opt-in:

{
  browser: {
    enabled: true,
    ssrfPolicy: {
      allowedHostnames: ["localhost", "127.0.0.1"],
      // or, for LAN/internal IPs you intentionally trust:
      dangerouslyAllowPrivateNetwork: true
    }
  }
}
#

And your agent currently denies browser:

deny: ["write", "exec", "delete", "browser"]

So change that agent tools block to allow browser, e.g.:

tools: {
  profile: "minimal",
  alsoAllow: ["read", "edit", "web_fetch", "browser", "sessions_list", "sessions_history", "sessions_send"],
  deny: ["write", "exec", "delete"]
}

Then restart the gateway:

wooden falcon
#

for web_fetch is there any temporary by pass

magic tideBOT
#

For web_fetch itself: no, there’s no temporary “allow localhost/internal IP” bypass today.

That’s intentional: web_fetch is treated like arbitrary URL fetching, so OpenClaw keeps loopback/LAN/metadata IPs blocked to avoid SSRF. The only web_fetch.ssrfPolicy exceptions are for special proxy fake-IP ranges, not normal localhost / 192.168.x / 10.x.

Practical temporary options:

  1. Use browser instead for internal pages, with:
browser: {
  ssrfPolicy: {
    allowedHostnames: ["localhost", "127.0.0.1"]
    // or dangerouslyAllowPrivateNetwork: true for trusted LAN/internal access
  }
}
  1. Expose the internal service through a safe tunnel / domain and fetch that public HTTPS URL.
#
  1. If this is for a very controlled local automation agent, allow a shell tool and have it use curl — but that’s a different security tradeoff, and I’d keep it tightly scoped rather than broad.

So: no hidden config flag for web_fetch localhost. If you need that exact capability, it should be a GitHub issue/feature request: https://github.com/openclaw/openclaw/issues

wooden falcon
#

how to achive same thing with curl

MANDATORY: Update Case Status via API
After completing triage, you MUST invoke the web_fetch tool to advance the pipeline. If you skip this step, the pipeline stalls and no downstream agents are triggered.

Invoke the web_fetch tool with the following URL (replace {case_id} with the actual case ID from the webhook message, and use your xxxxxx token):

web_fetch(url="http://<PUBLIC_IP>:9091/api/agent-action/update-case?case_id={case_id}&status=triaged&token=xxxxx")

this is my agent Agents.md file content right now its with web_fetch how to achive same with curl what to change in Agents.md and openclaw.json

magic tideBOT
#

Yes — use exec + curl, not web_fetch.

Important tradeoff: exec is a shell surface, so keep it narrowly scoped. Don’t enable full unrestricted exec for a public/support-facing agent if you can avoid it.

In openclaw.json, change your triage agent tools from this:

tools: {
  profile: "minimal",
  alsoAllow: ["read", "edit", "web_fetch", "sessions_list", "sessions_history", "sessions_send"],
  deny: ["write", "exec", "delete", "browser"]
}

to something like this:

#
tools: {
  profile: "minimal",
  alsoAllow: ["read", "edit", "exec", "process", "sessions_list", "sessions_history", "sessions_send"],
  deny: ["write", "delete", "browser"],
  exec: {
    host: "gateway",        // important if localhost is on the OpenClaw host
    security: "allowlist",  // safer than full
    ask: "on-miss",
    timeoutSec: 30
  }
}

Why host: "gateway": with your sandbox config, default exec may run inside the sandbox/container. In that case localhost means the sandbox, not your API service. gateway makes curl run from the OpenClaw host.

Then update your agent AGENTS.md instruction from web_fetch(...) to exec:

#
## MANDATORY: Update Case Status via API

After completing triage, you MUST invoke the `exec` tool to advance the pipeline.
Do not use `web_fetch` for this step; localhost/internal URLs are blocked there.

Run this curl command, replacing CASE_ID with the actual case id:

curl -fsS --get "http://127.0.0.1:9091/api/agent-action/update-case" \
  --data-urlencode "case_id=CASE_ID" \
  --data-urlencode "status=triaged" \
  --data-urlencode "token=xxxxx"

If curl exits non-zero, report the failure and do not claim the case was updated.

If the API is on another internal host, use e.g.:

#
curl -fsS --get "http://192.168.1.50:9091/api/agent-action/update-case" \
  --data-urlencode "case_id=CASE_ID" \
  --data-urlencode "status=triaged" \
  --data-urlencode "token=xxxxx"

Safer token version: put the token in a file like /home/YOURUSER/.openclaw/wazuh-autopilot/case-update.token, chmod 600, then use:

curl -fsS --get "http://127.0.0.1:9091/api/agent-action/update-case" \
  --data-urlencode "case_id=CASE_ID" \
  --data-urlencode "status=triaged" \
  --data-urlencode "token@/home/YOURUSER/.openclaw/wazuh-autopilot/case-update.token"

After config change:

wooden falcon
#
  // ---------------------------------------------------------------------------
  // GLOBAL TOOL POLICY
  // ---------------------------------------------------------------------------
  "tools": {
    "profile": "minimal",
    "alsoAllow": ["read", "web_fetch", "sessions_list", "sessions_history", "sessions_send"],
    "deny": ["browser", "canvas"],
    "sandbox": {
      "tools": {
        "allow": ["group:runtime", "group:fs", "group:sessions", "group:memory", "group:web"],
    }
    },
    "web": {
      "search": { "enabled": false },
      "fetch": { "enabled": true }
    }
  },

need to change anything globally

magic tideBOT
#

Yes — if you want the agent to use curl, add exec globally or only on that triage agent. Since your global profile is minimal, I’d add it explicitly.

Change global tools to include exec + process, and add an exec policy:

"tools": {
  "profile": "minimal",
  "alsoAllow": [
    "read",
    "web_fetch",
    "exec",
    "process",
    "sessions_list",
    "sessions_history",
    "sessions_send"
  ],
#
  "deny": ["browser", "canvas"],

  "exec": {
    "host": "gateway",
    "security": "allowlist",
    "ask": "on-miss",
    "timeoutSec": 30
  },

  "sandbox": {
    "tools": {
      "allow": ["group:runtime", "group:fs", "group:sessions", "group:memory", "group:web"]
    }
  },
#
  "web": {
    "search": { "enabled": false },
    "fetch": { "enabled": true }
  }
}

Main point: host: "gateway" makes curl http://127.0.0.1:9091/... run from the OpenClaw host. If it runs inside sandbox, localhost may point at the sandbox instead.

For your triage agent, also remove exec from its deny list and add it to alsoAllow:

"alsoAllow": ["read", "edit", "exec", "process", "sessions_list", "sessions_history", "sessions_send"],
"deny": ["write", "apply_patch", "browser", "canvas"]
#

Then restart:

openclaw gateway restart

One note: security: "allowlist" means first run may ask approval. If this is a fully unattended pipeline, you’ll need to allowlist that exact curl command via exec approvals; don’t set security: "full" unless this agent is isolated/trusted.

wooden falcon
#

i dont want approval.

magic tideBOT
#

Got it. Don’t use security: "full" unless this is a totally private throwaway box — that gives the agent no-approval shell access.

Better no-approval setup: allowlist one wrapper script, then set exec to allowlist + ask off. That means the pipeline won’t pause, but random shell commands still won’t run.

1) Add exec globally

"tools": {
  "profile": "minimal",
  "alsoAllow": [
    "read",
    "web_fetch",
    "exec",
    "process",
    "sessions_list",
    "sessions_history",
#
    "sessions_send"
  ],
  "deny": ["browser", "canvas"],

  "exec": {
    "host": "gateway",
    "security": "allowlist",
    "ask": "off",
    "timeoutSec": 30
  },

  "sandbox": {
    "tools": {
      "allow": ["group:runtime", "group:fs", "group:sessions", "group:memory", "group:web"]
    }
#
  },

  "web": {
    "search": { "enabled": false },
    "fetch": { "enabled": true }
  }
}

2) Remove exec from the triage agent deny list

"alsoAllow": ["read", "edit", "exec", "process", "sessions_list", "sessions_history", "sessions_send"],
"deny": ["write", "apply_patch", "browser", "canvas"]
#

3) Create a tiny wrapper script

Example path:

mkdir -p ~/.openclaw/wazuh-autopilot/bin
nano ~/.openclaw/wazuh-autopilot/bin/update-case-status
chmod 700 ~/.openclaw/wazuh-autopilot/bin/update-case-status

Script:

#!/usr/bin/env bash
set -euo pipefail
#
case_id="${1:?case_id required}"
status="${2:-triaged}"
token="xxxxx"

curl -fsS --get "http://127.0.0.1:9091/api/agent-action/update-case" \
  --data-urlencode "case_id=${case_id}" \
  --data-urlencode "status=${status}" \
  --data-urlencode "token=${token}"

Use your internal IP instead of 127.0.0.1 if the API is on another host.

4) Allowlist only that script, no prompts

#
openclaw approvals allowlist add --gateway --agent wazuh-triage "$HOME/.openclaw/wazuh-autopilot/bin/update-case-status"
openclaw config set tools.exec.host gateway
openclaw config set tools.exec.security allowlist
openclaw config set tools.exec.ask off
openclaw gateway restart

5) Update AGENTS.md instruction

After completing triage, run the exec tool:

~/.openclaw/wazuh-autopilot/bin/update-case-status CASE_ID triaged

Replace CASE_ID with the actual case id. If the command fails, report the failure and do not claim the case was updated.
#

If you truly want unrestricted no-approval exec, the setting is security: "full", ask: "off", but I strongly would not use that for this agent. The wrapper + allowlist gives you unattended pipeline updates without handing the agent a blank shell check. 🦞

wooden falcon
#

no need of helper i can use full instead

magic tideBOT
#

You can, yes — that’s the no-approval mode.

Global openclaw.json:

"tools": {
  "profile": "minimal",
  "alsoAllow": [
    "read",
    "web_fetch",
    "exec",
    "process",
    "sessions_list",
    "sessions_history",
    "sessions_send"
  ],
#
  "deny": ["browser", "canvas"],

  "exec": {
    "host": "gateway",
    "security": "full",
    "ask": "off",
    "timeoutSec": 30
  },

  "sandbox": {
    "tools": {
      "allow": ["group:runtime", "group:fs", "group:sessions", "group:memory", "group:web"]
    }
  },
#
  "web": {
    "search": { "enabled": false },
    "fetch": { "enabled": true }
  }
}

For the wazuh-triage agent, make sure exec is not denied:

"tools": {
  "profile": "minimal",
  "alsoAllow": ["read", "edit", "exec", "process", "sessions_list", "sessions_history", "sessions_send"],
  "deny": ["write", "apply_patch", "browser", "canvas"],
  "exec": {
#
    "host": "gateway",
    "security": "full",
    "ask": "off",
    "timeoutSec": 30
  }
}

Then restart:

openclaw gateway restart

Update AGENTS.md from web_fetch to something like: