mirror of https://github.com/openclaw/openclaw.git
249 lines
9.6 KiB
Markdown
249 lines
9.6 KiB
Markdown
---
|
||
summary: "Remote access using SSH tunnels (Gateway WS) and tailnets"
|
||
read_when:
|
||
- Running or troubleshooting remote gateway setups
|
||
title: "Remote Access"
|
||
---
|
||
|
||
# Remote access (SSH, tunnels, and tailnets)
|
||
|
||
This repo supports “remote over SSH” by keeping a single Gateway (the master) running on a dedicated host (desktop/server) and connecting clients to it.
|
||
|
||
- For **operators (you / the macOS app)**: SSH tunneling is the universal fallback.
|
||
- For **nodes (iOS/Android and future devices)**: connect to the Gateway **WebSocket** (LAN/tailnet or SSH tunnel as needed).
|
||
|
||
## The core idea
|
||
|
||
- The Gateway WebSocket binds to **loopback** on your configured port (defaults to 18789).
|
||
- For remote use, you forward that loopback port over SSH (or use a tailnet/VPN and tunnel less).
|
||
|
||
## Common VPN/tailnet setups (where the agent lives)
|
||
|
||
Think of the **Gateway host** as “where the agent lives.” It owns sessions, auth profiles, channels, and state.
|
||
Your laptop/desktop (and nodes) connect to that host.
|
||
|
||
### 1) Always-on Gateway in your tailnet (VPS or home server)
|
||
|
||
Run the Gateway on a persistent host and reach it via **Tailscale** or SSH.
|
||
|
||
- **Best UX:** keep `gateway.bind: "loopback"` and use **Tailscale Serve** for the Control UI.
|
||
- **Fallback:** keep loopback + SSH tunnel from any machine that needs access.
|
||
- **Examples:** [exe.dev](/install/exe-dev) (easy VM) or [Hetzner](/install/hetzner) (production VPS).
|
||
|
||
This is ideal when your laptop sleeps often but you want the agent always-on.
|
||
|
||
### 2) Home desktop runs the Gateway, laptop is remote control
|
||
|
||
The laptop does **not** run the agent. It connects remotely:
|
||
|
||
- Use the macOS app’s **Remote over SSH** mode (Settings → General → “OpenClaw runs”).
|
||
- The app opens and manages the tunnel, so WebChat + health checks “just work.”
|
||
|
||
Runbook: [macOS remote access](/platforms/mac/remote).
|
||
|
||
### 3) Laptop runs the Gateway, remote access from other machines
|
||
|
||
Keep the Gateway local but expose it safely:
|
||
|
||
- SSH tunnel to the laptop from other machines, or
|
||
- Tailscale Serve the Control UI and keep the Gateway loopback-only.
|
||
|
||
Guide: [Tailscale](/gateway/tailscale) and [Web overview](/web).
|
||
|
||
## Command flow (what runs where)
|
||
|
||
One gateway service owns state + channels. Nodes are peripherals.
|
||
|
||
Flow example (Telegram → node):
|
||
|
||
- Telegram message arrives at the **Gateway**.
|
||
- Gateway runs the **agent** and decides whether to call a node tool.
|
||
- Gateway calls the **node** over the Gateway WebSocket (`node.*` RPC).
|
||
- Node returns the result; Gateway replies back out to Telegram.
|
||
|
||
Notes:
|
||
|
||
- **Nodes do not run the gateway service.** Only one gateway should run per host unless you intentionally run isolated profiles (see [Multiple gateways](/gateway/multiple-gateways)).
|
||
- macOS app “node mode” is just a node client over the Gateway WebSocket.
|
||
|
||
## SSH tunnel (CLI + tools)
|
||
|
||
Create a local tunnel to the remote Gateway WS:
|
||
|
||
```bash
|
||
ssh -N -L 18789:127.0.0.1:18789 user@host
|
||
```
|
||
|
||
With the tunnel up:
|
||
|
||
- `openclaw health` and `openclaw status --deep` now reach the remote gateway via `ws://127.0.0.1:18789`.
|
||
- `openclaw gateway {status,health,send,agent,call}` can also target the forwarded URL via `--url` when needed.
|
||
|
||
Note: replace `18789` with your configured `gateway.port` (or `--port`/`OPENCLAW_GATEWAY_PORT`).
|
||
Note: when you pass `--url`, the CLI does not fall back to config or environment credentials.
|
||
Include `--token` or `--password` explicitly. Missing explicit credentials is an error.
|
||
|
||
## CLI remote defaults
|
||
|
||
You can persist a remote target so CLI commands use it by default:
|
||
|
||
```json5
|
||
{
|
||
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.
|
||
|
||
## Credential precedence
|
||
|
||
Gateway credential resolution follows one shared contract across call/probe/status paths and Discord exec-approval monitoring. Node-host uses the same base contract with one local-mode exception (it intentionally ignores `gateway.remote.*`):
|
||
|
||
- Explicit credentials (`--token`, `--password`, or tool `gatewayToken`) always win on call paths that accept explicit auth.
|
||
- URL override safety:
|
||
- CLI URL overrides (`--url`) never reuse implicit config/env credentials.
|
||
- Env URL overrides (`OPENCLAW_GATEWAY_URL`) may use env credentials only (`OPENCLAW_GATEWAY_TOKEN` / `OPENCLAW_GATEWAY_PASSWORD`).
|
||
- Local mode defaults:
|
||
- token: `OPENCLAW_GATEWAY_TOKEN` -> `gateway.auth.token` -> `gateway.remote.token` (remote fallback applies only when local auth token input is unset)
|
||
- password: `OPENCLAW_GATEWAY_PASSWORD` -> `gateway.auth.password` -> `gateway.remote.password` (remote fallback applies only when local auth password input is unset)
|
||
- 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 ignored.
|
||
- Remote probe/status token checks are strict by default: they use `gateway.remote.token` only (no local token fallback) when targeting remote mode.
|
||
- Gateway env overrides use `OPENCLAW_GATEWAY_*` only.
|
||
|
||
## Chat UI over SSH
|
||
|
||
WebChat no longer uses a separate HTTP port. The SwiftUI chat UI connects directly to the Gateway WebSocket.
|
||
|
||
- Forward `18789` over SSH (see above), then connect clients to `ws://127.0.0.1:18789`.
|
||
- On macOS, prefer the app’s “Remote over SSH” mode, which manages the tunnel automatically.
|
||
|
||
## macOS app "Remote over SSH"
|
||
|
||
The macOS menu bar app can drive the same setup end-to-end (remote status checks, WebChat, and Voice Wake forwarding).
|
||
|
||
Runbook: [macOS remote access](/platforms/mac/remote).
|
||
|
||
## Security rules (remote/VPN)
|
||
|
||
Short version: **keep the Gateway loopback-only** unless you’re sure you need a bind.
|
||
|
||
- **Loopback + SSH/Tailscale Serve** is the safest default (no public exposure).
|
||
- Plaintext `ws://` is loopback-only by default. For trusted private networks,
|
||
set `OPENCLAW_ALLOW_INSECURE_PRIVATE_WS=1` on the client process as break-glass.
|
||
- **Non-loopback binds** (`lan`/`tailnet`/`custom`, or `auto` when loopback is unavailable) must use auth tokens/passwords.
|
||
- `gateway.remote.token` / `.password` are client credential sources. They do **not** configure server auth by themselves.
|
||
- Local call paths can use `gateway.remote.*` as fallback only when `gateway.auth.*` is unset.
|
||
- If `gateway.auth.token` / `gateway.auth.password` is explicitly configured via SecretRef and unresolved, resolution fails closed (no remote fallback masking).
|
||
- `gateway.remote.tlsFingerprint` pins the remote TLS cert when using `wss://`.
|
||
- **Tailscale Serve** can authenticate Control UI/WebSocket traffic via identity
|
||
headers when `gateway.auth.allowTailscale: true`; HTTP API endpoints still
|
||
require token/password auth. This tokenless flow assumes the gateway host is
|
||
trusted. Set it to `false` if you want tokens/passwords everywhere.
|
||
- Treat browser control like operator access: tailnet-only + deliberate node pairing.
|
||
|
||
Deep dive: [Security](/gateway/security).
|
||
|
||
### macOS: persistent SSH tunnel via LaunchAgent
|
||
|
||
For macOS clients connecting to a remote gateway, the easiest persistent setup uses an SSH `LocalForward` config entry plus a LaunchAgent to keep the tunnel alive across reboots and crashes.
|
||
|
||
#### Step 1: add SSH config
|
||
|
||
Edit `~/.ssh/config`:
|
||
|
||
```ssh
|
||
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)
|
||
|
||
```bash
|
||
ssh-copy-id -i ~/.ssh/id_rsa <REMOTE_USER>@<REMOTE_IP>
|
||
```
|
||
|
||
#### Step 3: configure the gateway token
|
||
|
||
Store the token in config so it persists across restarts:
|
||
|
||
```bash
|
||
openclaw config set gateway.remote.token "<your-token>"
|
||
```
|
||
|
||
#### Step 4: create the LaunchAgent
|
||
|
||
Save this as `~/Library/LaunchAgents/ai.openclaw.ssh-tunnel.plist`:
|
||
|
||
```xml
|
||
<?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
|
||
|
||
```bash
|
||
launchctl bootstrap gui/$UID ~/Library/LaunchAgents/ai.openclaw.ssh-tunnel.plist
|
||
```
|
||
|
||
The tunnel will start automatically at login, restart on crash, and keep the forwarded port live.
|
||
|
||
Note: if you have a leftover `com.openclaw.ssh-tunnel` LaunchAgent from an older setup, unload and delete it.
|
||
|
||
#### Troubleshooting
|
||
|
||
Check if the tunnel is running:
|
||
|
||
```bash
|
||
ps aux | grep "ssh -N remote-gateway" | grep -v grep
|
||
lsof -i :18789
|
||
```
|
||
|
||
Restart the tunnel:
|
||
|
||
```bash
|
||
launchctl kickstart -k gui/$UID/ai.openclaw.ssh-tunnel
|
||
```
|
||
|
||
Stop the tunnel:
|
||
|
||
```bash
|
||
launchctl bootout gui/$UID/ai.openclaw.ssh-tunnel
|
||
```
|
||
|
||
| Config entry | What it does |
|
||
| ------------------------------------ | ------------------------------------------------------------ |
|
||
| `LocalForward 18789 127.0.0.1:18789` | Forwards local port 18789 to remote port 18789 |
|
||
| `ssh -N` | SSH without executing remote commands (port-forwarding only) |
|
||
| `KeepAlive` | Automatically restarts the tunnel if it crashes |
|
||
| `RunAtLoad` | Starts the tunnel when the LaunchAgent loads at login |
|