mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-06 06:20:43 +00:00
fix: harden published gateway secret placeholders
This commit is contained in:
@@ -67,6 +67,7 @@ Docs: https://docs.openclaw.ai
|
||||
- Agents/Google: strip `thinkingBudget=0` for the thinking-required `gemini-2.5-pro` model in embedded-runner and native Google payloads, so requests no longer fail with `Budget 0 is invalid. This model only works in thinking mode.` and the API uses its default thinking behavior instead. (#68607) Thanks @josmithiii.
|
||||
- Slack/threads: log failed thread starter and history fetches at verbose level while preserving best-effort fallback behavior, so missing Slack thread context is diagnosable without interrupting inbound handling. (#68594) Thanks @martingarramon.
|
||||
- Gateway/restart: keep stale-gateway cleanup from terminating the current process's parent or ancestors, so plugin sidecars like WeChat no longer kill the active gateway and trigger an infinite supervisor restart loop. Fixes #68451. (#68517) Thanks @openperf.
|
||||
- Gateway/auth: reject gateway auth credentials that match published example placeholders at startup and secret reload, and keep cloud install snippets from publishing copy-paste gateway/keyring secrets. (#68404) Thanks @coygeek.
|
||||
|
||||
## 2026.4.15
|
||||
|
||||
|
||||
@@ -220,11 +220,14 @@ For the generic Docker flow, see [Docker](/install/docker).
|
||||
OPENCLAW_CONFIG_DIR=/home/$USER/.openclaw
|
||||
OPENCLAW_WORKSPACE_DIR=/home/$USER/.openclaw/workspace
|
||||
|
||||
GOG_KEYRING_PASSWORD=change-me-now
|
||||
GOG_KEYRING_PASSWORD=
|
||||
XDG_CONFIG_HOME=/home/node/.openclaw
|
||||
```
|
||||
|
||||
Generate strong secrets:
|
||||
Leave `OPENCLAW_GATEWAY_TOKEN` blank unless you explicitly want to
|
||||
manage it through `.env`; OpenClaw writes a random gateway token to
|
||||
config on first start. Generate a keyring password and paste it into
|
||||
`GOG_KEYRING_PASSWORD`:
|
||||
|
||||
```bash
|
||||
openssl rand -hex 32
|
||||
|
||||
@@ -141,11 +141,14 @@ For the generic Docker flow, see [Docker](/install/docker).
|
||||
OPENCLAW_CONFIG_DIR=/root/.openclaw
|
||||
OPENCLAW_WORKSPACE_DIR=/root/.openclaw/workspace
|
||||
|
||||
GOG_KEYRING_PASSWORD=change-me-now
|
||||
GOG_KEYRING_PASSWORD=
|
||||
XDG_CONFIG_HOME=/home/node/.openclaw
|
||||
```
|
||||
|
||||
Generate strong secrets:
|
||||
Leave `OPENCLAW_GATEWAY_TOKEN` blank unless you explicitly want to
|
||||
manage it through `.env`; OpenClaw writes a random gateway token to
|
||||
config on first start. Generate a keyring password and paste it into
|
||||
`GOG_KEYRING_PASSWORD`:
|
||||
|
||||
```bash
|
||||
openssl rand -hex 32
|
||||
|
||||
@@ -1,21 +1,42 @@
|
||||
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";
|
||||
import {
|
||||
KNOWN_WEAK_GATEWAY_PASSWORD_PLACEHOLDERS,
|
||||
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;
|
||||
const CLOUD_DOCKER_VM_INSTALL_DOCS = new Set(["gcp.md", "hetzner.md"]);
|
||||
|
||||
async function readInstallDocs(): Promise<Array<{ docName: string; markdown: string }>> {
|
||||
const entries = await fs.readdir(INSTALL_DOCS_DIR, { withFileTypes: true });
|
||||
return await Promise.all(
|
||||
entries
|
||||
.filter((entry) => entry.isFile() && entry.name.endsWith(".md"))
|
||||
.toSorted((a, b) => a.name.localeCompare(b.name))
|
||||
.map(async (entry) => ({
|
||||
docName: entry.name,
|
||||
markdown: await fs.readFile(path.join(INSTALL_DOCS_DIR, entry.name), "utf8"),
|
||||
})),
|
||||
);
|
||||
}
|
||||
|
||||
describe("cloud install docs", () => {
|
||||
it("does not publish a copy-paste gateway token placeholder", async () => {
|
||||
for (const docName of CLOUD_INSTALL_DOCS) {
|
||||
const markdown = await fs.readFile(path.join(INSTALL_DOCS_DIR, docName), "utf8");
|
||||
|
||||
for (const { docName, markdown } of await readInstallDocs()) {
|
||||
for (const token of KNOWN_WEAK_GATEWAY_TOKEN_PLACEHOLDERS) {
|
||||
expect(markdown).not.toContain(`OPENCLAW_GATEWAY_TOKEN=${token}`);
|
||||
expect(markdown, docName).not.toContain(`OPENCLAW_GATEWAY_TOKEN=${token}`);
|
||||
}
|
||||
for (const password of KNOWN_WEAK_GATEWAY_PASSWORD_PLACEHOLDERS) {
|
||||
expect(markdown, docName).not.toContain(`OPENCLAW_GATEWAY_PASSWORD=${password}`);
|
||||
}
|
||||
expect(markdown, docName).not.toMatch(/^ GOG_KEYRING_PASSWORD=change-me-now$/m);
|
||||
if (CLOUD_DOCKER_VM_INSTALL_DOCS.has(docName)) {
|
||||
expect(markdown, docName).toMatch(/^ OPENCLAW_GATEWAY_TOKEN=[ \t]*\r?$/m);
|
||||
expect(markdown, docName).toMatch(/^ GOG_KEYRING_PASSWORD=[ \t]*\r?$/m);
|
||||
expect(markdown, docName).toContain("openssl rand -hex 32");
|
||||
}
|
||||
expect(markdown).toMatch(/OPENCLAW_GATEWAY_TOKEN=[ \t]*\r?\n/);
|
||||
expect(markdown).toContain("openssl rand -hex 32");
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,6 +1,49 @@
|
||||
import type { ResolvedGatewayAuth } from "./auth.js";
|
||||
|
||||
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;
|
||||
|
||||
/**
|
||||
* Placeholder credentials that have ever shipped in `.env.example` or been
|
||||
* used as copy-paste examples in onboarding docs. If any of these ever
|
||||
* becomes the resolved gateway credential, reject it. The operator almost
|
||||
* certainly copied an example file verbatim without replacing the sentinel,
|
||||
* which would otherwise leave the gateway protected by a publicly-known
|
||||
* credential.
|
||||
*/
|
||||
const KNOWN_WEAK_GATEWAY_TOKENS: ReadonlySet<string> = new Set(
|
||||
KNOWN_WEAK_GATEWAY_TOKEN_PLACEHOLDERS,
|
||||
);
|
||||
|
||||
const KNOWN_WEAK_GATEWAY_PASSWORDS: ReadonlySet<string> = new Set(
|
||||
KNOWN_WEAK_GATEWAY_PASSWORD_PLACEHOLDERS,
|
||||
);
|
||||
|
||||
export function assertGatewayAuthNotKnownWeak(auth: ResolvedGatewayAuth): void {
|
||||
if (auth.mode === "token") {
|
||||
const token = auth.token?.trim() ?? "";
|
||||
if (token && KNOWN_WEAK_GATEWAY_TOKENS.has(token)) {
|
||||
throw new Error(
|
||||
"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.",
|
||||
);
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (auth.mode === "password") {
|
||||
const password = auth.password?.trim() ?? "";
|
||||
if (password && KNOWN_WEAK_GATEWAY_PASSWORDS.has(password)) {
|
||||
throw new Error(
|
||||
"Invalid config: gateway auth password is set to the example placeholder " +
|
||||
"from .env.example. Choose a real password and set OPENCLAW_GATEWAY_PASSWORD " +
|
||||
"or gateway.auth.password before starting the gateway.",
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { afterEach, describe, expect, it, vi } from "vitest";
|
||||
import type { ConfigFileSnapshot, OpenClawConfig } from "../config/types.js";
|
||||
import type { PreparedSecretsRuntimeSnapshot, SecretResolverWarning } from "../secrets/runtime.js";
|
||||
import { KNOWN_WEAK_GATEWAY_TOKEN_PLACEHOLDERS } from "./known-weak-gateway-secrets.js";
|
||||
import {
|
||||
createRuntimeSecretsActivator,
|
||||
prepareGatewayStartupConfig,
|
||||
@@ -157,6 +158,56 @@ describe("gateway startup config secret preflight", () => {
|
||||
expect(emitStateEvent).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it.each(KNOWN_WEAK_GATEWAY_TOKEN_PLACEHOLDERS)(
|
||||
"rejects known weak gateway tokens resolved during secret activation: %s",
|
||||
async (token) => {
|
||||
const sourceConfig = gatewayTokenConfig({
|
||||
secrets: {
|
||||
providers: {
|
||||
default: { source: "env" },
|
||||
},
|
||||
},
|
||||
gateway: {
|
||||
auth: {
|
||||
mode: "token",
|
||||
token: { source: "env", provider: "default", id: "GATEWAY_TOKEN_REF" },
|
||||
},
|
||||
},
|
||||
});
|
||||
const prepareRuntimeSecretsSnapshot = vi.fn(async () =>
|
||||
preparedSnapshot({
|
||||
...sourceConfig,
|
||||
gateway: {
|
||||
...sourceConfig.gateway,
|
||||
auth: {
|
||||
...sourceConfig.gateway?.auth,
|
||||
token,
|
||||
},
|
||||
},
|
||||
}),
|
||||
);
|
||||
const activateRuntimeSecretsSnapshot = vi.fn();
|
||||
const activateRuntimeSecrets = createRuntimeSecretsActivator({
|
||||
logSecrets: {
|
||||
info: vi.fn(),
|
||||
warn: vi.fn(),
|
||||
error: vi.fn(),
|
||||
},
|
||||
emitStateEvent: vi.fn(),
|
||||
prepareRuntimeSecretsSnapshot,
|
||||
activateRuntimeSecretsSnapshot,
|
||||
});
|
||||
|
||||
await expect(
|
||||
activateRuntimeSecrets(sourceConfig, {
|
||||
reason: "reload",
|
||||
activate: true,
|
||||
}),
|
||||
).rejects.toThrow(/published example placeholder/);
|
||||
expect(activateRuntimeSecretsSnapshot).not.toHaveBeenCalled();
|
||||
},
|
||||
);
|
||||
|
||||
it("prunes channel refs from startup secret preflight when channels are skipped", async () => {
|
||||
process.env.OPENCLAW_SKIP_CHANNELS = "1";
|
||||
const prepareRuntimeSecretsSnapshot = vi.fn(async ({ config }) => preparedSnapshot(config));
|
||||
|
||||
@@ -20,6 +20,8 @@ import {
|
||||
activateSecretsRuntimeSnapshot,
|
||||
prepareSecretsRuntimeSnapshot,
|
||||
} from "../secrets/runtime.js";
|
||||
import { resolveGatewayAuth } from "./auth.js";
|
||||
import { assertGatewayAuthNotKnownWeak } from "./known-weak-gateway-secrets.js";
|
||||
import {
|
||||
ensureGatewayStartupAuth,
|
||||
mergeGatewayAuthConfig,
|
||||
@@ -114,6 +116,7 @@ export function createRuntimeSecretsActivator(params: {
|
||||
const prepared = await prepareRuntimeSecretsSnapshot({
|
||||
config: pruneSkippedStartupSecretSurfaces(config),
|
||||
});
|
||||
assertRuntimeGatewayAuthNotKnownWeak(prepared.config);
|
||||
if (activationParams.activate) {
|
||||
activateRuntimeSecretsSnapshot(prepared);
|
||||
logGatewayAuthSurfaceDiagnostics(prepared, params.logSecrets);
|
||||
@@ -241,6 +244,16 @@ function pruneSkippedStartupSecretSurfaces(config: OpenClawConfig): OpenClawConf
|
||||
};
|
||||
}
|
||||
|
||||
function assertRuntimeGatewayAuthNotKnownWeak(config: OpenClawConfig): void {
|
||||
assertGatewayAuthNotKnownWeak(
|
||||
resolveGatewayAuth({
|
||||
authConfig: config.gateway?.auth,
|
||||
env: process.env,
|
||||
tailscaleMode: config.gateway?.tailscale?.mode ?? "off",
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
function logGatewayAuthSurfaceDiagnostics(
|
||||
prepared: {
|
||||
sourceConfig: OpenClawConfig;
|
||||
|
||||
@@ -18,30 +18,9 @@ import {
|
||||
hasGatewayTokenEnvCandidate,
|
||||
trimToUndefined,
|
||||
} from "./credentials.js";
|
||||
import {
|
||||
KNOWN_WEAK_GATEWAY_PASSWORD_PLACEHOLDERS,
|
||||
KNOWN_WEAK_GATEWAY_TOKEN_PLACEHOLDERS,
|
||||
} from "./known-weak-gateway-secrets.js";
|
||||
import { assertGatewayAuthNotKnownWeak } from "./known-weak-gateway-secrets.js";
|
||||
|
||||
/**
|
||||
* Placeholder credentials that have ever shipped in `.env.example` or been
|
||||
* used as copy-paste examples in onboarding docs. If any of these ever
|
||||
* becomes the resolved gateway credential at startup, reject the launch —
|
||||
* the operator almost certainly copied an example file verbatim without
|
||||
* replacing the sentinel, which would otherwise leave the gateway protected
|
||||
* by a publicly-known credential.
|
||||
*
|
||||
* This is a belt-and-suspenders complement to keeping `.env.example` blank:
|
||||
* 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<string> = new Set(
|
||||
KNOWN_WEAK_GATEWAY_TOKEN_PLACEHOLDERS,
|
||||
);
|
||||
|
||||
const KNOWN_WEAK_GATEWAY_PASSWORDS: ReadonlySet<string> = new Set(
|
||||
KNOWN_WEAK_GATEWAY_PASSWORD_PLACEHOLDERS,
|
||||
);
|
||||
export { assertGatewayAuthNotKnownWeak } from "./known-weak-gateway-secrets.js";
|
||||
|
||||
export function mergeGatewayAuthConfig(
|
||||
base?: GatewayAuthConfig,
|
||||
@@ -268,31 +247,6 @@ export async function ensureGatewayStartupAuth(params: {
|
||||
};
|
||||
}
|
||||
|
||||
export function assertGatewayAuthNotKnownWeak(auth: ResolvedGatewayAuth): void {
|
||||
if (auth.mode === "token") {
|
||||
const token = auth.token?.trim() ?? "";
|
||||
if (token && KNOWN_WEAK_GATEWAY_TOKENS.has(token)) {
|
||||
throw new Error(
|
||||
"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.",
|
||||
);
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (auth.mode === "password") {
|
||||
const password = auth.password?.trim() ?? "";
|
||||
if (password && KNOWN_WEAK_GATEWAY_PASSWORDS.has(password)) {
|
||||
throw new Error(
|
||||
"Invalid config: gateway auth password is set to the example placeholder " +
|
||||
"from .env.example. Choose a real password and set OPENCLAW_GATEWAY_PASSWORD " +
|
||||
"or gateway.auth.password before starting the gateway.",
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export function assertHooksTokenSeparateFromGatewayAuth(params: {
|
||||
cfg: OpenClawConfig;
|
||||
auth: ResolvedGatewayAuth;
|
||||
|
||||
Reference in New Issue
Block a user