fix(gateway): trusted-proxy auth rejected when bind=loopback (#20097)

Merged via /review-pr -> /prepare-pr -> /merge-pr.

Prepared head SHA: 8de62f1a8f
Co-authored-by: xinhuagu <562450+xinhuagu@users.noreply.github.com>
Co-authored-by: mbelinky <132747814+mbelinky@users.noreply.github.com>
Reviewed-by: @mbelinky
This commit is contained in:
Xinhua Gu
2026-02-20 18:51:35 +01:00
committed by GitHub
parent 868fe48d58
commit 9c5249714d
5 changed files with 32 additions and 15 deletions

View File

@@ -13,6 +13,7 @@ Docs: https://docs.openclaw.ai
### Fixes
- Gateway/Auth: allow trusted-proxy mode with loopback bind for same-host reverse-proxy deployments, while still requiring configured `gateway.trustedProxies`. (#20097) thanks @xinhuagu.
- Gateway/Auth: allow authenticated clients across roles/scopes to call `health` while preserving role and scope enforcement for non-health methods. (#19699) thanks @Nachx639.
- Gateway/Hooks: include transform export name in hook-transform cache keys so distinct exports from the same module do not reuse the wrong cached transform function. (#13855) thanks @mcaxtr.
- Gateway/Control UI: return 404 for missing static-asset paths instead of serving SPA fallback HTML, while preserving client-route fallback behavior for extensionless and non-asset dotted paths. (#12060) thanks @mcaxtr.

View File

@@ -127,7 +127,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?.bind).toBe("loopback");
expect(result.config.gateway?.trustedProxies).toEqual(["10.0.1.10", "192.168.1.5"]);
});
@@ -141,7 +141,7 @@ 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?.bind).toBe("loopback");
expect(result.config.gateway?.trustedProxies).toEqual(["10.0.0.1"]);
});
@@ -150,7 +150,7 @@ describe("promptGatewayConfig", () => {
tailscaleMode: "serve",
textQueue: ["18789", "x-forwarded-user", "", "", "10.0.0.1"],
});
expect(result.config.gateway?.bind).toBe("lan");
expect(result.config.gateway?.bind).toBe("loopback");
expect(result.config.gateway?.tailscale?.mode).toBe("off");
expect(result.config.gateway?.tailscale?.resetOnExit).toBe(false);
});

View File

@@ -142,10 +142,8 @@ 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";
}
// trusted-proxy + loopback is valid when the reverse proxy runs on the same
// host (e.g. cloudflared, nginx, Caddy). trustedProxies must include 127.0.0.1.
if (authMode === "trusted-proxy" && tailscaleMode !== "off") {
note(
"Trusted proxy auth is incompatible with Tailscale serve/funnel. Disabling Tailscale.",

View File

@@ -30,7 +30,7 @@ describe("resolveGatewayRuntimeConfig", () => {
expect(result.bindHost).toBe("0.0.0.0");
});
it("should reject loopback binding with trusted-proxy auth mode", async () => {
it("should allow loopback binding with trusted-proxy auth mode", async () => {
const cfg = {
gateway: {
bind: "loopback" as const,
@@ -40,7 +40,28 @@ describe("resolveGatewayRuntimeConfig", () => {
userHeader: "x-forwarded-user",
},
},
trustedProxies: ["192.168.1.1"],
trustedProxies: ["127.0.0.1"],
},
};
const result = await resolveGatewayRuntimeConfig({
cfg,
port: 18789,
});
expect(result.bindHost).toBe("127.0.0.1");
});
it("should reject loopback trusted-proxy without trustedProxies configured", async () => {
const cfg = {
gateway: {
bind: "loopback" as const,
auth: {
mode: "trusted-proxy" as const,
trustedProxy: {
userHeader: "x-forwarded-user",
},
},
trustedProxies: [],
},
};
@@ -49,7 +70,9 @@ describe("resolveGatewayRuntimeConfig", () => {
cfg,
port: 18789,
}),
).rejects.toThrow("gateway auth mode=trusted-proxy makes no sense with bind=loopback");
).rejects.toThrow(
"gateway auth mode=trusted-proxy requires gateway.trustedProxies to be configured",
);
});
it("should reject trusted-proxy without trustedProxies configured", async () => {

View File

@@ -117,11 +117,6 @@ export async function resolveGatewayRuntimeConfig(params: {
}
if (authMode === "trusted-proxy") {
if (isLoopbackHost(bindHost)) {
throw new Error(
"gateway auth mode=trusted-proxy makes no sense with bind=loopback; use bind=lan or bind=custom with gateway.trustedProxies configured",
);
}
if (trustedProxies.length === 0) {
throw new Error(
"gateway auth mode=trusted-proxy requires gateway.trustedProxies to be configured with at least one proxy IP",