Skip to main content

Tailscale (Gateway dashboard)

Equabot can auto-configure Tailscale Serve (tailnet) or Funnel (public) for the Gateway dashboard and WebSocket port. This keeps the Gateway bound to loopback while Tailscale provides HTTPS, routing, and (for Serve) identity headers.

Modes

  • serve: Tailnet-only Serve via tailscale serve. The gateway stays on 127.0.0.1.
  • funnel: Public HTTPS via tailscale funnel. Equabot requires a shared password.
  • off: Default (no Tailscale automation).

Auth

Set gateway.auth.mode to control the handshake:
  • token (default when EQUABOT_GATEWAY_TOKEN is set)
  • password (shared secret via EQUABOT_GATEWAY_PASSWORD or config)
When tailscale.mode = "serve" and gateway.auth.allowTailscale is true, valid Serve proxy requests can authenticate via Tailscale identity headers (tailscale-user-login) without supplying a token/password. Equabot only treats a request as Serve when it arrives from loopback with Tailscale’s x-forwarded-for, x-forwarded-proto, and x-forwarded-host headers. To require explicit credentials, set gateway.auth.allowTailscale: false or force gateway.auth.mode: "password".

Device pairing (Serve)

Remote devices connecting via Tailscale Serve still require device pairing, the same as any non-local device. Tailscale identity headers handle authentication, but the device itself must be paired before it can establish a session. On the first connection from a new browser or device:
  1. The gateway creates a pairing request and closes the connection.
  2. Approve the request from the local Control UI (or any already-paired session).
  3. The remote device reconnects and succeeds.
Loopback connections (127.0.0.1 / ::1) auto-approve pairing silently. Serve connections do not, because the real client IP is the remote tailnet peer.

Config examples

Tailnet-only (Serve)

{
  gateway: {
    bind: "loopback",
    tailscale: { mode: "serve" },
    trustedProxies: ["127.0.0.1"]
  }
}
Tailscale Serve acts as a local reverse proxy on 127.0.0.1, injecting X-Forwarded-For headers. Adding trustedProxies: ["127.0.0.1"] tells the gateway to trust those headers for client IP detection. Without it, connections log “Proxy headers detected from untrusted address” warnings and proxy headers are ignored. Serve setup checklist:
  1. Set gateway.tailscale.mode: "serve" and gateway.trustedProxies: ["127.0.0.1"].
  2. Restart the gateway.
  3. Open the Control UI from the remote device via https://<magicdns>/.
  4. Approve the device pairing prompt from the local Control UI.
  5. Verify: look for [tailscale] serve enabled in the gateway log and confirm no token_mismatch or pairing required errors.
Open: https://<magicdns>/ (or your configured gateway.controlUi.basePath)

Tailnet-only (bind to Tailnet IP)

Use this when you want the Gateway to listen directly on the Tailnet IP (no Serve/Funnel).
{
  gateway: {
    bind: "tailnet",
    auth: { mode: "token", token: "your-token" }
  }
}
Connect from another Tailnet device:
  • Control UI: http://<tailscale-ip>:18789/
  • WebSocket: ws://<tailscale-ip>:18789
Note: loopback (http://127.0.0.1:18789) will not work in this mode.

Public internet (Funnel + shared password)

{
  gateway: {
    bind: "loopback",
    tailscale: { mode: "funnel" },
    auth: { mode: "password", password: "replace-me" }
  }
}
Prefer EQUABOT_GATEWAY_PASSWORD over committing a password to disk.

CLI examples

equabot gateway --tailscale serve
equabot gateway --tailscale funnel --auth password

Notes

  • Tailscale Serve/Funnel requires the tailscale CLI to be installed and logged in.
  • tailscale.mode: "funnel" refuses to start unless auth mode is password to avoid public exposure.
  • Set gateway.tailscale.resetOnExit if you want Equabot to undo tailscale serve or tailscale funnel configuration on shutdown.
  • gateway.bind: "tailnet" is a direct Tailnet bind (no HTTPS, no Serve/Funnel).
  • gateway.bind: "auto" prefers loopback; use tailnet if you want Tailnet-only.
  • Serve/Funnel only expose the Gateway control UI + WS. Nodes connect over the same Gateway WS endpoint, so Serve can work for node access.

Browser control server (remote Gateway + local browser)

If you run the Gateway on one machine but want to drive a browser on another machine, use a separate browser control server and publish it through Tailscale Serve (tailnet-only):
# on the machine that runs Chrome
equabot browser serve --bind 127.0.0.1 --port 18791 --token <token>
tailscale serve https / http://127.0.0.1:18791
Then point the Gateway config at the HTTPS URL:
{
  browser: {
    enabled: true,
    controlUrl: "https://<magicdns>/"
  }
}
And authenticate from the Gateway with the same token (prefer env):
export EQUABOT_BROWSER_CONTROL_TOKEN="<token>"
Avoid Funnel for browser control endpoints unless you explicitly want public exposure.

Tailscale prerequisites + limits

  • Serve requires HTTPS enabled for your tailnet; the CLI prompts if it is missing.
  • Serve injects Tailscale identity headers; Funnel does not.
  • Funnel requires Tailscale v1.38.3+, MagicDNS, HTTPS enabled, and a funnel node attribute.
  • Funnel only supports ports 443, 8443, and 10000 over TLS.
  • Funnel on macOS requires the open-source Tailscale app variant.

Learn more