#Tailscale setup for local OpenClaw gateway
1 messages · Page 1 of 1 (latest)
- Recommended architecture (safest / best-practice)
- Keep the OpenClaw Gateway bound to loopback (
127.0.0.1:18889) exactly as you have it now. - Put the host on Tailscale, then use Tailscale Serve to publish the Control UI + WebSocket over tailnet HTTPS without ever binding the Gateway to LAN/tailnet interfaces.
- Auth best-practice: keep normal Gateway auth (token/password) available; optionally allow the Serve identity-header shortcut for Control UI/WS (
gateway.auth.allowTailscale: true) only if you trust code running on the gateway host. (HTTP API endpoints still require token/password either way.)
Docs: https://docs.openclaw.ai/gateway/tailscale and https://docs.openclaw.ai/gateway/remote
- Safest exact next command (only):
curl -fsSL https://tailscale.com/install.sh | sh
(Your spawn tailscale ENOENT means the tailscale CLI isn’t available, so OpenClaw can’t do Serve automation yet.)
- Should gateway bind stay loopback or be changed?
Stay on loopback. For this goal, you do not wantgateway.bind: "tailnet"or"lan"—Serve is specifically the “keep it on localhost, expose safely via Tailscale” path.
openclaw config get gateway.auth.allowTailscale returning “Config path not found …” just means the key is currently unset in your JSON5, not that the build doesn’t support it. (config get reads your config file; it doesn’t print defaults.)
In v2026.3.13 the config key is:
gateway.auth.allowTailscale(boolean)
Safest exact next step (only) to enable Tailscale identity-header auth for Control UI / WS:
openclaw config set gateway.auth.allowTailscale true
Notes (so you can choose the right security posture):
- If you want the strictest setup, token/password-only is always valid: set
gateway.auth.allowTailscale falseand keep using the token/password in the UI. - With
allowTailscale: true, Control UI/WS can be tokenless when reached via Tailscale Serve; HTTP API endpoints still require token/password.
Hey! I run a similar setup — gateway on loopback + Tailscale. A few practical tips:
-
Keep gateway bound to loopback (
127.0.0.1). Don't change it. Tailscale Serve will handle the rest. -
The
spawn tailscale ENOENTerror just means Tailscale CLI isn't on the PATH that OpenClaw sees. Install Tailscale first: https://tailscale.com/download/linux — oncetailscale statusworks in your terminal, OpenClaw will pick it up after a gateway restart. -
After Tailscale is installed and connected, the simplest safe approach:
tailscale serve --bg https+insecure://127.0.0.1:18889
This exposes your gateway over your tailnet via HTTPS with Tailscale's built-in certs. Only devices on your tailnet can reach it.
- You do NOT need to change
bindto0.0.0.0— that would expose the gateway to your whole LAN which you don't want.
The key insight: Tailscale Serve acts as a reverse proxy on the same machine, so loopback binding is exactly right.
Hal’s overall instinct is right (loopback bind + Tailscale proxy is the safe shape), but I’d tweak two details to keep it “OpenClaw-best-practice” and avoid Tailscale CLI/version footguns:
-
Yes: keep the Gateway bound to loopback. Don’t switch to
0.0.0.0just to make it reachable. That’s exactly what you don’t want. -
spawn tailscale ENOENT: that’s not a “Tailscale is broken” signal, it’s simply “thetailscalebinary wasn’t found in the environment OpenClaw is running under” (either not installed or not on the PATH seen by the systemd user service). Iftailscale statusworks as the same user that runs the gateway, restarting the gateway usually resolves it. -
On the
tailscale serve …command: the Serve CLI has changed across Tailscale versions, and thehttps+insecure://…style isn’t what OpenClaw docs recommend. The cleanest approach is to let OpenClaw configure Serve for you:- set
gateway.tailscale.mode: "serve"(or runopenclaw gateway --tailscale serve) - keep
gateway.bind: "loopback" - then access via
https://<magicdns>/(OpenClaw will also be able to use/verify identity headers whengateway.auth.allowTailscale: true)
- set
OpenClaw docs for this exact flow:
If you want the strictest security posture even on tailnet: keep gateway.auth.allowTailscale: false and require token/password in the UI. Otherwise, allowTailscale: true is the convenience option (still tailnet-only, but assumes the gateway host itself is trusted).
OpenClaw 2026.3.13 — status --all still reports missing scope: operator.read even though RPC + health are OK
Environment
- OpenClaw:
2026.3.13 - OS: Ubuntu 24
- Gateway URL:
ws://127.0.0.1:18889 - Gateway bind: loopback
- Service model: systemd user service
What is already proven locally
1) Gateway is actually running
systemctl --user status openclaw-gateway.service --no-pager shows:
Loaded: loaded (...; enabled; preset: enabled)Active: active (running)
2) Gateway listener is live
status --all and gateway status --json both show port 18889 is busy with:
- PID
80056 - user
openclaw2 - command
openclaw-gateway
3) Direct gateway RPC works
Command:
openclaw gateway status --json --url ws://127.0.0.1:18889 --token <known-good-token>
Result:
"rpc": { "ok": true, "url": "ws://127.0.0.1:18889" }
4) Built-in health is OK
Command:
openclaw health --json
Result:
"ok": true
5) Service-runtime check inside gateway status still misreports
Even with:
XDG_RUNTIME_DIR=/run/user/1003DBUS_SESSION_BUS_ADDRESS=unix:path=/run/user/1003/bus
openclaw gateway status --json ... still reports:
"runtime": { "status": "unknown" }- detail includes:
systemctl --user unavailable: Failed to connect to bus: Permission deniedFailed to read server status: Transport endpoint is not connected
But in the same general sudo/run context, this works:
systemctl --user status openclaw-gateway.service --no-pager
So local systemd/user-bus service truth is good; the mismatch seems to be in OpenClaw’s own service-runtime probe path.
Auth / scope work already done
6) Original root cause for gateway auth error was real
openclaw status --all reported:
Gateway unreachable (missing scope: operator.read)
7) We inspected device/token state directly
Using:
openclaw devices list --json --url ws://127.0.0.1:18889 --token <configured-token>
We found several openclaw-control-ui device entries that only had:
operator.adminoperator.approvalsoperator.pairing
One CLI device entry had full scopes including:
operator.readoperator.write
8) We rotated one operator token explicitly with full scopes
Using:
openclaw devices rotate --json --url ws://127.0.0.1:18889 --token <configured-token> --device <device-id> --role operator --scope operator.read --scope operator.write --scope operator.admin --scope operator.approvals --scope operator.pairing
Rotation succeeded and returned a new token with:
operator.adminoperator.approvalsoperator.pairingoperator.readoperator.write
9) We wrote that new token into the live config and verified readback
jq -r ".gateway.auth.token" /home/openclaw2/.openclaw/openclaw.json
now matches the newly rotated token.
10) The new token is valid when used explicitly
Using:
openclaw devices list --json --url ws://127.0.0.1:18889 --token <new-token>
The rotated token is accepted and the token record for that device shows:
operator.readoperator.write- plus prior operator scopes
The remaining mismatch
Despite all of the above, openclaw status --all still shows:
Gateway unreachable (missing scope: operator.read)Gateway self unknownGateway service systemd installed · disabled · unknown
At the same time:
openclaw health --jsonis OK- direct
openclaw gateway status --json --url ... --token <new-token>shows"rpc": { "ok": true } systemctl --user statusshows the service is active and enabled
Question
What is the most likely explanation for this remaining mismatch in 2026.3.13?
Specifically, is openclaw status --all likely using:
- a different cached/default token path than the one in
openclaw.json - a different device identity than the explicit token-based RPC commands
- a separate service-runtime probe path that is known to misreport under some sudo / user-bus contexts
- a known bug in
status --all/doctorwhere status output can be stale or misleading even whenhealthand direct RPC are OK
Most useful next ask
What exact command should we use to prove which auth source and probe path openclaw status --all is actually using, so we can separate:
- real auth failure
- service-runtime inspection bug
- cached/default-token mismatch
That missing scope: operator.read warning is about your device-scoped operator token, not about “is the gateway up.” So it’s totally consistent to have:
- RPC/health OK (especially when you pass the shared gateway
--token …) - but
status --allcan’t call some “operator read” endpoints (liketools.catalog) using the cached device token it has for your CLI/UI device identity.
Safest fix is to rotate the operator device token and explicitly grant the scopes.
Next step (you’ll need the deviceId first):
openclaw devices list --json --url ws://127.0.0.1:18889 --token <known-good-token>
Then rotate with scopes (this is the actual fix; paste your deviceId into it):
openclaw devices rotate --device <deviceId> --role operator \
--scope operator.read --scope operator.write \
--url ws://127.0.0.1:18889 --token <known-good-token>
Docs: https://docs.openclaw.ai/cli/devices and (what scopes mean) https://docs.openclaw.ai/gateway/protocol
If you paste the devices list --json output (redact tokens), I can tell you which deviceId to rotate (CLI vs Control UI vs node).
Nice — the operator.read part is solved the “right” way (device token scopes), and your RPC/health proofs look solid.
For the remaining gateway status → runtime.status: "unknown" with:
systemctl --user unavailable: Failed to connect to bus: Permission denied- even though
systemctl --user status openclaw-gateway.serviceworks
…the most common gotcha on Ubuntu is: you’re running openclaw … from a sudo-influenced environment where SUDO_USER is set, and OpenClaw’s systemd probe path will try a systemctl --machine <user>@ --user … fallback. That can fail with “permission denied / transport endpoint not connected” even when plain systemctl --user … works.
Safest next check (no config changes): run the same command but explicitly without SUDO_USER in the environment (and ideally as the actual service user):
env -u SUDO_USER openclaw gateway status --json
If that makes runtime.status flip to running, you’ve confirmed it’s just the service-runtime probe heuristic being tripped by the sudo env, not an actual service problem.
If it still reports unknown, paste the exact runtime.detail blob from openclaw gateway status --json and whether you’re invoking OpenClaw via sudo, sudo -u openclaw2, or as a normal login shell user — the fix is usually just “run OpenClaw CLI as the same user that owns the systemd user service (no sudo wrapper), or strip sudo env vars for that call.”
If you can reproduce reliably, this is also worth filing as a bug (service probe should not pick the wrong systemd user scope just because SUDO_USER exists).
OpenClaw 2026.3.13 — status --all reports missing operator.read despite healthy gateway and valid tokens
Environment state (verified)
Gateway:
- Service active and running (
systemctl --user status openclaw-gateway.service) - RPC reachable (
openclaw gateway status --json) rpc.ok = trueservice.runtime.status = running
Overall health:
openclaw health --json→"ok": true
Identity state (verified)
Local CLI identity:
- Located in
~/.openclaw/identity/ - Client mode:
cli - Token scopes include:
- operator.admin
- operator.approvals
- operator.pairing
- operator.read
- operator.write
Paired devices:
- Multiple
openclaw-control-ui(webchat) devices exist in:
~/.openclaw/devices/paired.json
Their rotated tokens correctly include:
- operator.admin
- operator.approvals
- operator.pairing
- operator.read
- operator.write
Critical mismatch discovered
Inside paired.json:
CLI device:
- Top-level scopes include read/write → correct
Webchat devices:
- Top-level scopes still show only:
["operator.admin","operator.approvals","operator.pairing"]
But nested token scopes show:
["operator.admin","operator.approvals","operator.pairing","operator.read","operator.write"]
So metadata appears inconsistent:
- token scopes = correct
- device top-level scopes = stale
Observed failure
openclaw status --all reports:
Gateway unreachable (missing scope: operator.read)
Gateway logs show real WS calls failing:
status
system-presence
config.get
Error:
missing scope: operator.read
Key technical question
Does openclaw status --all check:
- paired device top-level scopes
instead of - active token scopes?
Because if yes, stale paired metadata could explain the false failure.
What is the correct fix?
Safest resolution path:
1 — Remove and re-pair stale webchat devices
2 — Force metadata refresh of paired devices
3 — Rotate identity again
4 — Treat as cosmetic status bug if runtime is confirmed healthy
Looking for the intended corrective action.
OpenClaw 2026.3.13 — verified why the non-loopback probe path did not work here
Thanks — I checked this locally step by step.
Proven on the installed CLI:
openclaw status --helpdoes not support--urlor--token- root
openclaw --helpalso does not expose--url/--token - the correct command family for explicit remote probing here is:
openclaw gateway probe --url ... --token ...
Proven runtime/config state:
- active config has:
gateway.port = 18889gateway.mode = localgateway.auth.mode = tokengateway.auth.allowTailscale = true
gateway.bindis not explicitly set in config- local code inspection shows unset
gateway.binddefaults toloopback - live listener confirms:
127.0.0.1:18889only- not listening on tailnet IP
Proven probe result:
- probing
ws://100.x.x.x:18889with insecure-private-WS allowed returns:ECONNREFUSED
- so the gateway is not reachable on raw non-loopback WS in the current service shape
Conclusion:
- your idea was directionally useful, but on this install it maps to
gateway probe, notstatus --all - and it still cannot work yet because the gateway is effectively loopback-bound
Question:
- for
2026.3.13, what is the safest minimal way to test whether the remainingstatus --all/operator.readissue is just localhost identity/probe behavior versus stale paired-device metadata, without widening the live gateway bind? - preferred options to compare:
- temporary foreground gateway with
--bind tailnet - config/service change to
gateway.bind=tailnet - another read-only diagnostic path that preserves loopback bind
- temporary foreground gateway with
You don’t need to widen the bind at all here. The clean “A/B test” is: make status --all probe the gateway through your existing Tailscale Serve HTTPS endpoint (wss://) instead of loopback ws://, for one command, and see if the operator.read error disappears.
Why this is the safest discriminator:
- In 2026.3.13 the
statuscommand’s gateway probe path uses a lightweight “probe” client. - That probe behaves differently depending on whether the target host is loopback vs non-loopback (notably around device identity / scope handling).
- Using Tailscale Serve changes only the probe transport, not your gateway listener exposure (gateway stays
127.0.0.1).
Safest minimal test (single command, no bind change)
Run:
OPENCLAW_GATEWAY_URL="wss://arthur-a6.tail3c61b.ts.net" openclaw status --all
Interpretation:
- If the
missing scope: operator.readline goes away (or the gateway becomes “reachable” in status), the problem is status’s localhost probe behavior (not real gateway auth, not device metadata). - If it does not change, then you’re likely dealing with stale/incorrect paired-device scope metadata being used by the client (or another status/daemon probe bug), and we can focus there next.
Notes:
statusdoesn’t take--url/--token, but it does honorOPENCLAW_GATEWAY_URL(you can see this in the “Gateway target / Source” verbose details).- This keeps the gateway loopback-bound; you’re just asking the CLI to reach it via your Serve proxy.
If you paste the delta between:
- normal
openclaw status --all - and the one-liner above
…I can tell you which of your numbered hypotheses (1–4) it conclusively confirms.
OpenClaw 2026.3.13 — verified OPENCLAW_GATEWAY_URL override, but A/B test is still blocked before operator.read
Thanks — I checked this locally.
Proven now:
openclaw status --alldoes honorOPENCLAW_GATEWAY_URL- the override shows up in
status --allas:Gateway target: ...Source: env OPENCLAW_GATEWAY_URL
Current setup/proof:
- gateway is healthy locally
- live listener is loopback-only (
127.0.0.1:18889) - Tailscale Serve is active as HTTP-only proxy to the local gateway port
- no HTTPS/WSS Serve endpoint is available on this account
- MagicDNS is disabled tailnet-wide, so the
.ts.nethostname does not resolve locally from shell
A/B test results:
OPENCLAW_GATEWAY_URL="http://<tsnet-hostname>"with insecure-private-WS allowed:status --alluses the env override- fails with:
getaddrinfo ENOTFOUND <tsnet-hostname>
OPENCLAW_GATEWAY_URL="http://<tailnet-ip>"with insecure-private-WS allowed:status --alluses the env override- fails with:
Unexpected server response: 404
Interpretation:
- we successfully moved
status --alloff local loopback target selection - but the test still does not reach the
operator.readlayer - hostname form dies at DNS
- tailnet-IP form reaches something remote enough to stop being ENOTFOUND, but the WS/HTTP path comes back
404
Question:
- for
2026.3.13, what exact URL form/path shouldOPENCLAW_GATEWAY_URLuse when targeting a Tailscale Serve HTTP endpoint? - is
status --allcompatible with HTTP-only Serve at all, or does this path require truewss:/// HTTPS-backed Serve? - if a path suffix is required for the gateway WS probe, what is the exact expected path?
openclaw status --all is doing a WebSocket probe. So OPENCLAW_GATEWAY_URL needs to point at the Gateway WS endpoint (ws/wss), not an HTTP URL.
1) Correct URL scheme + path
- Use
ws://…(orwss://…), nothttp://…. - No path suffix is expected for the Gateway WS probe (it’s just the WS server on that origin, i.e.
/).
So the shape should be:
-
HTTP-only Serve (plaintext on tailnet):
OPENCLAW_GATEWAY_URL="ws://<tailnet-ip>[:<serve-port>]"
(and you already know you needOPENCLAW_ALLOW_INSECURE_PRIVATE_WS=1for non-loopback plaintext) -
HTTPS Serve (preferred):
OPENCLAW_GATEWAY_URL="wss://<magicdns-or-tsnet-hostname>"
2) Is status --all compatible with HTTP-only Serve?
Yes only if your Serve endpoint actually forwards WebSocket upgrades to the gateway (it usually does when it’s proxying an HTTP backend). If you truly have no HTTPS/WSS available, the plaintext ws://<tailnet-ip> path is the only way to “move off loopback” without changing gateway.bind, but it’s inherently a break-glass mode (still tailnet-only, but no TLS).
3) Why you’re seeing Unexpected server response: 404
That 404 is almost certainly because the WS upgrade request is hitting the wrong Tailscale Serve listener (wrong port / wrong Serve target), not because OpenClaw needs a special WS path.
The external port you must use is whatever Tailscale Serve is listening on, which is often 80 (HTTP) / 443 (HTTPS) — not your backend port 18889.
To answer “what exact host:port should I use” precisely, I need one thing:
Paste the output of:
tailscale serve status
Specifically the lines that show what it’s serving on (ports / any path mounts) and what it forwards to. With that, I can tell you the exact OPENCLAW_GATEWAY_URL=ws://… form that should work in your current “HTTP-only Serve + loopback-bound gateway” setup.
OpenClaw 2026.3.13 — status --all gateway override credential behavior
Testing gateway status behavior when using OPENCLAW_GATEWAY_URL.
What is proven locally:
- Gateway service is running normally.
status --allworks normally on loopback.- When using OPENCLAW_GATEWAY_URL to force non-loopback probe:
- Gateway becomes reachable.
- But status fails with:
gateway url override requires explicit credentials
Observed behavior:
- Error suggests:
Fix: pass --token or --password (or gatewayToken in tools).
Question:
For 2026.3.13, what is the correct minimal way to satisfy the credential requirement when using OPENCLAW_GATEWAY_URL with status --all?
Specifically:
- Should existing gateway token in config be enough?
- Or is an additional explicit credential required for override mode?
- If explicit is required, what is the intended method for
status(since it has no --token flag)?
That behavior is intentional in 2026.3.13 and matches the “URL override safety” rules:
- When you override the gateway URL via
OPENCLAW_GATEWAY_URL, OpenClaw treats that as a potentially remote target and will not implicitly reuse the token/password fromopenclaw.json. - For an env URL override, it will only proceed if it can also find credentials in env (
OPENCLAW_GATEWAY_TOKEN/OPENCLAW_GATEWAY_PASSWORD). Otherwise you get:
gateway url override requires explicit credentials
Since status has no --token, the intended minimal way is:
Minimal one-shot invocation
Set both env vars for the same command:
OPENCLAW_GATEWAY_URL="ws://<tailnet-ip>:<serve-port>" \
OPENCLAW_GATEWAY_TOKEN="$(jq -r '.gateway.auth.token' ~/.openclaw/openclaw.json)" \
openclaw status --all
(And because that’s non-loopback plaintext ws://, you’ll also need OPENCLAW_ALLOW_INSECURE_PRIVATE_WS=1 in that same invocation unless you’re using wss://.)
Answering your bullets
- Should existing gateway token in config be enough? Not when you use
OPENCLAW_GATEWAY_URLoverride. (By design.) - Is an additional explicit credential required? Yes — explicit in the environment (
OPENCLAW_GATEWAY_TOKEN/PASSWORD) for env override. - Intended method for
statuswith no--token? Provide the credential via env vars, or useopenclaw gateway probe --url … --token …instead ofstatus.
This is the same policy described in the Remote Access docs (URL override safety): https://docs.openclaw.ai/gateway/remote
OpenClaw 2026.3.13 — decisive A/B result on status --all loopback vs non-loopback probe
Local proof is now decisive.
A/B result:
- Default
openclaw status --allon local loopback:- gateway target = local loopback
- fails with:
missing scope: operator.read
openclaw status --allwith non-loopback gateway URL override plus explicit env credential:- gateway reachable
- no channel issues
- no
operator.readfailure
So this is now proven not to be:
- real gateway down
- broken service/runtime
- bad main gateway token in config
- general auth failure
Strongest conclusion:
- in 2026.3.13, the default loopback
status --allprobe path is producing a false negative / auth-scope mismatch - the same runtime works when forced through non-loopback override with explicit credentials
Question:
- Is this a known localhost probe bug/regression in
status --allfor 2026.3.13? - Safest recommendation from here:
- ignore as status-only false negative
- use a known workaround
- upgrade/downgrade target version