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

SetupWhere the Gateway runsBest for
Always-on Gateway in your tailnetPersistent host (VPS or home server), reached via Tailscale or SSHLaptops that sleep often but need the agent always-on. See exe.dev (easy VM) or Hetzner (production VPS).
Home desktopDesktop; 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.
LaptopLaptop, 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):

  1. Telegram message arrives at the Gateway.
  2. The Gateway runs the agent, which determines whether to invoke a node tool.
  3. The Gateway contacts the node over the Gateway WebSocket (node.invoke RPC).
  4. 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 18789 with your configured gateway.port (or --port / OPENCLAW_GATEWAY_PORT).

Warning

--url does not fall back to configuration or environment credentials. You must pass --token or --password explicitly; 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's gatewayToken) always take precedence.

  • URL override safety:

    • CLI --url never falls back to implicit credentials from configuration or environment variables.
    • Env OPENCLAW_GATEWAY_URL can only use environment credentials (OPENCLAW_GATEWAY_TOKEN / OPENCLAW_GATEWAY_PASSWORD).
  • 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)
  • Remote mode defaults:

    • token: gateway.remote.token -> OPENCLAW_GATEWAY_TOKEN -> gateway.auth.token
    • password: OPENCLAW_GATEWAY_PASSWORD -> gateway.remote.password -> gateway.auth.password
  • Node-host local-mode exception: gateway.remote.token / gateway.remote.password are disregarded.

  • Remote probe/status token checks are strict by default: when targeting remote mode, they use gateway.remote.token exclusively (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 18789 over SSH (see above), then point clients to ws://127.0.0.1:18789.
  • For LAN/Tailnet direct mode, point clients to the configured private ws:// or secure wss:// 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.net hosts. Public remote hosts must use wss://.
  • Non-loopback binds (lan/tailnet/custom, or auto when loopback is unavailable) must use Gateway auth: token, password, or an identity-aware reverse proxy with gateway.auth.mode: "trusted-proxy".
  • gateway.remote.token / .password are client credential sources; they do not configure server authentication by themselves.
  • Local call paths can use gateway.remote.* as a fallback only when gateway.auth.* is not set.
  • If gateway.auth.token / gateway.auth.password is explicitly configured via SecretRef and cannot be resolved, resolution fails closed (no remote fallback masking).
  • gateway.remote.tlsFingerprint pins the remote TLS cert for wss://, 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 to false for 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-tunnel LaunchAgent 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 entryWhat it does
LocalForward 18789 127.0.0.1:18789Routes local port 18789 to the same port on the remote side
ssh -NRuns SSH in a mode that forwards ports without running commands
KeepAliveAutomatically re-establishes the tunnel after an unexpected stop
RunAtLoadLaunches the tunnel as soon as the LaunchAgent loads at login