fix: harden gateway auth defaults

This commit is contained in:
Peter Steinberger
2026-01-11 01:50:46 +01:00
parent 49e7004664
commit d33285a9cd
9 changed files with 187 additions and 30 deletions

View File

@@ -288,6 +288,7 @@ vi.mock("./onboard-helpers.js", () => ({
DEFAULT_WORKSPACE: "/tmp",
guardCancel: (value: unknown) => value,
printWizardHeader: vi.fn(),
randomToken: vi.fn(() => "test-gateway-token"),
}));
vi.mock("./doctor-state-migrations.js", () => ({
@@ -749,7 +750,10 @@ describe("doctor", () => {
return Promise.resolve({ stdout: "", stderr: "" });
});
confirm.mockResolvedValueOnce(false).mockResolvedValueOnce(true);
confirm
.mockResolvedValueOnce(false) // skip gateway token prompt
.mockResolvedValueOnce(false) // skip build
.mockResolvedValueOnce(true); // accept legacy fallback
const { doctorCommand } = await import("./doctor.js");
const runtime = {

View File

@@ -84,7 +84,11 @@ import {
} from "./doctor-workspace.js";
import { healthCommand } from "./health.js";
import { formatHealthCheckFailure } from "./health-format.js";
import { applyWizardMetadata, printWizardHeader } from "./onboard-helpers.js";
import {
applyWizardMetadata,
printWizardHeader,
randomToken,
} from "./onboard-helpers.js";
import { ensureSystemdUserLingerInteractive } from "./systemd-linger.js";
const intro = (message: string) =>
@@ -279,6 +283,45 @@ export async function doctorCommand(
if (gatewayDetails.remoteFallbackNote) {
note(gatewayDetails.remoteFallbackNote, "Gateway");
}
if (resolveMode(cfg) === "local") {
const authMode = cfg.gateway?.auth?.mode;
const token =
typeof cfg.gateway?.auth?.token === "string"
? cfg.gateway?.auth?.token.trim()
: "";
const needsToken =
authMode !== "password" && (authMode !== "token" || !token);
if (needsToken) {
note(
"Gateway auth is off or missing a token. Token auth is now the recommended default (including loopback).",
"Gateway auth",
);
const shouldSetToken =
options.generateGatewayToken === true
? true
: options.nonInteractive === true
? false
: await prompter.confirmRepair({
message: "Generate and configure a gateway token now?",
initialValue: true,
});
if (shouldSetToken) {
const nextToken = randomToken();
cfg = {
...cfg,
gateway: {
...cfg.gateway,
auth: {
...cfg.gateway?.auth,
mode: "token",
token: nextToken,
},
},
};
note("Gateway token configured.", "Gateway auth");
}
}
}
const legacyState = await detectLegacyStateMigrations({ cfg });
if (legacyState.preview.length > 0) {

View File

@@ -14,6 +14,7 @@ import { CONFIG_PATH_CLAWDBOT } from "../config/config.js";
import { resolveSessionTranscriptsDirForAgent } from "../config/sessions.js";
import { callGateway } from "../gateway/call.js";
import { normalizeControlUiBasePath } from "../gateway/control-ui.js";
import { isSafeExecutableValue } from "../infra/exec-safety.js";
import { pickPrimaryTailnetIPv4 } from "../infra/tailnet.js";
import { runCommandWithTimeout } from "../process/exec.js";
import type { RuntimeEnv } from "../runtime.js";
@@ -288,8 +289,14 @@ export async function handleReset(
export async function detectBinary(name: string): Promise<boolean> {
if (!name?.trim()) return false;
if (!isSafeExecutableValue(name)) return false;
const resolved = name.startsWith("~") ? resolveUserPath(name) : name;
if (path.isAbsolute(resolved) || resolved.startsWith(".")) {
if (
path.isAbsolute(resolved) ||
resolved.startsWith(".") ||
resolved.includes("/") ||
resolved.includes("\\")
) {
try {
await fs.access(resolved);
return true;
@@ -301,7 +308,7 @@ export async function detectBinary(name: string): Promise<boolean> {
const command =
process.platform === "win32"
? ["where", name]
: ["/usr/bin/env", "sh", "-lc", `command -v ${name}`];
: ["/usr/bin/env", "which", name];
try {
const result = await runCommandWithTimeout(command, { timeoutMs: 2000 });
return result.code === 0 && result.stdout.trim().length > 0;