Skip to content

Commit 7cdbb65

Browse files
committed
Gateway/Auth: guard loopback trusted-proxy proxy list
1 parent 9c52497 commit 7cdbb65

File tree

4 files changed

+67
-3
lines changed

4 files changed

+67
-3
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ Docs: https://docs.openclaw.ai
1313

1414
### Fixes
1515

16+
- Gateway/Auth: require `gateway.trustedProxies` to include a loopback proxy address when `auth.mode="trusted-proxy"` and `bind="loopback"`, preventing same-host proxy misconfiguration from silently blocking auth. (follow-up to #20097)
1617
- Gateway/Auth: allow trusted-proxy mode with loopback bind for same-host reverse-proxy deployments, while still requiring configured `gateway.trustedProxies`. (#20097) thanks @xinhuagu.
1718
- Gateway/Auth: allow authenticated clients across roles/scopes to call `health` while preserving role and scope enforcement for non-health methods. (#19699) thanks @Nachx639.
1819
- 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.

docs/gateway/trusted-proxy-auth.md

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -39,8 +39,8 @@ Use `trusted-proxy` auth mode when:
3939
```json5
4040
{
4141
gateway: {
42-
// Must bind to network interface (not loopback)
43-
bind: "lan",
42+
// Use loopback for same-host proxy setups; use lan/custom for remote proxy hosts
43+
bind: "loopback",
4444

4545
// CRITICAL: Only add your proxy's IP(s) here
4646
trustedProxies: ["10.0.0.1", "172.17.0.1"],
@@ -62,6 +62,9 @@ Use `trusted-proxy` auth mode when:
6262
}
6363
```
6464

65+
If `gateway.bind` is `loopback`, include a loopback proxy address in
66+
`gateway.trustedProxies` (`127.0.0.1`, `::1`, or an equivalent loopback CIDR).
67+
6568
### Configuration Reference
6669

6770
| Field | Required | Description |

src/gateway/server-runtime-config.test.ts

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,27 @@ describe("resolveGatewayRuntimeConfig", () => {
5151
expect(result.bindHost).toBe("127.0.0.1");
5252
});
5353

54+
it("should allow loopback trusted-proxy when trustedProxies includes ::1", async () => {
55+
const cfg = {
56+
gateway: {
57+
bind: "loopback" as const,
58+
auth: {
59+
mode: "trusted-proxy" as const,
60+
trustedProxy: {
61+
userHeader: "x-forwarded-user",
62+
},
63+
},
64+
trustedProxies: ["::1"],
65+
},
66+
};
67+
68+
const result = await resolveGatewayRuntimeConfig({
69+
cfg,
70+
port: 18789,
71+
});
72+
expect(result.bindHost).toBe("127.0.0.1");
73+
});
74+
5475
it("should reject loopback trusted-proxy without trustedProxies configured", async () => {
5576
const cfg = {
5677
gateway: {
@@ -75,6 +96,30 @@ describe("resolveGatewayRuntimeConfig", () => {
7596
);
7697
});
7798

99+
it("should reject loopback trusted-proxy when trustedProxies has no loopback address", async () => {
100+
const cfg = {
101+
gateway: {
102+
bind: "loopback" as const,
103+
auth: {
104+
mode: "trusted-proxy" as const,
105+
trustedProxy: {
106+
userHeader: "x-forwarded-user",
107+
},
108+
},
109+
trustedProxies: ["10.0.0.1"],
110+
},
111+
};
112+
113+
await expect(
114+
resolveGatewayRuntimeConfig({
115+
cfg,
116+
port: 18789,
117+
}),
118+
).rejects.toThrow(
119+
"gateway auth mode=trusted-proxy with bind=loopback requires gateway.trustedProxies to include 127.0.0.1, ::1, or a loopback CIDR",
120+
);
121+
});
122+
78123
it("should reject trusted-proxy without trustedProxies configured", async () => {
79124
const cfg = {
80125
gateway: {

src/gateway/server-runtime-config.ts

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,12 @@ import {
1111
} from "./auth.js";
1212
import { normalizeControlUiBasePath } from "./control-ui-shared.js";
1313
import { resolveHooksConfig } from "./hooks.js";
14-
import { isLoopbackHost, isValidIPv4, resolveGatewayBindHost } from "./net.js";
14+
import {
15+
isLoopbackHost,
16+
isTrustedProxyAddress,
17+
isValidIPv4,
18+
resolveGatewayBindHost,
19+
} from "./net.js";
1520
import { mergeGatewayTailscaleConfig } from "./startup-auth.js";
1621

1722
export type GatewayRuntimeConfig = {
@@ -122,6 +127,16 @@ export async function resolveGatewayRuntimeConfig(params: {
122127
"gateway auth mode=trusted-proxy requires gateway.trustedProxies to be configured with at least one proxy IP",
123128
);
124129
}
130+
if (isLoopbackHost(bindHost)) {
131+
const hasLoopbackTrustedProxy =
132+
isTrustedProxyAddress("127.0.0.1", trustedProxies) ||
133+
isTrustedProxyAddress("::1", trustedProxies);
134+
if (!hasLoopbackTrustedProxy) {
135+
throw new Error(
136+
"gateway auth mode=trusted-proxy with bind=loopback requires gateway.trustedProxies to include 127.0.0.1, ::1, or a loopback CIDR",
137+
);
138+
}
139+
}
125140
}
126141

127142
return {

0 commit comments

Comments
 (0)