mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-10 16:24:58 +00:00
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:
@@ -13,6 +13,7 @@ Docs: https://docs.openclaw.ai
|
|||||||
|
|
||||||
### Fixes
|
### 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/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/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.
|
- 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.
|
||||||
|
|||||||
@@ -127,7 +127,7 @@ describe("promptGatewayConfig", () => {
|
|||||||
requiredHeaders: ["x-forwarded-proto", "x-forwarded-host"],
|
requiredHeaders: ["x-forwarded-proto", "x-forwarded-host"],
|
||||||
allowUsers: ["nick@example.com"],
|
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"]);
|
expect(result.config.gateway?.trustedProxies).toEqual(["10.0.1.10", "192.168.1.5"]);
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -141,7 +141,7 @@ describe("promptGatewayConfig", () => {
|
|||||||
userHeader: "x-remote-user",
|
userHeader: "x-remote-user",
|
||||||
// requiredHeaders and allowUsers should be undefined when empty
|
// 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"]);
|
expect(result.config.gateway?.trustedProxies).toEqual(["10.0.0.1"]);
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -150,7 +150,7 @@ describe("promptGatewayConfig", () => {
|
|||||||
tailscaleMode: "serve",
|
tailscaleMode: "serve",
|
||||||
textQueue: ["18789", "x-forwarded-user", "", "", "10.0.0.1"],
|
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?.mode).toBe("off");
|
||||||
expect(result.config.gateway?.tailscale?.resetOnExit).toBe(false);
|
expect(result.config.gateway?.tailscale?.resetOnExit).toBe(false);
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -142,10 +142,8 @@ export async function promptGatewayConfig(
|
|||||||
authMode = "password";
|
authMode = "password";
|
||||||
}
|
}
|
||||||
|
|
||||||
if (authMode === "trusted-proxy" && bind === "loopback") {
|
// trusted-proxy + loopback is valid when the reverse proxy runs on the same
|
||||||
note("Trusted proxy auth requires network bind. Adjusting bind to lan.", "Note");
|
// host (e.g. cloudflared, nginx, Caddy). trustedProxies must include 127.0.0.1.
|
||||||
bind = "lan";
|
|
||||||
}
|
|
||||||
if (authMode === "trusted-proxy" && tailscaleMode !== "off") {
|
if (authMode === "trusted-proxy" && tailscaleMode !== "off") {
|
||||||
note(
|
note(
|
||||||
"Trusted proxy auth is incompatible with Tailscale serve/funnel. Disabling Tailscale.",
|
"Trusted proxy auth is incompatible with Tailscale serve/funnel. Disabling Tailscale.",
|
||||||
|
|||||||
@@ -30,7 +30,7 @@ describe("resolveGatewayRuntimeConfig", () => {
|
|||||||
expect(result.bindHost).toBe("0.0.0.0");
|
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 = {
|
const cfg = {
|
||||||
gateway: {
|
gateway: {
|
||||||
bind: "loopback" as const,
|
bind: "loopback" as const,
|
||||||
@@ -40,7 +40,28 @@ describe("resolveGatewayRuntimeConfig", () => {
|
|||||||
userHeader: "x-forwarded-user",
|
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,
|
cfg,
|
||||||
port: 18789,
|
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 () => {
|
it("should reject trusted-proxy without trustedProxies configured", async () => {
|
||||||
|
|||||||
@@ -117,11 +117,6 @@ export async function resolveGatewayRuntimeConfig(params: {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (authMode === "trusted-proxy") {
|
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) {
|
if (trustedProxies.length === 0) {
|
||||||
throw new Error(
|
throw new Error(
|
||||||
"gateway auth mode=trusted-proxy requires gateway.trustedProxies to be configured with at least one proxy IP",
|
"gateway auth mode=trusted-proxy requires gateway.trustedProxies to be configured with at least one proxy IP",
|
||||||
|
|||||||
Reference in New Issue
Block a user