---
summary: "Browser-based control UI for the Gateway (chat, nodes, config)"
read_when:
- You want to operate the Gateway from a browser
- You want Tailnet access without SSH tunnels
title: "Control UI"
---
The Control UI is a small **Vite + Lit** single-page app served by the Gateway:
- default: `http://<host>:18789/`
- optional prefix: set `gateway.controlUi.basePath` (e.g. `/openclaw`)
It speaks **directly to the Gateway WebSocket** on the same port.
## Quick open (local)
If the Gateway is running on the same computer, open:
Keep the Gateway on loopback and let Tailscale Serve proxy it with HTTPS:
```bash
openclaw gateway --tailscale serve
```
Open:
- `https://<magicdns>/` (or your configured `gateway.controlUi.basePath`)
By default, Control UI/WebSocket Serve requests can authenticate via Tailscale identity headers
(`tailscale-user-login`) when `gateway.auth.allowTailscale` is `true`. OpenClaw
verifies the identity by resolving the `x-forwarded-for` address with
`tailscale whois` and matching it to the header, and only accepts these when the
request hits loopback with Tailscale’s `x-forwarded-*` headers. Set
`gateway.auth.allowTailscale: false` if you want to require explicit shared-secret
credentials even for Serve traffic. Then use `gateway.auth.mode: "token"` or
`"password"`.
For that async Serve identity path, failed auth attempts for the same client IP
and auth scope are serialized before rate-limit writes. Concurrent bad retries
from the same browser can therefore show `retry later` on the second request
instead of two plain mismatches racing in parallel.
Tokenless Serve auth assumes the gateway host is trusted. If untrusted local
code may run on that host, require token/password auth.
- `http://<tailscale-ip>:18789/` (or your configured `gateway.controlUi.basePath`)
Paste the matching shared secret into the UI settings (sent as
`connect.params.auth.token` or `connect.params.auth.password`).
## Insecure HTTP
If you open the dashboard over plain HTTP (`http://<lan-ip>` or `http://<tailscale-ip>`),
the browser runs in a **non-secure context** and blocks WebCrypto. By default,
OpenClaw **blocks** Control UI connections without device identity.
Documented exceptions:
- localhost-only insecure HTTP compatibility with `gateway.controlUi.allowInsecureAuth=true`
- successful operator Control UI auth through `gateway.auth.mode: "trusted-proxy"`
- break-glass `gateway.controlUi.dangerouslyDisableDeviceAuth=true`
**Recommended fix:** use HTTPS (Tailscale Serve) or open the UI locally:
- `gatewayUrl` is stored in localStorage after load and removed from the URL.
- `token` should be passed via the URL fragment (`#token=...`) whenever possible. Fragments are not sent to the server, which avoids request-log and Referer leakage. Legacy `?token=` query params are still imported once for compatibility, but only as a fallback, and are stripped immediately after bootstrap.
- `password` is kept in memory only.
- When `gatewayUrl` is set, the UI does not fall back to config or environment credentials.
Provide `token` (or `password`) explicitly. Missing explicit credentials is an error.
- Use `wss://` when the Gateway is behind TLS (Tailscale Serve, HTTPS proxy, etc.).
- `gatewayUrl` is only accepted in a top-level window (not embedded) to prevent clickjacking.
- Non-loopback Control UI deployments must set `gateway.controlUi.allowedOrigins`
explicitly (full origins). This includes remote dev setups.
- Do not use `gateway.controlUi.allowedOrigins: ["*"]` except for tightly controlled
local testing. It means allow any browser origin, not “match whatever host I am
using.”
- `gateway.controlUi.dangerouslyAllowHostHeaderOriginFallback=true` enables
Host-header origin fallback mode, but it is a dangerous security mode.