mirror of
https://github.com/openclaw/openclaw.git
synced 2026-02-19 18:39:20 -05:00
fix: harden trusted-proxy auth follow-ups
This commit is contained in:
@@ -16,6 +16,7 @@ Docs: https://docs.openclaw.ai
|
||||
|
||||
### Fixes
|
||||
|
||||
- Gateway/Auth: add trusted-proxy mode hardening follow-ups by keeping `OPENCLAW_GATEWAY_*` env compatibility, auto-normalizing invalid setup combinations in interactive `gateway configure` (trusted-proxy forces `bind=lan` and disables Tailscale serve/funnel), and suppressing shared-secret/rate-limit audit findings that do not apply to trusted-proxy deployments. (#15940) Thanks @nickytonline.
|
||||
- Feishu: stop persistent Typing reaction on NO_REPLY/suppressed runs by wiring reply-dispatcher cleanup to remove typing indicators. (#15464) Thanks @arosstale.
|
||||
- BlueBubbles: gracefully degrade when Private API is disabled by filtering private-only actions, skipping private-only reactions/reply effects, and avoiding private reply markers so non-private flows remain usable. (#16002) Thanks @L-U-C-K-Y.
|
||||
- Outbound: add a write-ahead delivery queue with crash-recovery retries to prevent lost outbound messages after gateway restarts. (#15636) Thanks @nabbilkhan, @thewilloftheshadow.
|
||||
|
||||
@@ -133,6 +133,7 @@ describe("promptGatewayConfig", () => {
|
||||
requiredHeaders: ["x-forwarded-proto", "x-forwarded-host"],
|
||||
allowUsers: ["nick@example.com"],
|
||||
});
|
||||
expect(result.config.gateway?.bind).toBe("lan");
|
||||
expect(result.config.gateway?.trustedProxies).toEqual(["10.0.1.10", "192.168.1.5"]);
|
||||
});
|
||||
|
||||
@@ -163,6 +164,32 @@ describe("promptGatewayConfig", () => {
|
||||
userHeader: "x-remote-user",
|
||||
// requiredHeaders and allowUsers should be undefined when empty
|
||||
});
|
||||
expect(result.config.gateway?.bind).toBe("lan");
|
||||
expect(result.config.gateway?.trustedProxies).toEqual(["10.0.0.1"]);
|
||||
});
|
||||
|
||||
it("forces tailscale off when trusted-proxy is selected", async () => {
|
||||
vi.clearAllMocks();
|
||||
mocks.resolveGatewayPort.mockReturnValue(18789);
|
||||
const selectQueue = ["loopback", "trusted-proxy", "serve"];
|
||||
mocks.select.mockImplementation(async () => selectQueue.shift());
|
||||
const textQueue = ["18789", "x-forwarded-user", "", "", "10.0.0.1"];
|
||||
mocks.text.mockImplementation(async () => textQueue.shift());
|
||||
mocks.confirm.mockResolvedValue(true);
|
||||
mocks.buildGatewayAuthConfig.mockImplementation(({ mode, trustedProxy }) => ({
|
||||
mode,
|
||||
trustedProxy,
|
||||
}));
|
||||
|
||||
const runtime: RuntimeEnv = {
|
||||
log: vi.fn(),
|
||||
error: vi.fn(),
|
||||
exit: vi.fn(),
|
||||
};
|
||||
|
||||
const result = await promptGatewayConfig({}, runtime);
|
||||
expect(result.config.gateway?.bind).toBe("lan");
|
||||
expect(result.config.gateway?.tailscale?.mode).toBe("off");
|
||||
expect(result.config.gateway?.tailscale?.resetOnExit).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -114,7 +114,7 @@ export async function promptGatewayConfig(
|
||||
runtime,
|
||||
) as GatewayAuthChoice;
|
||||
|
||||
const tailscaleMode = guardCancel(
|
||||
let tailscaleMode = guardCancel(
|
||||
await select({
|
||||
message: "Tailscale exposure",
|
||||
options: [
|
||||
@@ -180,6 +180,19 @@ export async function promptGatewayConfig(
|
||||
authMode = "password";
|
||||
}
|
||||
|
||||
if (authMode === "trusted-proxy" && bind === "loopback") {
|
||||
note("Trusted proxy auth requires network bind. Adjusting bind to lan.", "Note");
|
||||
bind = "lan";
|
||||
}
|
||||
if (authMode === "trusted-proxy" && tailscaleMode !== "off") {
|
||||
note(
|
||||
"Trusted proxy auth is incompatible with Tailscale serve/funnel. Disabling Tailscale.",
|
||||
"Note",
|
||||
);
|
||||
tailscaleMode = "off";
|
||||
tailscaleResetOnExit = false;
|
||||
}
|
||||
|
||||
let gatewayToken: string | undefined;
|
||||
let gatewayPassword: string | undefined;
|
||||
let trustedProxyConfig:
|
||||
@@ -218,7 +231,7 @@ export async function promptGatewayConfig(
|
||||
"Only requests from specified proxy IPs will be trusted.",
|
||||
"",
|
||||
"Common use cases: Pomerium, Caddy + OAuth, Traefik + forward auth",
|
||||
"Docs: https://docs.openclaw.ai/gateway/trusted-proxy",
|
||||
"Docs: https://docs.openclaw.ai/gateway/trusted-proxy-auth",
|
||||
].join("\n"),
|
||||
"Trusted Proxy Auth",
|
||||
);
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { describe, expect, it, vi } from "vitest";
|
||||
import type { AuthRateLimiter } from "./auth-rate-limit.js";
|
||||
import { authorizeGatewayConnect } from "./auth.js";
|
||||
import { authorizeGatewayConnect, resolveGatewayAuth } from "./auth.js";
|
||||
|
||||
function createLimiterSpy(): AuthRateLimiter & {
|
||||
check: ReturnType<typeof vi.fn>;
|
||||
@@ -18,6 +18,38 @@ function createLimiterSpy(): AuthRateLimiter & {
|
||||
}
|
||||
|
||||
describe("gateway auth", () => {
|
||||
it("resolves token/password from OPENCLAW gateway env vars", () => {
|
||||
expect(
|
||||
resolveGatewayAuth({
|
||||
authConfig: {},
|
||||
env: {
|
||||
OPENCLAW_GATEWAY_TOKEN: "env-token",
|
||||
OPENCLAW_GATEWAY_PASSWORD: "env-password",
|
||||
} as NodeJS.ProcessEnv,
|
||||
}),
|
||||
).toMatchObject({
|
||||
mode: "password",
|
||||
token: "env-token",
|
||||
password: "env-password",
|
||||
});
|
||||
});
|
||||
|
||||
it("does not resolve legacy CLAWDBOT gateway env vars", () => {
|
||||
expect(
|
||||
resolveGatewayAuth({
|
||||
authConfig: {},
|
||||
env: {
|
||||
CLAWDBOT_GATEWAY_TOKEN: "legacy-token",
|
||||
CLAWDBOT_GATEWAY_PASSWORD: "legacy-password",
|
||||
} as NodeJS.ProcessEnv,
|
||||
}),
|
||||
).toMatchObject({
|
||||
mode: "none",
|
||||
token: undefined,
|
||||
password: undefined,
|
||||
});
|
||||
});
|
||||
|
||||
it("does not throw when req is missing socket", async () => {
|
||||
const res = await authorizeGatewayConnect({
|
||||
auth: { mode: "token", token: "secret", allowTailscale: false },
|
||||
|
||||
@@ -197,8 +197,8 @@ export function resolveGatewayAuth(params: {
|
||||
}): ResolvedGatewayAuth {
|
||||
const authConfig = params.authConfig ?? {};
|
||||
const env = params.env ?? process.env;
|
||||
const token = authConfig.token ?? env.CLAWDBOT_GATEWAY_TOKEN ?? undefined;
|
||||
const password = authConfig.password ?? env.CLAWDBOT_GATEWAY_PASSWORD ?? undefined;
|
||||
const token = authConfig.token ?? env.OPENCLAW_GATEWAY_TOKEN ?? undefined;
|
||||
const password = authConfig.password ?? env.OPENCLAW_GATEWAY_PASSWORD ?? undefined;
|
||||
const trustedProxy = authConfig.trustedProxy;
|
||||
|
||||
let mode: ResolvedGatewayAuth["mode"];
|
||||
|
||||
@@ -96,13 +96,13 @@ describe("security audit", () => {
|
||||
|
||||
it("flags non-loopback bind without auth as critical", async () => {
|
||||
// Clear env tokens so resolveGatewayAuth defaults to mode=none
|
||||
const prevToken = process.env.CLAWDBOT_GATEWAY_TOKEN;
|
||||
const prevPassword = process.env.CLAWDBOT_GATEWAY_PASSWORD;
|
||||
delete process.env.CLAWDBOT_GATEWAY_TOKEN;
|
||||
delete process.env.CLAWDBOT_GATEWAY_PASSWORD;
|
||||
const prevToken = process.env.OPENCLAW_GATEWAY_TOKEN;
|
||||
const prevPassword = process.env.OPENCLAW_GATEWAY_PASSWORD;
|
||||
delete process.env.OPENCLAW_GATEWAY_TOKEN;
|
||||
delete process.env.OPENCLAW_GATEWAY_PASSWORD;
|
||||
|
||||
try {
|
||||
const cfg: ClawdbotConfig = {
|
||||
const cfg: OpenClawConfig = {
|
||||
gateway: {
|
||||
bind: "lan",
|
||||
auth: {},
|
||||
@@ -121,14 +121,14 @@ describe("security audit", () => {
|
||||
} finally {
|
||||
// Restore env
|
||||
if (prevToken === undefined) {
|
||||
delete process.env.CLAWDBOT_GATEWAY_TOKEN;
|
||||
delete process.env.OPENCLAW_GATEWAY_TOKEN;
|
||||
} else {
|
||||
process.env.CLAWDBOT_GATEWAY_TOKEN = prevToken;
|
||||
process.env.OPENCLAW_GATEWAY_TOKEN = prevToken;
|
||||
}
|
||||
if (prevPassword === undefined) {
|
||||
delete process.env.CLAWDBOT_GATEWAY_PASSWORD;
|
||||
delete process.env.OPENCLAW_GATEWAY_PASSWORD;
|
||||
} else {
|
||||
process.env.CLAWDBOT_GATEWAY_PASSWORD = prevPassword;
|
||||
process.env.OPENCLAW_GATEWAY_PASSWORD = prevPassword;
|
||||
}
|
||||
}
|
||||
});
|
||||
@@ -612,8 +612,8 @@ describe("security audit", () => {
|
||||
);
|
||||
});
|
||||
|
||||
it("flags trusted-proxy auth mode as critical warning", async () => {
|
||||
const cfg: ClawdbotConfig = {
|
||||
it("flags trusted-proxy auth mode without generic shared-secret findings", async () => {
|
||||
const cfg: OpenClawConfig = {
|
||||
gateway: {
|
||||
bind: "lan",
|
||||
trustedProxies: ["10.0.0.1"],
|
||||
@@ -640,10 +640,12 @@ describe("security audit", () => {
|
||||
}),
|
||||
]),
|
||||
);
|
||||
expect(res.findings.some((f) => f.checkId === "gateway.bind_no_auth")).toBe(false);
|
||||
expect(res.findings.some((f) => f.checkId === "gateway.auth_no_rate_limit")).toBe(false);
|
||||
});
|
||||
|
||||
it("flags trusted-proxy auth without trustedProxies configured", async () => {
|
||||
const cfg: ClawdbotConfig = {
|
||||
const cfg: OpenClawConfig = {
|
||||
gateway: {
|
||||
bind: "lan",
|
||||
trustedProxies: [],
|
||||
@@ -673,7 +675,7 @@ describe("security audit", () => {
|
||||
});
|
||||
|
||||
it("flags trusted-proxy auth without userHeader configured", async () => {
|
||||
const cfg: ClawdbotConfig = {
|
||||
const cfg: OpenClawConfig = {
|
||||
gateway: {
|
||||
bind: "lan",
|
||||
trustedProxies: ["10.0.0.1"],
|
||||
@@ -701,7 +703,7 @@ describe("security audit", () => {
|
||||
});
|
||||
|
||||
it("warns when trusted-proxy auth allows all users", async () => {
|
||||
const cfg: ClawdbotConfig = {
|
||||
const cfg: OpenClawConfig = {
|
||||
gateway: {
|
||||
bind: "lan",
|
||||
trustedProxies: ["10.0.0.1"],
|
||||
|
||||
@@ -258,7 +258,7 @@ function collectGatewayConfigFindings(
|
||||
(auth.mode === "token" && hasToken) || (auth.mode === "password" && hasPassword);
|
||||
const hasTailscaleAuth = auth.allowTailscale && tailscaleMode === "serve";
|
||||
const hasGatewayAuth = hasSharedSecret || hasTailscaleAuth;
|
||||
if (bind !== "loopback" && !hasSharedSecret) {
|
||||
if (bind !== "loopback" && !hasSharedSecret && auth.mode !== "trusted-proxy") {
|
||||
findings.push({
|
||||
checkId: "gateway.bind_no_auth",
|
||||
severity: "critical",
|
||||
@@ -403,7 +403,7 @@ function collectGatewayConfigFindings(
|
||||
}
|
||||
}
|
||||
|
||||
if (bind !== "loopback" && !cfg.gateway?.auth?.rateLimit) {
|
||||
if (bind !== "loopback" && auth.mode !== "trusted-proxy" && !cfg.gateway?.auth?.rateLimit) {
|
||||
findings.push({
|
||||
checkId: "gateway.auth_no_rate_limit",
|
||||
severity: "warn",
|
||||
|
||||
Reference in New Issue
Block a user