Skip to content

Commit ef573d3

Browse files
Gateway: fail closed for loopback trusted-proxy auth
1 parent 6757878 commit ef573d3

2 files changed

Lines changed: 51 additions & 60 deletions

File tree

src/gateway/auth.test.ts

Lines changed: 50 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -721,6 +721,22 @@ describe("trusted-proxy auth", () => {
721721
expect(() => assertGatewayAuthConfigured(auth, authConfig)).toThrow(/mutually exclusive/);
722722
});
723723

724+
it("still requires trustedProxy config before reporting a token conflict", () => {
725+
const auth = resolveGatewayAuth({
726+
authConfig: {
727+
mode: "trusted-proxy",
728+
token: "shared-secret",
729+
},
730+
});
731+
732+
expect(() =>
733+
assertGatewayAuthConfigured(auth, {
734+
mode: "trusted-proxy",
735+
token: "shared-secret",
736+
}),
737+
).toThrow(/no trustedProxy config was provided/);
738+
});
739+
724740
it("supports Pomerium-style headers", async () => {
725741
const res = await authorizeTrustedProxy({
726742
auth: {
@@ -762,7 +778,7 @@ describe("trusted-proxy auth", () => {
762778
expect(res.user).toBe("nick@example.com");
763779
});
764780

765-
describe("local-direct token fallback", () => {
781+
describe("local-direct trusted-proxy requests", () => {
766782
function authorizeLocalDirect(options?: {
767783
token?: string;
768784
connectToken?: string;
@@ -787,38 +803,37 @@ describe("trusted-proxy auth", () => {
787803
});
788804
}
789805

790-
it("allows local-direct request with a valid token", async () => {
791-
const res = await authorizeLocalDirect({
792-
token: "secret",
793-
connectToken: "secret",
794-
});
795-
expect(res.ok).toBe(true);
796-
expect(res.method).toBe("token");
797-
});
798-
799-
it("rejects local-direct request without credentials", async () => {
800-
const res = await authorizeLocalDirect({
801-
token: "secret",
802-
});
803-
expect(res.ok).toBe(false);
804-
expect(res.reason).toBe("token_missing");
805-
});
806-
807-
it("rejects local-direct request with a wrong token", async () => {
808-
const res = await authorizeLocalDirect({
809-
token: "secret",
810-
connectToken: "wrong",
811-
});
812-
expect(res.ok).toBe(false);
813-
expect(res.reason).toBe("token_mismatch");
814-
});
815-
816-
it("rejects local-direct request when no local token is configured", async () => {
817-
const res = await authorizeLocalDirect({
818-
connectToken: "secret",
819-
});
806+
it.each([
807+
{
808+
name: "without credentials",
809+
options: {
810+
token: "secret",
811+
},
812+
},
813+
{
814+
name: "with a valid token",
815+
options: {
816+
token: "secret",
817+
connectToken: "secret",
818+
},
819+
},
820+
{
821+
name: "with a wrong token",
822+
options: {
823+
token: "secret",
824+
connectToken: "wrong",
825+
},
826+
},
827+
{
828+
name: "when no local token is configured",
829+
options: {
830+
connectToken: "secret",
831+
},
832+
},
833+
])("rejects local-direct request $name", async ({ options }) => {
834+
const res = await authorizeLocalDirect(options);
820835
expect(res.ok).toBe(false);
821-
expect(res.reason).toBe("token_missing_config");
836+
expect(res.reason).toBe("trusted_proxy_loopback_source");
822837
});
823838

824839
it("rejects trusted-proxy identity headers from loopback sources", async () => {
@@ -867,7 +882,7 @@ describe("trusted-proxy auth", () => {
867882
expect(res.reason).toBe("trusted_proxy_loopback_source");
868883
});
869884

870-
it("uses token fallback for direct loopback even when Host is not localish", async () => {
885+
it("rejects direct loopback even when Host is not localish", async () => {
871886
const res = await authorizeGatewayConnect({
872887
auth: {
873888
mode: "trusted-proxy",
@@ -885,8 +900,8 @@ describe("trusted-proxy auth", () => {
885900
} as never,
886901
});
887902

888-
expect(res.ok).toBe(true);
889-
expect(res.method).toBe("token");
903+
expect(res.ok).toBe(false);
904+
expect(res.reason).toBe("trusted_proxy_loopback_source");
890905
});
891906

892907
it("rejects same-host proxy request with missing required header", async () => {

src/gateway/auth.ts

Lines changed: 1 addition & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -452,38 +452,14 @@ export async function authorizeGatewayConnect(
452452
if (auth.mode === "trusted-proxy") {
453453
// Same-host reverse proxies may forward identity headers without a full
454454
// forwarded chain; keep those on the trusted-proxy path so allowUsers and
455-
// requiredHeaders still apply. Only raw local-direct traffic falls back.
455+
// requiredHeaders still apply.
456456
if (!auth.trustedProxy) {
457457
return { ok: false, reason: "trusted_proxy_config_missing" };
458458
}
459459
if (!trustedProxies || trustedProxies.length === 0) {
460460
return { ok: false, reason: "trusted_proxy_no_proxies_configured" };
461461
}
462462

463-
const proxyUserHeader = auth.trustedProxy?.userHeader?.toLowerCase();
464-
const hasProxyIdentityHeader =
465-
proxyUserHeader !== undefined && Boolean(req?.headers?.[proxyUserHeader]);
466-
if (localDirect && !hasProxyIdentityHeader) {
467-
if (limiter) {
468-
const rlCheck: RateLimitCheckResult = limiter.check(ip, rateLimitScope);
469-
if (!rlCheck.allowed) {
470-
return {
471-
ok: false,
472-
reason: "rate_limited",
473-
rateLimited: true,
474-
retryAfterMs: rlCheck.retryAfterMs,
475-
};
476-
}
477-
}
478-
return authorizeTokenAuth({
479-
authToken: auth.token,
480-
connectToken: connectAuth?.token,
481-
limiter,
482-
ip,
483-
rateLimitScope,
484-
});
485-
}
486-
487463
const result = authorizeTrustedProxy({
488464
req,
489465
trustedProxies,

0 commit comments

Comments
 (0)