diff --git a/src/docs/install-cloud-secrets.test.ts b/src/docs/install-cloud-secrets.test.ts index 6953c734584..f5afa614702 100644 --- a/src/docs/install-cloud-secrets.test.ts +++ b/src/docs/install-cloud-secrets.test.ts @@ -1,6 +1,7 @@ import fs from "node:fs/promises"; import path from "node:path"; import { describe, expect, it } from "vitest"; +import { KNOWN_WEAK_GATEWAY_TOKEN_PLACEHOLDERS } from "../gateway/known-weak-gateway-secrets.js"; const INSTALL_DOCS_DIR = path.join(process.cwd(), "docs", "install"); const CLOUD_INSTALL_DOCS = ["gcp.md", "hetzner.md"] as const; @@ -10,7 +11,9 @@ describe("cloud install docs", () => { for (const docName of CLOUD_INSTALL_DOCS) { const markdown = await fs.readFile(path.join(INSTALL_DOCS_DIR, docName), "utf8"); - expect(markdown).not.toContain("OPENCLAW_GATEWAY_TOKEN=change-me-now"); + for (const token of KNOWN_WEAK_GATEWAY_TOKEN_PLACEHOLDERS) { + expect(markdown).not.toContain(`OPENCLAW_GATEWAY_TOKEN=${token}`); + } expect(markdown).toMatch(/OPENCLAW_GATEWAY_TOKEN=[ \t]*\r?\n/); expect(markdown).toContain("openssl rand -hex 32"); } diff --git a/src/gateway/known-weak-gateway-secrets.ts b/src/gateway/known-weak-gateway-secrets.ts new file mode 100644 index 00000000000..f820fd9bb53 --- /dev/null +++ b/src/gateway/known-weak-gateway-secrets.ts @@ -0,0 +1,6 @@ +export const KNOWN_WEAK_GATEWAY_TOKEN_PLACEHOLDERS = [ + "change-me-to-a-long-random-token", + "change-me-now", +] as const; + +export const KNOWN_WEAK_GATEWAY_PASSWORD_PLACEHOLDERS = ["change-me-to-a-strong-password"] as const; diff --git a/src/gateway/startup-auth.test.ts b/src/gateway/startup-auth.test.ts index 0c9f9c1d447..1d39d7aea12 100644 --- a/src/gateway/startup-auth.test.ts +++ b/src/gateway/startup-auth.test.ts @@ -1,6 +1,7 @@ import { beforeEach, describe, expect, it, vi } from "vitest"; import type { OpenClawConfig } from "../config/config.js"; import { expectGeneratedTokenPersistedToGatewayAuth } from "../test-utils/auth-token-assertions.js"; +import { KNOWN_WEAK_GATEWAY_TOKEN_PLACEHOLDERS } from "./known-weak-gateway-secrets.js"; import { assertGatewayAuthNotKnownWeak, assertHooksTokenSeparateFromGatewayAuth, @@ -404,34 +405,40 @@ describe("ensureGatewayStartupAuth", () => { ).rejects.toThrow(/hooks\.token must not match gateway auth token/i); }); - it("rejects the .env.example placeholder token supplied via environment", async () => { - await expect( - ensureGatewayStartupAuth({ - cfg: {}, - env: { - OPENCLAW_GATEWAY_TOKEN: "change-me-to-a-long-random-token", - } as NodeJS.ProcessEnv, - }), - ).rejects.toThrow(/example placeholder/i); - expect(mocks.replaceConfigFile).not.toHaveBeenCalled(); - }); + it.each(KNOWN_WEAK_GATEWAY_TOKEN_PLACEHOLDERS)( + "rejects the published placeholder token %s supplied via environment", + async (token) => { + await expect( + ensureGatewayStartupAuth({ + cfg: {}, + env: { + OPENCLAW_GATEWAY_TOKEN: token, + } as NodeJS.ProcessEnv, + }), + ).rejects.toThrow(/example placeholder/i); + expect(mocks.replaceConfigFile).not.toHaveBeenCalled(); + }, + ); - it("rejects the .env.example placeholder token supplied via config", async () => { - await expect( - ensureGatewayStartupAuth({ - cfg: { - gateway: { - auth: { - mode: "token", - token: "change-me-to-a-long-random-token", + it.each(KNOWN_WEAK_GATEWAY_TOKEN_PLACEHOLDERS)( + "rejects the published placeholder token %s supplied via config", + async (token) => { + await expect( + ensureGatewayStartupAuth({ + cfg: { + gateway: { + auth: { + mode: "token", + token, + }, }, }, - }, - env: {} as NodeJS.ProcessEnv, - }), - ).rejects.toThrow(/example placeholder/i); - expect(mocks.replaceConfigFile).not.toHaveBeenCalled(); - }); + env: {} as NodeJS.ProcessEnv, + }), + ).rejects.toThrow(/example placeholder/i); + expect(mocks.replaceConfigFile).not.toHaveBeenCalled(); + }, + ); it("rejects the .env.example placeholder password supplied via config", async () => { await expect( @@ -472,16 +479,19 @@ describe("assertGatewayAuthNotKnownWeak", () => { mocks.replaceConfigFile.mockClear(); }); - it("throws on the known-weak token sentinel", () => { - expect(() => - assertGatewayAuthNotKnownWeak({ - mode: "token", - modeSource: "config", - token: "change-me-to-a-long-random-token", - allowTailscale: false, - }), - ).toThrow(/example placeholder/i); - }); + it.each(KNOWN_WEAK_GATEWAY_TOKEN_PLACEHOLDERS)( + "throws on the known-weak token sentinel %s", + (token) => { + expect(() => + assertGatewayAuthNotKnownWeak({ + mode: "token", + modeSource: "config", + token, + allowTailscale: false, + }), + ).toThrow(/example placeholder/i); + }, + ); it("throws on the known-weak password sentinel", () => { expect(() => @@ -494,16 +504,19 @@ describe("assertGatewayAuthNotKnownWeak", () => { ).toThrow(/example placeholder/i); }); - it("ignores whitespace-padded placeholder tokens (trimmed match)", () => { - expect(() => - assertGatewayAuthNotKnownWeak({ - mode: "token", - modeSource: "config", - token: " change-me-to-a-long-random-token ", - allowTailscale: false, - }), - ).toThrow(/example placeholder/i); - }); + it.each(KNOWN_WEAK_GATEWAY_TOKEN_PLACEHOLDERS)( + "rejects whitespace-padded placeholder token %s after trimming", + (token) => { + expect(() => + assertGatewayAuthNotKnownWeak({ + mode: "token", + modeSource: "config", + token: ` ${token} `, + allowTailscale: false, + }), + ).toThrow(/example placeholder/i); + }, + ); it("does not throw on an empty token (falls through to generation path)", () => { expect(() => diff --git a/src/gateway/startup-auth.ts b/src/gateway/startup-auth.ts index 1cc94070a8c..8ad3560e486 100644 --- a/src/gateway/startup-auth.ts +++ b/src/gateway/startup-auth.ts @@ -18,6 +18,10 @@ import { hasGatewayTokenEnvCandidate, trimToUndefined, } from "./credentials.js"; +import { + KNOWN_WEAK_GATEWAY_PASSWORD_PLACEHOLDERS, + KNOWN_WEAK_GATEWAY_TOKEN_PLACEHOLDERS, +} from "./known-weak-gateway-secrets.js"; /** * Placeholder credentials that have ever shipped in `.env.example` or been @@ -31,13 +35,13 @@ import { * the example file alone does not protect users who follow an older doc * snippet or copy a tutorial command line. */ -const KNOWN_WEAK_GATEWAY_TOKENS: ReadonlySet = new Set([ - "change-me-to-a-long-random-token", -]); +const KNOWN_WEAK_GATEWAY_TOKENS: ReadonlySet = new Set( + KNOWN_WEAK_GATEWAY_TOKEN_PLACEHOLDERS, +); -const KNOWN_WEAK_GATEWAY_PASSWORDS: ReadonlySet = new Set([ - "change-me-to-a-strong-password", // pragma: allowlist secret -]); +const KNOWN_WEAK_GATEWAY_PASSWORDS: ReadonlySet = new Set( + KNOWN_WEAK_GATEWAY_PASSWORD_PLACEHOLDERS, +); export function mergeGatewayAuthConfig( base?: GatewayAuthConfig, @@ -269,8 +273,8 @@ export function assertGatewayAuthNotKnownWeak(auth: ResolvedGatewayAuth): void { const token = auth.token?.trim() ?? ""; if (token && KNOWN_WEAK_GATEWAY_TOKENS.has(token)) { throw new Error( - "Invalid config: gateway auth token is set to the example placeholder " + - "from .env.example. Generate a real secret (e.g. `openssl rand -hex 32`) " + + "Invalid config: gateway auth token is set to a published example placeholder " + + "from docs or .env.example. Generate a real secret (e.g. `openssl rand -hex 32`) " + "and set OPENCLAW_GATEWAY_TOKEN or gateway.auth.token before starting " + "the gateway.", );