diff --git a/src/gateway/auth.test.ts b/src/gateway/auth.test.ts index 1488b438237..f9ad9251437 100644 --- a/src/gateway/auth.test.ts +++ b/src/gateway/auth.test.ts @@ -649,4 +649,46 @@ describe("trusted-proxy auth", () => { expect(res.ok).toBe(true); expect(res.user).toBe("nick@example.com"); }); + + it("allows local direct loopback connections to bypass trusted-proxy auth", async () => { + const res = await authorizeGatewayConnect({ + auth: { + mode: "trusted-proxy", + allowTailscale: false, + trustedProxy: trustedProxyConfig, + }, + connectAuth: null, + trustedProxies: ["10.0.0.1"], + req: { + socket: { remoteAddress: "127.0.0.1" }, + headers: { + host: "127.0.0.1:18789", + }, + } as never, + }); + + expect(res.ok).toBe(true); + expect(res.method).toBe("none"); + }); + + it("does not bypass trusted-proxy auth for non-loopback connections", async () => { + const res = await authorizeGatewayConnect({ + auth: { + mode: "trusted-proxy", + allowTailscale: false, + trustedProxy: trustedProxyConfig, + }, + connectAuth: null, + trustedProxies: ["10.0.0.1"], + req: { + socket: { remoteAddress: "192.168.1.50" }, + headers: { + host: "192.168.1.50:18789", + }, + } as never, + }); + + expect(res.ok).toBe(false); + expect(res.reason).toBe("trusted_proxy_untrusted_source"); + }); }); diff --git a/src/gateway/auth.ts b/src/gateway/auth.ts index dbfac4c8631..36c082e69af 100644 --- a/src/gateway/auth.ts +++ b/src/gateway/auth.ts @@ -380,6 +380,15 @@ export async function authorizeGatewayConnect( ); if (auth.mode === "trusted-proxy") { + // Allow local direct connections (loopback, no proxy headers) to bypass + // trusted-proxy auth. Local CLI commands connect directly to 127.0.0.1 + // without going through a reverse proxy, so they lack proxy identity + // headers. This is safe because only processes on the same machine can + // reach the loopback interface. + if (localDirect) { + return { ok: true, method: "none" }; + } + if (!auth.trustedProxy) { return { ok: false, reason: "trusted_proxy_config_missing" }; }