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:
committed by
GitHub
parent
a2e846f649
commit
c5698caca3
@@ -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",
|
||||
}),
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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) {
|
||||
|
||||
Reference in New Issue
Block a user