fix(gateway): require loopback proxy IP for trusted-proxy + bind=loopback (#22082)

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

Prepared head SHA: 6ff3ca9b5db530c2ea4abbd027ee98a9c4a1be67
Co-authored-by: mbelinky <132747814+mbelinky@users.noreply.github.com>
Co-authored-by: mbelinky <132747814+mbelinky@users.noreply.github.com>
Reviewed-by: @mbelinky
This commit is contained in:
Mariano
2026-02-20 18:03:53 +00:00
committed by GitHub
parent 9c5249714d
commit 094dbdaf2b
4 changed files with 67 additions and 3 deletions

View File

@@ -51,6 +51,27 @@ describe("resolveGatewayRuntimeConfig", () => {
expect(result.bindHost).toBe("127.0.0.1");
});
it("should allow loopback trusted-proxy when trustedProxies includes ::1", async () => {
const cfg = {
gateway: {
bind: "loopback" as const,
auth: {
mode: "trusted-proxy" as const,
trustedProxy: {
userHeader: "x-forwarded-user",
},
},
trustedProxies: ["::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: {
@@ -75,6 +96,30 @@ describe("resolveGatewayRuntimeConfig", () => {
);
});
it("should reject loopback trusted-proxy when trustedProxies has no loopback address", async () => {
const cfg = {
gateway: {
bind: "loopback" as const,
auth: {
mode: "trusted-proxy" as const,
trustedProxy: {
userHeader: "x-forwarded-user",
},
},
trustedProxies: ["10.0.0.1"],
},
};
await expect(
resolveGatewayRuntimeConfig({
cfg,
port: 18789,
}),
).rejects.toThrow(
"gateway auth mode=trusted-proxy with bind=loopback requires gateway.trustedProxies to include 127.0.0.1, ::1, or a loopback CIDR",
);
});
it("should reject trusted-proxy without trustedProxies configured", async () => {
const cfg = {
gateway: {

View File

@@ -11,7 +11,12 @@ import {
} from "./auth.js";
import { normalizeControlUiBasePath } from "./control-ui-shared.js";
import { resolveHooksConfig } from "./hooks.js";
import { isLoopbackHost, isValidIPv4, resolveGatewayBindHost } from "./net.js";
import {
isLoopbackHost,
isTrustedProxyAddress,
isValidIPv4,
resolveGatewayBindHost,
} from "./net.js";
import { mergeGatewayTailscaleConfig } from "./startup-auth.js";
export type GatewayRuntimeConfig = {
@@ -122,6 +127,16 @@ export async function resolveGatewayRuntimeConfig(params: {
"gateway auth mode=trusted-proxy requires gateway.trustedProxies to be configured with at least one proxy IP",
);
}
if (isLoopbackHost(bindHost)) {
const hasLoopbackTrustedProxy =
isTrustedProxyAddress("127.0.0.1", trustedProxies) ||
isTrustedProxyAddress("::1", trustedProxies);
if (!hasLoopbackTrustedProxy) {
throw new Error(
"gateway auth mode=trusted-proxy with bind=loopback requires gateway.trustedProxies to include 127.0.0.1, ::1, or a loopback CIDR",
);
}
}
}
return {