diff --git a/CHANGELOG.md b/CHANGELOG.md
index 668a91823..5fc2690ab 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -36,6 +36,7 @@ Status: unreleased.
### Fixes
- Security: harden Tailscale Serve auth by validating identity via local tailscaled before trusting headers.
+- Security: harden gateway runtime defaults and security guidance. (#2000) Thanks @rhuanssauro.
- Web UI: improve WebChat image paste previews and allow image-only sends. (#1925) Thanks @smartprogrammer93.
- Gateway: default auth now fail-closed (token/password required; Tailscale Serve identity remains allowed).
diff --git a/README.md b/README.md
index 217a4b61c..6c20d6d6d 100644
--- a/README.md
+++ b/README.md
@@ -479,32 +479,32 @@ Thanks to all clawtributors:
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/SECURITY.md b/SECURITY.md
index 11aa0b781..773924ccd 100644
--- a/SECURITY.md
+++ b/SECURITY.md
@@ -17,10 +17,8 @@ For threat model + hardening guidance (including `clawdbot security audit --deep
### Node.js Version
-Clawdbot requires **Node.js 22.12.0 or later** (LTS). This version includes important security patches:
-
-- CVE-2025-59466: async_hooks DoS vulnerability
-- CVE-2026-21636: Permission model bypass vulnerability
+Clawdbot requires **Node.js 22.12.0 or later** (LTS). Keep Node updated for
+security patches and compatibility fixes.
Verify your Node.js version:
@@ -33,14 +31,16 @@ node --version # Should be v22.12.0 or later
When running Clawdbot in Docker:
1. The official image runs as a non-root user (`node`) for reduced attack surface
-2. Use `--read-only` flag when possible for additional filesystem protection
+2. Use `--read-only` when possible and provide a writable state volume
3. Limit container capabilities with `--cap-drop=ALL`
Example secure Docker run:
```bash
docker run --read-only --cap-drop=ALL \
- -v clawdbot-data:/app/data \
+ --tmpfs /tmp \
+ -e CLAWDBOT_STATE_DIR=/data \
+ -v clawdbot-data:/data \
clawdbot/clawdbot:latest
```
diff --git a/docs/gateway/configuration-examples.md b/docs/gateway/configuration-examples.md
index ff38859a7..bb8f0b70b 100644
--- a/docs/gateway/configuration-examples.md
+++ b/docs/gateway/configuration-examples.md
@@ -392,7 +392,7 @@ Save to `~/.clawdbot/clawdbot.json` and you can DM the bot from that number.
auth: {
mode: "token",
token: "gateway-token",
- allowTailscale: true
+ allowTailscale: false
},
tailscale: { mode: "serve", resetOnExit: false },
remote: { url: "ws://gateway.tailnet:18789", token: "remote-token" },
diff --git a/docs/gateway/configuration.md b/docs/gateway/configuration.md
index 97427debe..97148ff47 100644
--- a/docs/gateway/configuration.md
+++ b/docs/gateway/configuration.md
@@ -2867,22 +2867,17 @@ Notes:
- `gateway.port` controls the single multiplexed port used for WebSocket + HTTP (control UI, hooks, A2UI).
- OpenAI Chat Completions endpoint: **disabled by default**; enable with `gateway.http.endpoints.chatCompletions.enabled: true`.
- Precedence: `--port` > `CLAWDBOT_GATEWAY_PORT` > `gateway.port` > default `18789`.
-- Gateway auth is required by default (token/password or Tailscale Serve identity). Non-loopback binds require a shared token/password.
+- Gateway auth is required by default (token/password). Non-loopback binds require a shared token/password.
- The onboarding wizard generates a gateway token by default (even on loopback).
- `gateway.remote.token` is **only** for remote CLI calls; it does not enable local gateway auth. `gateway.token` is ignored.
Auth and Tailscale:
- `gateway.auth.mode` sets the handshake requirements (`token` or `password`). When unset, token auth is assumed.
- `gateway.auth.token` stores the shared token for token auth (used by the CLI on the same machine).
-- When `gateway.auth.mode` is set, only that method is accepted (plus optional Tailscale headers).
+- When `gateway.auth.mode` is set, only that method is accepted.
- `gateway.auth.password` can be set here, or via `CLAWDBOT_GATEWAY_PASSWORD` (recommended).
-- `gateway.auth.allowTailscale` allows Tailscale Serve identity headers
- (`tailscale-user-login`) to satisfy auth when the request arrives on loopback
- with `x-forwarded-for`, `x-forwarded-proto`, and `x-forwarded-host`. Clawdbot
- verifies the identity by resolving the `x-forwarded-for` address via
- `tailscale whois` before accepting it. When `true`, Serve requests do not need
- a token/password; set `false` to require explicit credentials. Defaults to
- `true` when `tailscale.mode = "serve"` and auth mode is not `password`.
+- `gateway.auth.allowTailscale` is a legacy option; do not use it for auth.
+ Keep it `false` and require token/password even with Tailscale Serve.
- `gateway.tailscale.mode: "serve"` uses Tailscale Serve (tailnet only, loopback bind).
- `gateway.tailscale.mode: "funnel"` exposes the dashboard publicly; requires auth.
- `gateway.tailscale.resetOnExit` resets Serve/Funnel config on shutdown.
diff --git a/docs/gateway/remote.md b/docs/gateway/remote.md
index aa3fff7f7..8a5f5fff6 100644
--- a/docs/gateway/remote.md
+++ b/docs/gateway/remote.md
@@ -115,8 +115,8 @@ Short version: **keep the Gateway loopback-only** unless you’re sure you need
- **Non-loopback binds** (`lan`/`tailnet`/`custom`, or `auto` when loopback is unavailable) must use auth tokens/passwords.
- `gateway.remote.token` is **only** for remote CLI calls — it does **not** enable local auth.
- `gateway.remote.tlsFingerprint` pins the remote TLS cert when using `wss://`.
-- **Tailscale Serve** can authenticate via identity headers when `gateway.auth.allowTailscale: true`.
- Set it to `false` if you want tokens/passwords instead.
+- **Tailscale Serve** only provides HTTPS routing; it does not replace auth. Use a
+ token/password and keep `gateway.auth.allowTailscale: false`.
- Treat `browser.controlUrl` like an admin API: tailnet-only + token auth.
Deep dive: [Security](/gateway/security).
diff --git a/docs/gateway/security.md b/docs/gateway/security.md
index d13d830cf..6ca2df023 100644
--- a/docs/gateway/security.md
+++ b/docs/gateway/security.md
@@ -327,19 +327,13 @@ Rotation checklist (token/password):
3. Update any remote clients (`gateway.remote.token` / `.password` on machines that call into the Gateway).
4. Verify you can no longer connect with the old credentials.
-### 0.6) Tailscale Serve identity headers
+### 0.6) Tailscale Serve auth
-When `gateway.auth.allowTailscale` is `true` (default for Serve), Clawdbot
-accepts Tailscale Serve identity headers (`tailscale-user-login`) as
-authentication. Clawdbot verifies the identity by resolving the
-`x-forwarded-for` address through the local Tailscale daemon (`tailscale whois`)
-and matching it to the header. This only triggers for requests that hit loopback
-and include `x-forwarded-for`, `x-forwarded-proto`, and `x-forwarded-host` as
-injected by Tailscale.
-
-**Security rule:** do not forward these headers from your own reverse proxy. If
-you terminate TLS or proxy in front of the gateway, disable
-`gateway.auth.allowTailscale` and use token/password auth instead.
+Tailscale Serve/Funnel provide HTTPS and routing; they do **not** replace Gateway
+auth. Always require a token/password and keep `gateway.auth.allowTailscale: false`
+(legacy option). If you terminate TLS or proxy in front of the gateway,
+ensure your proxy overwrites `x-forwarded-*` and is listed in
+`gateway.trustedProxies`.
Trusted proxies:
- If you terminate TLS in front of the Gateway, set `gateway.trustedProxies` to your proxy IPs.
diff --git a/docs/gateway/tailscale.md b/docs/gateway/tailscale.md
index e6477fbfc..41baebc98 100644
--- a/docs/gateway/tailscale.md
+++ b/docs/gateway/tailscale.md
@@ -8,7 +8,7 @@ read_when:
Clawdbot 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.
+Tailscale provides HTTPS and routing.
## Modes
@@ -23,16 +23,8 @@ Set `gateway.auth.mode` to control the handshake:
- `token` (default when `CLAWDBOT_GATEWAY_TOKEN` is set)
- `password` (shared secret via `CLAWDBOT_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. Clawdbot verifies
-the identity by resolving the `x-forwarded-for` address via the local Tailscale
-daemon (`tailscale whois`) and matching it to the header before accepting it.
-Clawdbot 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"`.
+Tailscale Serve/Funnel do **not** replace Gateway auth. Always require a token
+or password, and keep `gateway.auth.allowTailscale: false` (legacy option).
## Config examples
@@ -42,7 +34,8 @@ force `gateway.auth.mode: "password"`.
{
gateway: {
bind: "loopback",
- tailscale: { mode: "serve" }
+ tailscale: { mode: "serve" },
+ auth: { mode: "token", token: "your-token", allowTailscale: false }
}
}
```
@@ -133,7 +126,7 @@ Avoid Funnel for browser control endpoints unless you explicitly want public exp
## 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.
+- Serve injects Tailscale identity headers; Clawdbot does not use them for auth.
- 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.
diff --git a/docs/help/faq.md b/docs/help/faq.md
index aadbda9de..1d744047a 100644
--- a/docs/help/faq.md
+++ b/docs/help/faq.md
@@ -327,7 +327,7 @@ The wizard now opens your browser with a tokenized dashboard URL right after onb
- The token is the same value as `gateway.auth.token` (or `CLAWDBOT_GATEWAY_TOKEN`) and is stored by the UI after first load.
**Not on localhost:**
-- **Tailscale Serve** (recommended): keep bind loopback, run `clawdbot gateway --tailscale serve`, open `https:///`. If `gateway.auth.allowTailscale` is `true`, identity headers satisfy auth (no token).
+- **Tailscale Serve** (recommended): keep bind loopback, run `clawdbot gateway --tailscale serve`, open `https:///`, and authenticate with your token/password (keep `gateway.auth.allowTailscale: false`).
- **Tailnet bind**: run `clawdbot gateway --bind tailnet --token ""`, open `http://:18789/`, paste token in dashboard settings.
- **SSH tunnel**: `ssh -N -L 18789:127.0.0.1:18789 user@host` then open `http://127.0.0.1:18789/?token=...` from `clawdbot dashboard`.
@@ -1434,7 +1434,7 @@ Check the basics:
- Channel health: `clawdbot channels status`
Then verify auth and routing:
-- If you use Tailscale Serve, make sure `gateway.auth.allowTailscale` is set correctly.
+- If you use Tailscale Serve, confirm you’re using token/password auth and `gateway.auth.allowTailscale: false`.
- If you connect via SSH tunnel, confirm the local tunnel is up and points at the right port.
- Confirm your allowlists (DM or group) include your account.
diff --git a/docs/platforms/digitalocean.md b/docs/platforms/digitalocean.md
index 632057c84..04edb34d8 100644
--- a/docs/platforms/digitalocean.md
+++ b/docs/platforms/digitalocean.md
@@ -122,8 +122,8 @@ clawdbot gateway restart
Open: `https:///`
Notes:
-- Serve keeps the Gateway loopback-only and authenticates via Tailscale identity headers.
-- To require token/password instead, set `gateway.auth.allowTailscale: false` or use `gateway.auth.mode: "password"`.
+- Serve keeps the Gateway loopback-only and provides HTTPS routing.
+- You still need gateway auth (token/password). Keep `gateway.auth.allowTailscale: false` and set `gateway.auth.mode` to `token` or `password`.
**Option C: Tailnet bind (no Serve)**
```bash
diff --git a/docs/web/control-ui.md b/docs/web/control-ui.md
index 996ed0fe4..cf2b403cf 100644
--- a/docs/web/control-ui.md
+++ b/docs/web/control-ui.md
@@ -68,13 +68,9 @@ clawdbot gateway --tailscale serve
Open:
- `https:///` (or your configured `gateway.controlUi.basePath`)
-By default, Serve requests can authenticate via Tailscale identity headers
-(`tailscale-user-login`) when `gateway.auth.allowTailscale` is `true`. Clawdbot
-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` (or force `gateway.auth.mode: "password"`)
-if you want to require a token/password even for Serve traffic.
+Serve does **not** replace Gateway auth. Set a token/password (prefer
+`CLAWDBOT_GATEWAY_TOKEN` or `CLAWDBOT_GATEWAY_PASSWORD`) and connect with it.
+Keep `gateway.auth.allowTailscale: false` (legacy option).
### Bind to tailnet + token
diff --git a/docs/web/dashboard.md b/docs/web/dashboard.md
index 81d0aacc4..ad0a57a6c 100644
--- a/docs/web/dashboard.md
+++ b/docs/web/dashboard.md
@@ -29,7 +29,7 @@ Authentication is enforced at the WebSocket handshake via `connect.params.auth`
- **Localhost**: open `http://127.0.0.1:18789/`. If you see “unauthorized,” run `clawdbot dashboard` and use the tokenized link (`?token=...`).
- **Token source**: `gateway.auth.token` (or `CLAWDBOT_GATEWAY_TOKEN`); the UI stores it after first load.
-- **Not localhost**: use Tailscale Serve (tokenless if `gateway.auth.allowTailscale: true`), tailnet bind with a token, or an SSH tunnel. See [Web surfaces](/web).
+- **Not localhost**: use Tailscale Serve with a token/password, tailnet bind with a token, or an SSH tunnel. See [Web surfaces](/web).
## If you see “unauthorized” / 1008
diff --git a/docs/web/index.md b/docs/web/index.md
index 0e1fadfa4..3e919db7e 100644
--- a/docs/web/index.md
+++ b/docs/web/index.md
@@ -91,14 +91,13 @@ Open:
## Security notes
-- Gateway auth is required by default (token/password or Tailscale identity headers).
+- Gateway auth is required by default (token/password).
- Non-loopback binds still **require** a shared token/password (`gateway.auth` or env).
- The wizard generates a gateway token by default (even on loopback).
- The UI sends `connect.params.auth.token` or `connect.params.auth.password`.
-- With Serve, Tailscale identity headers can satisfy auth when
- `gateway.auth.allowTailscale` is `true` (no token/password required). Set
- `gateway.auth.allowTailscale: false` to require explicit credentials. See
- [Tailscale](/gateway/tailscale) and [Security](/gateway/security).
+- Tailscale Serve does not bypass auth; use token/password and keep
+ `gateway.auth.allowTailscale: false`. See [Tailscale](/gateway/tailscale) and
+ [Security](/gateway/security).
- `gateway.tailscale.mode: "funnel"` requires `gateway.auth.mode: "password"` (shared password).
## Building the UI
diff --git a/extensions/memory-core/package.json b/extensions/memory-core/package.json
index e9a682855..c70da1395 100644
--- a/extensions/memory-core/package.json
+++ b/extensions/memory-core/package.json
@@ -9,6 +9,6 @@
]
},
"peerDependencies": {
- "clawdbot": ">=2026.1.24"
+ "clawdbot": ">=2026.1.25"
}
}