fix: remove unsupported gateway auth off option

This commit is contained in:
Peter Steinberger
2026-01-26 17:44:13 +00:00
parent e6bdffe568
commit b9098f3401
12 changed files with 21 additions and 50 deletions

View File

@@ -52,6 +52,7 @@ Status: unreleased.
- Web UI: improve WebChat image paste previews and allow image-only sends. (#1925) Thanks @smartprogrammer93. - Web UI: improve WebChat image paste previews and allow image-only sends. (#1925) Thanks @smartprogrammer93.
- Security: wrap external hook content by default with a per-hook opt-out. (#1827) Thanks @mertcicekci0. - Security: wrap external hook content by default with a per-hook opt-out. (#1827) Thanks @mertcicekci0.
- Gateway: default auth now fail-closed (token/password required; Tailscale Serve identity remains allowed). - Gateway: default auth now fail-closed (token/password required; Tailscale Serve identity remains allowed).
- Onboarding: remove unsupported gateway auth "off" choice from onboarding/configure flows and CLI flags.
## 2026.1.24-3 ## 2026.1.24-3

View File

@@ -314,7 +314,7 @@ Options:
- `--opencode-zen-api-key <key>` - `--opencode-zen-api-key <key>`
- `--gateway-port <port>` - `--gateway-port <port>`
- `--gateway-bind <loopback|lan|tailnet|auto|custom>` - `--gateway-bind <loopback|lan|tailnet|auto|custom>`
- `--gateway-auth <off|token|password>` - `--gateway-auth <token|password>`
- `--gateway-token <token>` - `--gateway-token <token>`
- `--gateway-password <password>` - `--gateway-password <password>`
- `--remote-url <url>` - `--remote-url <url>`

View File

@@ -214,7 +214,7 @@ the Gateway likely refused to bind.
- Fix: run `clawdbot doctor` to update it (or `clawdbot gateway install --force` for a full rewrite). - Fix: run `clawdbot doctor` to update it (or `clawdbot gateway install --force` for a full rewrite).
**If `Last gateway error:` mentions “refusing to bind … without auth”** **If `Last gateway error:` mentions “refusing to bind … without auth”**
- You set `gateway.bind` to a non-loopback mode (`lan`/`tailnet`/`custom`, or `auto` when loopback is unavailable) but left auth off. - You set `gateway.bind` to a non-loopback mode (`lan`/`tailnet`/`custom`, or `auto` when loopback is unavailable) but didnt configure auth.
- Fix: set `gateway.auth.mode` + `gateway.auth.token` (or export `CLAWDBOT_GATEWAY_TOKEN`) and restart the service. - Fix: set `gateway.auth.mode` + `gateway.auth.token` (or export `CLAWDBOT_GATEWAY_TOKEN`) and restart the service.
**If `clawdbot gateway status` says `bind=tailnet` but no tailnet interface was found** **If `clawdbot gateway status` says `bind=tailnet` but no tailnet interface was found**

View File

@@ -78,7 +78,7 @@ export function registerOnboardCommand(program: Command) {
.option("--opencode-zen-api-key <key>", "OpenCode Zen API key") .option("--opencode-zen-api-key <key>", "OpenCode Zen API key")
.option("--gateway-port <port>", "Gateway port") .option("--gateway-port <port>", "Gateway port")
.option("--gateway-bind <mode>", "Gateway bind: loopback|tailnet|lan|auto|custom") .option("--gateway-bind <mode>", "Gateway bind: loopback|tailnet|lan|auto|custom")
.option("--gateway-auth <mode>", "Gateway auth: off|token|password") .option("--gateway-auth <mode>", "Gateway auth: token|password")
.option("--gateway-token <token>", "Gateway token (token auth)") .option("--gateway-token <token>", "Gateway token (token auth)")
.option("--gateway-password <password>", "Gateway password (password auth)") .option("--gateway-password <password>", "Gateway password (password auth)")
.option("--remote-url <url>", "Remote Gateway WebSocket URL") .option("--remote-url <url>", "Remote Gateway WebSocket URL")

View File

@@ -3,26 +3,18 @@ import { describe, expect, it } from "vitest";
import { buildGatewayAuthConfig } from "./configure.js"; import { buildGatewayAuthConfig } from "./configure.js";
describe("buildGatewayAuthConfig", () => { describe("buildGatewayAuthConfig", () => {
it("clears token/password when auth is off", () => { it("preserves allowTailscale when switching to token", () => {
const result = buildGatewayAuthConfig({
existing: { mode: "token", token: "abc", password: "secret" },
mode: "off",
});
expect(result).toBeUndefined();
});
it("preserves allowTailscale when auth is off", () => {
const result = buildGatewayAuthConfig({ const result = buildGatewayAuthConfig({
existing: { existing: {
mode: "token", mode: "password",
token: "abc", password: "secret",
allowTailscale: true, allowTailscale: true,
}, },
mode: "off", mode: "token",
token: "abc",
}); });
expect(result).toEqual({ allowTailscale: true }); expect(result).toEqual({ mode: "token", token: "abc", allowTailscale: true });
}); });
it("drops password when switching to token", () => { it("drops password when switching to token", () => {

View File

@@ -12,7 +12,7 @@ import {
promptModelAllowlist, promptModelAllowlist,
} from "./model-picker.js"; } from "./model-picker.js";
type GatewayAuthChoice = "off" | "token" | "password"; type GatewayAuthChoice = "token" | "password";
const ANTHROPIC_OAUTH_MODEL_KEYS = [ const ANTHROPIC_OAUTH_MODEL_KEYS = [
"anthropic/claude-opus-4-5", "anthropic/claude-opus-4-5",
@@ -30,9 +30,6 @@ export function buildGatewayAuthConfig(params: {
const base: GatewayAuthConfig = {}; const base: GatewayAuthConfig = {};
if (typeof allowTailscale === "boolean") base.allowTailscale = allowTailscale; if (typeof allowTailscale === "boolean") base.allowTailscale = allowTailscale;
if (params.mode === "off") {
return Object.keys(base).length > 0 ? base : undefined;
}
if (params.mode === "token") { if (params.mode === "token") {
return { ...base, mode: "token", token: params.token }; return { ...base, mode: "token", token: params.token };
} }

View File

@@ -7,7 +7,7 @@ import { buildGatewayAuthConfig } from "./configure.gateway-auth.js";
import { confirm, select, text } from "./configure.shared.js"; import { confirm, select, text } from "./configure.shared.js";
import { guardCancel, randomToken } from "./onboard-helpers.js"; import { guardCancel, randomToken } from "./onboard-helpers.js";
type GatewayAuthChoice = "off" | "token" | "password"; type GatewayAuthChoice = "token" | "password";
export async function promptGatewayConfig( export async function promptGatewayConfig(
cfg: ClawdbotConfig, cfg: ClawdbotConfig,
@@ -91,11 +91,6 @@ export async function promptGatewayConfig(
await select({ await select({
message: "Gateway auth", message: "Gateway auth",
options: [ options: [
{
value: "off",
label: "Off (loopback only)",
hint: "Not recommended unless you fully trust local processes",
},
{ value: "token", label: "Token", hint: "Recommended default" }, { value: "token", label: "Token", hint: "Recommended default" },
{ value: "password", label: "Password" }, { value: "password", label: "Password" },
], ],
@@ -165,11 +160,6 @@ export async function promptGatewayConfig(
bind = "loopback"; bind = "loopback";
} }
if (authMode === "off" && bind !== "loopback") {
note("Non-loopback bind requires auth. Switching to token auth.", "Note");
authMode = "token";
}
if (tailscaleMode === "funnel" && authMode !== "password") { if (tailscaleMode === "funnel" && authMode !== "password") {
note("Tailscale funnel requires password auth.", "Note"); note("Tailscale funnel requires password auth.", "Note");
authMode = "password"; authMode = "password";

View File

@@ -210,7 +210,7 @@ describe("onboard (non-interactive): gateway and remote auth", () => {
await fs.rm(stateDir, { recursive: true, force: true }); await fs.rm(stateDir, { recursive: true, force: true });
}, 60_000); }, 60_000);
it("auto-enables token auth when binding LAN and persists the token", async () => { it("auto-generates token auth when binding LAN and persists the token", async () => {
if (process.platform === "win32") { if (process.platform === "win32") {
// Windows runner occasionally drops the temp config write in this flow; skip to keep CI green. // Windows runner occasionally drops the temp config write in this flow; skip to keep CI green.
return; return;
@@ -242,7 +242,6 @@ describe("onboard (non-interactive): gateway and remote auth", () => {
installDaemon: false, installDaemon: false,
gatewayPort: port, gatewayPort: port,
gatewayBind: "lan", gatewayBind: "lan",
gatewayAuth: "off",
}, },
runtime, runtime,
); );

View File

@@ -28,16 +28,20 @@ export function applyNonInteractiveGatewayConfig(params: {
const port = hasGatewayPort ? (opts.gatewayPort as number) : params.defaultPort; const port = hasGatewayPort ? (opts.gatewayPort as number) : params.defaultPort;
let bind = opts.gatewayBind ?? "loopback"; let bind = opts.gatewayBind ?? "loopback";
let authMode = opts.gatewayAuth ?? "token"; const authModeRaw = opts.gatewayAuth ?? "token";
if (authModeRaw !== "token" && authModeRaw !== "password") {
runtime.error("Invalid --gateway-auth (use token|password).");
runtime.exit(1);
return null;
}
let authMode = authModeRaw;
const tailscaleMode = opts.tailscale ?? "off"; const tailscaleMode = opts.tailscale ?? "off";
const tailscaleResetOnExit = Boolean(opts.tailscaleResetOnExit); const tailscaleResetOnExit = Boolean(opts.tailscaleResetOnExit);
// Tighten config to safe combos: // Tighten config to safe combos:
// - If Tailscale is on, force loopback bind (the tunnel handles external access). // - If Tailscale is on, force loopback bind (the tunnel handles external access).
// - If binding beyond loopback, disallow auth=off.
// - If using Tailscale Funnel, require password auth. // - If using Tailscale Funnel, require password auth.
if (tailscaleMode !== "off" && bind !== "loopback") bind = "loopback"; if (tailscaleMode !== "off" && bind !== "loopback") bind = "loopback";
if (authMode === "off" && bind !== "loopback") authMode = "token";
if (tailscaleMode === "funnel" && authMode !== "password") authMode = "password"; if (tailscaleMode === "funnel" && authMode !== "password") authMode = "password";
let nextConfig = params.nextConfig; let nextConfig = params.nextConfig;

View File

@@ -32,7 +32,7 @@ export type AuthChoice =
| "copilot-proxy" | "copilot-proxy"
| "qwen-portal" | "qwen-portal"
| "skip"; | "skip";
export type GatewayAuthChoice = "off" | "token" | "password"; export type GatewayAuthChoice = "token" | "password";
export type ResetScope = "config" | "config+creds+sessions" | "full"; export type ResetScope = "config" | "config+creds+sessions" | "full";
export type GatewayBind = "loopback" | "lan" | "auto" | "custom" | "tailnet"; export type GatewayBind = "loopback" | "lan" | "auto" | "custom" | "tailnet";
export type TailscaleMode = "off" | "serve" | "funnel"; export type TailscaleMode = "off" | "serve" | "funnel";

View File

@@ -93,11 +93,6 @@ export async function configureGatewayForOnboarding(
: ((await prompter.select({ : ((await prompter.select({
message: "Gateway auth", message: "Gateway auth",
options: [ options: [
{
value: "off",
label: "Off (loopback only)",
hint: "Not recommended unless you fully trust local processes",
},
{ {
value: "token", value: "token",
label: "Token", label: "Token",
@@ -165,7 +160,6 @@ export async function configureGatewayForOnboarding(
// Safety + constraints: // Safety + constraints:
// - Tailscale wants bind=loopback so we never expose a non-loopback server + tailscale serve/funnel at once. // - Tailscale wants bind=loopback so we never expose a non-loopback server + tailscale serve/funnel at once.
// - Auth off only allowed for bind=loopback.
// - Funnel requires password auth. // - Funnel requires password auth.
if (tailscaleMode !== "off" && bind !== "loopback") { if (tailscaleMode !== "off" && bind !== "loopback") {
await prompter.note("Tailscale requires bind=loopback. Adjusting bind to loopback.", "Note"); await prompter.note("Tailscale requires bind=loopback. Adjusting bind to loopback.", "Note");
@@ -173,11 +167,6 @@ export async function configureGatewayForOnboarding(
customBindHost = undefined; customBindHost = undefined;
} }
if (authMode === "off" && bind !== "loopback") {
await prompter.note("Non-loopback bind requires auth. Switching to token auth.", "Note");
authMode = "token";
}
if (tailscaleMode === "funnel" && authMode !== "password") { if (tailscaleMode === "funnel" && authMode !== "password") {
await prompter.note("Tailscale funnel requires password auth.", "Note"); await prompter.note("Tailscale funnel requires password auth.", "Note");
authMode = "password"; authMode = "password";

View File

@@ -244,7 +244,6 @@ export async function runOnboardingWizard(
return "Auto"; return "Auto";
}; };
const formatAuth = (value: GatewayAuthChoice) => { const formatAuth = (value: GatewayAuthChoice) => {
if (value === "off") return "Off (loopback only)";
if (value === "token") return "Token (default)"; if (value === "token") return "Token (default)";
return "Password"; return "Password";
}; };