Remote Access to Gateway via WebSocket, SSH Tunnels, and Tailnets
Learn how operators and nodes can remotely access the OpenClaw Gateway using WebSocket connections, SSH tunnels, or Tailscale. This page covers topology options and best practices for secure remote access.
Read this when
- Running or troubleshooting remote gateway setups
OpenClaw designates one Gateway instance as the master on a given host, and all clients connect to that single instance. The Gateway manages sessions, authentication profiles, channels, and overall state; everything else functions as a client.
- Operators (yourself or the macOS application): a direct LAN or Tailnet WebSocket connection is the simplest approach when the Gateway is accessible; SSH tunneling serves as a universal fallback option.
- Nodes (iOS, Android, and other devices): connect to the Gateway through its WebSocket interface, either over LAN/tailnet or via an SSH tunnel.
The core idea
By default, the Gateway WebSocket binds to loopback on port 18789 (gateway.port). To enable remote access, either expose it through Tailscale Serve or a trusted LAN/Tailnet bind, or forward the loopback port over SSH.
Topology options
| Setup | Where the Gateway runs | Best for |
|---|---|---|
| Always-on Gateway in your tailnet | Persistent host (VPS or home server), reached via Tailscale or SSH | Laptops that sleep often but need the agent always-on. See exe.dev (easy VM) or Hetzner (production VPS). |
| Home desktop | Desktop; laptop connects remotely via the macOS app's remote mode (Settings → Connection → OpenClaw runs) | Keeping the agent on hardware that stays powered on. Runbook: macOS remote access. |
| Laptop | Laptop, exposed safely via SSH tunnel or Tailscale Serve (keep gateway.bind: "loopback") | Single-machine setups. See Tailscale and Web. |
For the always-on and laptop configurations, the recommended approach is to keep gateway.bind: "loopback" and expose the Control UI through Tailscale Serve, or use a trusted LAN/Tailnet bind with gateway.remote.transport: "direct". The SSH tunnel works as a fallback from any machine.
Command flow (what runs where)
A single Gateway owns all state and channels; nodes are peripheral devices. Example flow (a Telegram message routed to a node tool):
- Telegram message arrives at the Gateway.
- The Gateway runs the agent, which determines whether to invoke a node tool.
- The Gateway contacts the node over the Gateway WebSocket (
node.invokeRPC). - The node sends back its result, and the Gateway responds to Telegram.
Nodes do not run the Gateway service. Only one Gateway should run per host unless you intentionally operate isolated profiles (see Multiple gateways). The macOS app's "node mode" is simply a node client communicating over the Gateway WebSocket.
SSH tunnel (CLI + tools)
ssh -N -L 18789:127.0.0.1:18789 user@gateway-host
Once the tunnel is active, openclaw health and openclaw status --deep can reach the remote Gateway through ws://127.0.0.1:18789. openclaw gateway status, openclaw gateway health, openclaw gateway probe, and openclaw gateway call can also address a forwarded URL using --url.
Note
Replace
18789with your configuredgateway.port(or--port/OPENCLAW_GATEWAY_PORT).
Warning
--urldoes not fall back to configuration or environment credentials. You must pass--tokenor--passwordexplicitly; without them the client sends no credentials and the connection will fail if the target Gateway requires authentication.
CLI remote defaults
To make a remote target persistent so CLI commands use it by default:
{
gateway: {
mode: "remote",
remote: {
url: "ws://127.0.0.1:18789",
token: "your-token",
},
},
}
When the Gateway is loopback-only, keep the URL at ws://127.0.0.1:18789 and open the SSH tunnel first. In the macOS app's SSH-tunnel transport, the discovered Gateway hostname goes into gateway.remote.sshTarget (user@host or user@host:port); gateway.remote.url remains the local tunnel URL. If the remote port differs from the local one, set gateway.remote.remotePort.
Host-key verification is strict by default (gateway.remote.sshHostKeyPolicy: "strict"). Change it to "openssh" to rely on your effective OpenSSH configuration instead; review both your user and system SSH settings before enabling this.
For a Gateway already reachable on a trusted LAN or Tailnet, use direct mode:
{
gateway: {
mode: "remote",
remote: {
transport: "direct",
url: "ws://192.168.0.202:18789",
token: "your-token",
},
},
}
Credential precedence
Gateway credential resolution follows a single shared contract across call, probe, and status paths, as well as Discord exec-approval monitoring. Node-host uses the same contract with one local-mode exception: it ignores gateway.remote.*.
-
On call paths that accept explicit authentication, explicit credentials (
--token,--password, or a tool'sgatewayToken) always take precedence. -
URL override safety:
- CLI
--urlnever falls back to implicit credentials from configuration or environment variables. - Env
OPENCLAW_GATEWAY_URLcan only use environment credentials (OPENCLAW_GATEWAY_TOKEN/OPENCLAW_GATEWAY_PASSWORD).
- CLI
-
Local mode defaults:
- token:
OPENCLAW_GATEWAY_TOKEN->gateway.auth.token->gateway.remote.token(remote fallback happens only when the local token is not set) - password:
OPENCLAW_GATEWAY_PASSWORD->gateway.auth.password->gateway.remote.password(remote fallback happens only when the local password is not set)
- token:
-
Remote mode defaults:
- token:
gateway.remote.token->OPENCLAW_GATEWAY_TOKEN->gateway.auth.token - password:
OPENCLAW_GATEWAY_PASSWORD->gateway.remote.password->gateway.auth.password
- token:
-
Node-host local-mode exception:
gateway.remote.token/gateway.remote.passwordare disregarded. -
Remote probe/status token checks are strict by default: when targeting remote mode, they use
gateway.remote.tokenexclusively (no local token fallback). -
Gateway environment overrides use only
OPENCLAW_GATEWAY_*.
Chat UI remote access
WebChat does not use a separate HTTP port; the SwiftUI chat UI connects directly to the Gateway WebSocket.
- Forward
18789over SSH (see above), then point clients tows://127.0.0.1:18789. - For LAN/Tailnet direct mode, point clients to the configured private
ws://or securewss://URL. - On macOS, the app's remote mode handles transport selection automatically.
macOS app remote mode
The macOS menu bar app drives the entire setup end-to-end: remote status checks, WebChat, and Voice Wake forwarding. Runbook: macOS remote access.
Security rules (remote/VPN)
Keep the Gateway loopback-only unless you are certain you need a bind.
- Loopback + SSH/Tailscale Serve is the safest default (no public exposure).
- Plaintext
ws://is accepted for loopback, private/LAN (RFC 1918), link-local, CGNAT,.local, and.ts.nethosts. Public remote hosts must usewss://. - Non-loopback binds (
lan/tailnet/custom, orautowhen loopback is unavailable) must use Gateway auth: token, password, or an identity-aware reverse proxy withgateway.auth.mode: "trusted-proxy". gateway.remote.token/.passwordare client credential sources; they do not configure server authentication by themselves.- Local call paths can use
gateway.remote.*as a fallback only whengateway.auth.*is not set. - If
gateway.auth.token/gateway.auth.passwordis explicitly configured via SecretRef and cannot be resolved, resolution fails closed (no remote fallback masking). gateway.remote.tlsFingerprintpins the remote TLS cert forwss://, including both operator/control traffic and the companion node in macOS direct mode. Without a stored pin, macOS pins on first use only after normal system trust passes; self-signed or private-CA Gateways need an explicit fingerprint or Remote over SSH.- Tailscale Serve can authenticate Control UI/WebSocket traffic via identity headers when
gateway.auth.allowTailscale: true. HTTP API endpoints do not use that header auth and instead follow the Gateway's normal HTTP auth mode. This tokenless flow assumes the Gateway host is trusted; set it tofalsefor shared-secret auth everywhere. - Trusted-proxy auth expects a non-loopback identity-aware proxy by default. Same-host loopback reverse proxies require explicit
gateway.auth.trustedProxy.allowLoopback = true. - Treat browser control like operator access: tailnet-only plus deliberate node pairing.
Deep dive: Security.
macOS: persistent SSH tunnel via LaunchAgent
For macOS clients, the simplest persistent setup uses an SSH LocalForward config entry plus a LaunchAgent that keeps the tunnel alive across reboots and crashes.
Step 1: add SSH config
Edit ~/.ssh/config:
Host remote-gateway
HostName <REMOTE_IP>
User <REMOTE_USER>
LocalForward 18789 127.0.0.1:18789
IdentityFile ~/.ssh/id_rsa
Replace <REMOTE_IP> and <REMOTE_USER> with your values.
Step 2: copy SSH key (one-time)
ssh-copy-id -i ~/.ssh/id_rsa <REMOTE_USER>@<REMOTE_IP>
Step 3: configure the gateway token
openclaw config set gateway.remote.token "<your-token>"
When the remote Gateway relies on password authentication, switch to gateway.remote.password. Although OPENCLAW_GATEWAY_TOKEN works as a shell-level override, the recommended configuration for a permanent remote client uses gateway.remote.token combined with gateway.remote.password.
Step 4: create the LaunchAgent
Store the configuration as ~/Library/LaunchAgents/ai.openclaw.ssh-tunnel.plist:
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>Label</key>
<string>ai.openclaw.ssh-tunnel</string>
<key>ProgramArguments</key>
<array>
<string>/usr/bin/ssh</string>
<string>-N</string>
<string>remote-gateway</string>
</array>
<key>KeepAlive</key>
<true/>
<key>RunAtLoad</key>
<true/>
</dict>
</plist>
Step 5: load the LaunchAgent
launchctl bootstrap gui/$UID ~/Library/LaunchAgents/ai.openclaw.ssh-tunnel.plist
The tunnel activates automatically upon login, recovers after a crash, and maintains the forwarded port without interruption.
Note
Remove any old
com.openclaw.ssh-tunnelLaunchAgent from a prior installation by unloading and deleting it.
Troubleshooting
# Check if the tunnel is running
ps aux | grep "ssh -N remote-gateway" | grep -v grep
lsof -i :18789
# Restart the tunnel
launchctl kickstart -k gui/$UID/ai.openclaw.ssh-tunnel
# Stop the tunnel
launchctl bootout gui/$UID/ai.openclaw.ssh-tunnel
| Config entry | What it does |
|---|---|
LocalForward 18789 127.0.0.1:18789 | Routes local port 18789 to the same port on the remote side |
ssh -N | Runs SSH in a mode that forwards ports without running commands |
KeepAlive | Automatically re-establishes the tunnel after an unexpected stop |
RunAtLoad | Launches the tunnel as soon as the LaunchAgent loads at login |