Security: default gateway auth bootstrap and explicit mode none (#20686)

Merged via /review-pr -> /prepare-pr -> /merge-pr.

Prepared head SHA: be1b73182cdca9c2331e2113bd1a08b977181974
Co-authored-by: gumadeiras <5599352+gumadeiras@users.noreply.github.com>
Co-authored-by: gumadeiras <5599352+gumadeiras@users.noreply.github.com>
Reviewed-by: @gumadeiras
This commit is contained in:
Gustavo Madeira Santana
2026-02-19 02:35:50 -05:00
committed by GitHub
parent a2e846f649
commit c5698caca3
18 changed files with 678 additions and 77 deletions

View File

@@ -132,4 +132,22 @@ describe("gateway run option collisions", () => {
}),
);
});
it("starts gateway when token mode has no configured token (startup bootstrap path)", async () => {
const { addGatewayRunCommand } = await import("./run.js");
const program = new Command();
const gateway = addGatewayRunCommand(program.command("gateway"));
addGatewayRunCommand(gateway.command("run"));
await program.parseAsync(["gateway", "run", "--allow-unconfigured"], {
from: "user",
});
expect(startGatewayServer).toHaveBeenCalledWith(
18789,
expect.objectContaining({
bind: "loopback",
}),
);
});
});

View File

@@ -1,7 +1,7 @@
import fs from "node:fs";
import path from "node:path";
import type { Command } from "commander";
import type { GatewayAuthMode } from "../../config/config.js";
import type { GatewayAuthMode, GatewayTailscaleMode } from "../../config/config.js";
import {
CONFIG_PATH,
loadConfig,
@@ -193,7 +193,7 @@ async function runGatewayCommand(opts: GatewayRunOpts) {
return;
}
const tailscaleRaw = toOptionString(opts.tailscale);
const tailscaleMode =
const tailscaleMode: GatewayTailscaleMode | null =
tailscaleRaw === "off" || tailscaleRaw === "serve" || tailscaleRaw === "funnel"
? tailscaleRaw
: null;
@@ -239,14 +239,17 @@ async function runGatewayCommand(opts: GatewayRunOpts) {
}
const miskeys = extractGatewayMiskeys(snapshot?.parsed);
const authConfig = {
...cfg.gateway?.auth,
...(authMode ? { mode: authMode } : {}),
...(passwordRaw ? { password: passwordRaw } : {}),
...(tokenRaw ? { token: tokenRaw } : {}),
};
const authOverride =
authMode || passwordRaw || tokenRaw || authModeRaw
? {
...(authMode ? { mode: authMode } : {}),
...(tokenRaw ? { token: tokenRaw } : {}),
...(passwordRaw ? { password: passwordRaw } : {}),
}
: undefined;
const resolvedAuth = resolveGatewayAuth({
authConfig,
authConfig: cfg.gateway?.auth,
authOverride,
env: process.env,
tailscaleMode: tailscaleMode ?? cfg.gateway?.tailscale?.mode ?? "off",
});
@@ -257,6 +260,7 @@ async function runGatewayCommand(opts: GatewayRunOpts) {
const hasPassword = typeof passwordValue === "string" && passwordValue.trim().length > 0;
const hasSharedSecret =
(resolvedAuthMode === "token" && hasToken) || (resolvedAuthMode === "password" && hasPassword);
const canBootstrapToken = resolvedAuthMode === "token" && !hasToken;
const authHints: string[] = [];
if (miskeys.hasGatewayToken) {
authHints.push('Found "gateway.token" in config. Use "gateway.auth.token" instead.');
@@ -266,19 +270,6 @@ async function runGatewayCommand(opts: GatewayRunOpts) {
'"gateway.remote.token" is for remote CLI calls; it does not enable local gateway auth.',
);
}
if (resolvedAuthMode === "token" && !hasToken && !resolvedAuth.allowTailscale) {
defaultRuntime.error(
[
"Gateway auth is set to token, but no token is configured.",
"Set gateway.auth.token (or OPENCLAW_GATEWAY_TOKEN), or pass --token.",
...authHints,
]
.filter(Boolean)
.join("\n"),
);
defaultRuntime.exit(1);
return;
}
if (resolvedAuthMode === "password" && !hasPassword) {
defaultRuntime.error(
[
@@ -292,7 +283,17 @@ async function runGatewayCommand(opts: GatewayRunOpts) {
defaultRuntime.exit(1);
return;
}
if (bind !== "loopback" && !hasSharedSecret && resolvedAuthMode !== "trusted-proxy") {
if (resolvedAuthMode === "none") {
gatewayLog.warn(
"Gateway auth mode=none explicitly configured; all gateway connections are unauthenticated.",
);
}
if (
bind !== "loopback" &&
!hasSharedSecret &&
!canBootstrapToken &&
resolvedAuthMode !== "trusted-proxy"
) {
defaultRuntime.error(
[
`Refusing to bind gateway to ${bind} without auth.`,
@@ -305,6 +306,13 @@ async function runGatewayCommand(opts: GatewayRunOpts) {
defaultRuntime.exit(1);
return;
}
const tailscaleOverride =
tailscaleMode || opts.tailscaleResetOnExit
? {
...(tailscaleMode ? { mode: tailscaleMode } : {}),
...(opts.tailscaleResetOnExit ? { resetOnExit: true } : {}),
}
: undefined;
try {
await runGatewayLoop({
@@ -312,21 +320,8 @@ async function runGatewayCommand(opts: GatewayRunOpts) {
start: async () =>
await startGatewayServer(port, {
bind,
auth:
authMode || passwordRaw || tokenRaw || authModeRaw
? {
mode: authMode ?? undefined,
token: tokenRaw,
password: passwordRaw,
}
: undefined,
tailscale:
tailscaleMode || opts.tailscaleResetOnExit
? {
mode: tailscaleMode ?? undefined,
resetOnExit: Boolean(opts.tailscaleResetOnExit),
}
: undefined,
auth: authOverride,
tailscale: tailscaleOverride,
}),
});
} catch (err) {