fix: remove unsupported gateway auth off option
This commit is contained in:
@@ -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
|
||||||
|
|
||||||
|
|||||||
@@ -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>`
|
||||||
|
|||||||
@@ -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 didn’t 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**
|
||||||
|
|||||||
@@ -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")
|
||||||
|
|||||||
@@ -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", () => {
|
||||||
|
|||||||
@@ -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 };
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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";
|
||||||
|
|||||||
@@ -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,
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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";
|
||||||
|
|||||||
@@ -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";
|
||||||
|
|||||||
@@ -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";
|
||||||
};
|
};
|
||||||
|
|||||||
Reference in New Issue
Block a user