mirror of
https://github.com/moltbot/moltbot.git
synced 2026-04-23 22:55:24 +00:00
Gateway: add safer password-file input for gateway run (#39067)
* CLI: add gateway password-file option * Docs: document safer gateway password input * Update src/cli/gateway-cli/run.ts Co-authored-by: greptile-apps[bot] <165735046+greptile-apps[bot]@users.noreply.github.com> * Tests: clean up gateway password temp dirs * CLI: restore gateway password warning flow * Security: harden secret file reads --------- Co-authored-by: greptile-apps[bot] <165735046+greptile-apps[bot]@users.noreply.github.com>
This commit is contained in:
@@ -1,3 +1,6 @@
|
||||
import fs from "node:fs/promises";
|
||||
import os from "node:os";
|
||||
import path from "node:path";
|
||||
import { Command } from "commander";
|
||||
import { beforeAll, beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import { createCliRuntimeCapture } from "../test-runtime-capture.js";
|
||||
@@ -239,4 +242,77 @@ describe("gateway run option collisions", () => {
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it("reads gateway password from --password-file", async () => {
|
||||
const tempDir = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-gateway-run-"));
|
||||
try {
|
||||
const passwordFile = path.join(tempDir, "gateway-password.txt");
|
||||
await fs.writeFile(passwordFile, "pw_from_file\n", "utf8");
|
||||
|
||||
await runGatewayCli([
|
||||
"gateway",
|
||||
"run",
|
||||
"--auth",
|
||||
"password",
|
||||
"--password-file",
|
||||
passwordFile,
|
||||
"--allow-unconfigured",
|
||||
]);
|
||||
|
||||
expect(startGatewayServer).toHaveBeenCalledWith(
|
||||
18789,
|
||||
expect.objectContaining({
|
||||
auth: expect.objectContaining({
|
||||
mode: "password",
|
||||
password: "pw_from_file",
|
||||
}),
|
||||
}),
|
||||
);
|
||||
expect(runtimeErrors).not.toContain(
|
||||
"Warning: --password can be exposed via process listings. Prefer --password-file or OPENCLAW_GATEWAY_PASSWORD.",
|
||||
);
|
||||
} finally {
|
||||
await fs.rm(tempDir, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
it("warns when gateway password is passed inline", async () => {
|
||||
await runGatewayCli([
|
||||
"gateway",
|
||||
"run",
|
||||
"--auth",
|
||||
"password",
|
||||
"--password",
|
||||
"pw_inline",
|
||||
"--allow-unconfigured",
|
||||
]);
|
||||
|
||||
expect(runtimeErrors).toContain(
|
||||
"Warning: --password can be exposed via process listings. Prefer --password-file or OPENCLAW_GATEWAY_PASSWORD.",
|
||||
);
|
||||
});
|
||||
|
||||
it("rejects using both --password and --password-file", async () => {
|
||||
const tempDir = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-gateway-run-"));
|
||||
try {
|
||||
const passwordFile = path.join(tempDir, "gateway-password.txt");
|
||||
await fs.writeFile(passwordFile, "pw_from_file\n", "utf8");
|
||||
|
||||
await expect(
|
||||
runGatewayCli([
|
||||
"gateway",
|
||||
"run",
|
||||
"--password",
|
||||
"pw_inline",
|
||||
"--password-file",
|
||||
passwordFile,
|
||||
"--allow-unconfigured",
|
||||
]),
|
||||
).rejects.toThrow("__exit__:1");
|
||||
|
||||
expect(runtimeErrors).toContain("Use either --password or --password-file.");
|
||||
} finally {
|
||||
await fs.rm(tempDir, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import fs from "node:fs";
|
||||
import path from "node:path";
|
||||
import type { Command } from "commander";
|
||||
import { readSecretFromFile } from "../../acp/secret-file.js";
|
||||
import type { GatewayAuthMode, GatewayTailscaleMode } from "../../config/config.js";
|
||||
import {
|
||||
CONFIG_PATH,
|
||||
@@ -40,6 +41,7 @@ type GatewayRunOpts = {
|
||||
token?: unknown;
|
||||
auth?: unknown;
|
||||
password?: unknown;
|
||||
passwordFile?: unknown;
|
||||
tailscale?: unknown;
|
||||
tailscaleResetOnExit?: boolean;
|
||||
allowUnconfigured?: boolean;
|
||||
@@ -62,6 +64,7 @@ const GATEWAY_RUN_VALUE_KEYS = [
|
||||
"token",
|
||||
"auth",
|
||||
"password",
|
||||
"passwordFile",
|
||||
"tailscale",
|
||||
"wsLog",
|
||||
"rawStreamPath",
|
||||
@@ -87,6 +90,24 @@ const GATEWAY_AUTH_MODES: readonly GatewayAuthMode[] = [
|
||||
];
|
||||
const GATEWAY_TAILSCALE_MODES: readonly GatewayTailscaleMode[] = ["off", "serve", "funnel"];
|
||||
|
||||
function warnInlinePasswordFlag() {
|
||||
defaultRuntime.error(
|
||||
"Warning: --password can be exposed via process listings. Prefer --password-file or OPENCLAW_GATEWAY_PASSWORD.",
|
||||
);
|
||||
}
|
||||
|
||||
function resolveGatewayPasswordOption(opts: GatewayRunOpts): string | undefined {
|
||||
const direct = toOptionString(opts.password);
|
||||
const file = toOptionString(opts.passwordFile);
|
||||
if (direct && file) {
|
||||
throw new Error("Use either --password or --password-file.");
|
||||
}
|
||||
if (file) {
|
||||
return readSecretFromFile(file, "Gateway password");
|
||||
}
|
||||
return direct;
|
||||
}
|
||||
|
||||
function parseEnumOption<T extends string>(
|
||||
raw: string | undefined,
|
||||
allowed: readonly T[],
|
||||
@@ -277,7 +298,17 @@ async function runGatewayCommand(opts: GatewayRunOpts) {
|
||||
defaultRuntime.exit(1);
|
||||
return;
|
||||
}
|
||||
const passwordRaw = toOptionString(opts.password);
|
||||
let passwordRaw: string | undefined;
|
||||
try {
|
||||
passwordRaw = resolveGatewayPasswordOption(opts);
|
||||
} catch (err) {
|
||||
defaultRuntime.error(err instanceof Error ? err.message : String(err));
|
||||
defaultRuntime.exit(1);
|
||||
return;
|
||||
}
|
||||
if (toOptionString(opts.password)) {
|
||||
warnInlinePasswordFlag();
|
||||
}
|
||||
const tokenRaw = toOptionString(opts.token);
|
||||
|
||||
const snapshot = await readConfigFileSnapshot().catch(() => null);
|
||||
@@ -439,6 +470,7 @@ export function addGatewayRunCommand(cmd: Command): Command {
|
||||
)
|
||||
.option("--auth <mode>", `Gateway auth mode (${formatModeChoices(GATEWAY_AUTH_MODES)})`)
|
||||
.option("--password <password>", "Password for auth mode=password")
|
||||
.option("--password-file <path>", "Read gateway password from file")
|
||||
.option(
|
||||
"--tailscale <mode>",
|
||||
`Tailscale exposure mode (${formatModeChoices(GATEWAY_TAILSCALE_MODES)})`,
|
||||
|
||||
Reference in New Issue
Block a user