From 7975305a893b66a1bfa5d7ce975c86d261dac1a6 Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Mon, 27 Apr 2026 22:06:06 +0100 Subject: [PATCH] test: cover trusted-proxy secret surfaces --- src/gateway/credential-planner.ts | 3 +- src/gateway/credentials.test.ts | 22 +++++++++++ src/gateway/credentials.ts | 4 +- .../runtime-gateway-auth-surfaces.test.ts | 37 +++++++++++++++++++ .../runtime-gateway-local-surfaces.test.ts | 18 ++++++++- 5 files changed, 80 insertions(+), 4 deletions(-) diff --git a/src/gateway/credential-planner.ts b/src/gateway/credential-planner.ts index 6933506efee..2aee7adfa43 100644 --- a/src/gateway/credential-planner.ts +++ b/src/gateway/credential-planner.ts @@ -139,7 +139,8 @@ export function createGatewayCredentialPlan(params: { gateway?.tailscale?.mode === "serve" || gateway?.tailscale?.mode === "funnel"; const remoteConfiguredSurface = remoteMode || remoteUrlConfigured || tailscaleRemoteExposure; const remoteTokenFallbackActive = localTokenCanWin && !envToken && !localToken.configured; - const remotePasswordFallbackActive = !envPassword && !localPassword.configured && passwordCanWin; + const remotePasswordFallbackActive = + authMode !== "trusted-proxy" && !envPassword && !localPassword.configured && passwordCanWin; return { configuredMode: gateway?.mode === "remote" ? "remote" : "local", diff --git a/src/gateway/credentials.test.ts b/src/gateway/credentials.test.ts index ef7b9560335..a64026f5bd0 100644 --- a/src/gateway/credentials.test.ts +++ b/src/gateway/credentials.test.ts @@ -309,6 +309,28 @@ describe("resolveGatewayCredentialsFromConfig", () => { }); }); + it("does not use remote password as trusted-proxy local fallback", () => { + const resolved = resolveGatewayCredentialsFromConfig({ + cfg: cfg({ + gateway: { + mode: "local", + auth: { + mode: "trusted-proxy", + }, + remote: { + password: "remote-password", // pragma: allowlist secret + }, + }, + }), + env: {} as NodeJS.ProcessEnv, + }); + + expect(resolved).toEqual({ + token: undefined, + password: undefined, + }); + }); + it("keeps local credentials ahead of remote fallback in local mode", () => { const resolved = resolveGatewayCredentialsFromConfig({ cfg: cfg({ diff --git a/src/gateway/credentials.ts b/src/gateway/credentials.ts index a95f791635b..c1d066b5c1d 100644 --- a/src/gateway/credentials.ts +++ b/src/gateway/credentials.ts @@ -110,7 +110,9 @@ function resolveLocalGatewayCredentials(params: { : params.plan.remoteToken.value; const fallbackPassword = params.plan.localPassword.configured ? params.plan.localPassword.value - : params.plan.remotePassword.value; + : params.plan.authMode === "trusted-proxy" + ? undefined + : params.plan.remotePassword.value; const localResolved = resolveGatewayCredentialsFromValues({ configToken: fallbackToken, configPassword: fallbackPassword, diff --git a/src/secrets/runtime-gateway-auth-surfaces.test.ts b/src/secrets/runtime-gateway-auth-surfaces.test.ts index efedd53b360..373645b107e 100644 --- a/src/secrets/runtime-gateway-auth-surfaces.test.ts +++ b/src/secrets/runtime-gateway-auth-surfaces.test.ts @@ -87,6 +87,23 @@ describe("evaluateGatewayAuthSurfaceStates", () => { }); }); + it("marks gateway.auth.password active when trusted-proxy mode is explicit", () => { + const states = evaluate({ + gateway: { + auth: { + mode: "trusted-proxy", + password: envRef("GW_AUTH_PASSWORD"), + }, + }, + } as OpenClawConfig); + + expect(states["gateway.auth.password"]).toMatchObject({ + hasSecretRef: true, + active: true, + reason: "no token source can win, so password auth can win.", + }); + }); + it("marks gateway.auth.password inactive when env token is configured", () => { const states = evaluate( { @@ -197,4 +214,24 @@ describe("evaluateGatewayAuthSurfaceStates", () => { reason: 'password auth cannot win with gateway.auth.mode="token".', }); }); + + it("marks gateway.remote.password inactive as a trusted-proxy local fallback", () => { + const states = evaluate({ + gateway: { + mode: "local", + auth: { + mode: "trusted-proxy", + }, + remote: { + password: envRef("GW_REMOTE_PASSWORD"), + }, + }, + } as OpenClawConfig); + + expect(states["gateway.remote.password"]).toMatchObject({ + hasSecretRef: true, + active: false, + reason: "remote password fallback is not active.", + }); + }); }); diff --git a/src/secrets/runtime-gateway-local-surfaces.test.ts b/src/secrets/runtime-gateway-local-surfaces.test.ts index 69dd0ccc38b..26c7e605421 100644 --- a/src/secrets/runtime-gateway-local-surfaces.test.ts +++ b/src/secrets/runtime-gateway-local-surfaces.test.ts @@ -19,6 +19,20 @@ async function expectInactiveGatewayPassword(config: unknown): Promise { expect(snapshot.warnings.map((warning) => warning.path)).toContain("gateway.auth.password"); } +async function expectActiveGatewayPassword(config: unknown): Promise { + const snapshot = await prepareSecretsRuntimeSnapshot({ + config: asConfig(config), + env: { + GATEWAY_PASSWORD_REF: "resolved-gateway-password", + }, + agentDirs: ["/tmp/openclaw-agent-main"], + loadAuthStore: () => ({ version: 1, profiles: {} }), + }); + + expect(snapshot.config.gateway?.auth?.password).toBe("resolved-gateway-password"); + expect(snapshot.warnings.map((warning) => warning.path)).not.toContain("gateway.auth.password"); +} + describe("secrets runtime gateway local surfaces", () => { it("treats gateway.remote refs as inactive when local auth credentials are configured", async () => { const snapshot = await prepareSecretsRuntimeSnapshot({ @@ -139,8 +153,8 @@ describe("secrets runtime gateway local surfaces", () => { ).rejects.toThrow(/MISSING_GATEWAY_TOKEN_REF/); }); - it("treats gateway.auth.password ref as inactive when auth mode is trusted-proxy", async () => { - await expectInactiveGatewayPassword({ + it("treats gateway.auth.password ref as active when auth mode is trusted-proxy", async () => { + await expectActiveGatewayPassword({ gateway: { auth: { mode: "trusted-proxy",