feat(gateway): implement trusted-proxy auth logic
- Add 'trusted-proxy' to ResolvedGatewayAuthMode - Add trustedProxy field to ResolvedGatewayAuth - Add authorizeTrustedProxy() helper function - Update authorizeGatewayConnect() to handle trusted-proxy mode - Validate proxy source IP against trustedProxies list - Support required headers and user allowlist validation Part of #1560 Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
This commit is contained in:
parent
b0dc39412f
commit
2acb4f47cc
@ -1,19 +1,24 @@
|
|||||||
import { timingSafeEqual } from "node:crypto";
|
import { timingSafeEqual } from "node:crypto";
|
||||||
import type { IncomingMessage } from "node:http";
|
import type { IncomingMessage } from "node:http";
|
||||||
import type { GatewayAuthConfig, GatewayTailscaleMode } from "../config/config.js";
|
import type {
|
||||||
|
GatewayAuthConfig,
|
||||||
|
GatewayTailscaleMode,
|
||||||
|
GatewayTrustedProxyConfig,
|
||||||
|
} from "../config/config.js";
|
||||||
import { isTrustedProxyAddress, resolveGatewayClientIp } from "./net.js";
|
import { isTrustedProxyAddress, resolveGatewayClientIp } from "./net.js";
|
||||||
export type ResolvedGatewayAuthMode = "none" | "token" | "password";
|
export type ResolvedGatewayAuthMode = "none" | "token" | "password" | "trusted-proxy";
|
||||||
|
|
||||||
export type ResolvedGatewayAuth = {
|
export type ResolvedGatewayAuth = {
|
||||||
mode: ResolvedGatewayAuthMode;
|
mode: ResolvedGatewayAuthMode;
|
||||||
token?: string;
|
token?: string;
|
||||||
password?: string;
|
password?: string;
|
||||||
allowTailscale: boolean;
|
allowTailscale: boolean;
|
||||||
|
trustedProxy?: GatewayTrustedProxyConfig;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type GatewayAuthResult = {
|
export type GatewayAuthResult = {
|
||||||
ok: boolean;
|
ok: boolean;
|
||||||
method?: "none" | "token" | "password" | "tailscale" | "device-token";
|
method?: "none" | "token" | "password" | "tailscale" | "device-token" | "trusted-proxy";
|
||||||
user?: string;
|
user?: string;
|
||||||
reason?: string;
|
reason?: string;
|
||||||
};
|
};
|
||||||
@ -127,15 +132,30 @@ export function resolveGatewayAuth(params: {
|
|||||||
const env = params.env ?? process.env;
|
const env = params.env ?? process.env;
|
||||||
const token = authConfig.token ?? env.CLAWDBOT_GATEWAY_TOKEN ?? undefined;
|
const token = authConfig.token ?? env.CLAWDBOT_GATEWAY_TOKEN ?? undefined;
|
||||||
const password = authConfig.password ?? env.CLAWDBOT_GATEWAY_PASSWORD ?? undefined;
|
const password = authConfig.password ?? env.CLAWDBOT_GATEWAY_PASSWORD ?? undefined;
|
||||||
const mode: ResolvedGatewayAuth["mode"] =
|
const trustedProxy = authConfig.trustedProxy;
|
||||||
authConfig.mode ?? (password ? "password" : token ? "token" : "none");
|
|
||||||
|
// Determine auth mode: explicit > password > token > trusted-proxy > none
|
||||||
|
let mode: ResolvedGatewayAuth["mode"];
|
||||||
|
if (authConfig.mode) {
|
||||||
|
mode = authConfig.mode;
|
||||||
|
} else if (password) {
|
||||||
|
mode = "password";
|
||||||
|
} else if (token) {
|
||||||
|
mode = "token";
|
||||||
|
} else {
|
||||||
|
mode = "none";
|
||||||
|
}
|
||||||
|
|
||||||
const allowTailscale =
|
const allowTailscale =
|
||||||
authConfig.allowTailscale ?? (params.tailscaleMode === "serve" && mode !== "password");
|
authConfig.allowTailscale ??
|
||||||
|
(params.tailscaleMode === "serve" && mode !== "password" && mode !== "trusted-proxy");
|
||||||
|
|
||||||
return {
|
return {
|
||||||
mode,
|
mode,
|
||||||
token,
|
token,
|
||||||
password,
|
password,
|
||||||
allowTailscale,
|
allowTailscale,
|
||||||
|
trustedProxy,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -148,6 +168,65 @@ export function assertGatewayAuthConfigured(auth: ResolvedGatewayAuth): void {
|
|||||||
if (auth.mode === "password" && !auth.password) {
|
if (auth.mode === "password" && !auth.password) {
|
||||||
throw new Error("gateway auth mode is password, but no password was configured");
|
throw new Error("gateway auth mode is password, but no password was configured");
|
||||||
}
|
}
|
||||||
|
if (auth.mode === "trusted-proxy") {
|
||||||
|
if (!auth.trustedProxy) {
|
||||||
|
throw new Error(
|
||||||
|
"gateway auth mode is trusted-proxy, but no trustedProxy config was provided (set gateway.auth.trustedProxy)",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (!auth.trustedProxy.userHeader || auth.trustedProxy.userHeader.trim() === "") {
|
||||||
|
throw new Error(
|
||||||
|
"gateway auth mode is trusted-proxy, but trustedProxy.userHeader is empty (set gateway.auth.trustedProxy.userHeader)",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if the request came from a trusted proxy and extract user identity.
|
||||||
|
* Returns the user identity if valid, or null with a reason if not.
|
||||||
|
*/
|
||||||
|
function authorizeTrustedProxy(params: {
|
||||||
|
req?: IncomingMessage;
|
||||||
|
trustedProxies?: string[];
|
||||||
|
trustedProxyConfig: GatewayTrustedProxyConfig;
|
||||||
|
}): { user: string } | { reason: string } {
|
||||||
|
const { req, trustedProxies, trustedProxyConfig } = params;
|
||||||
|
|
||||||
|
if (!req) {
|
||||||
|
return { reason: "trusted_proxy_no_request" };
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verify the request came from a trusted proxy IP
|
||||||
|
const remoteAddr = req.socket?.remoteAddress;
|
||||||
|
if (!remoteAddr || !isTrustedProxyAddress(remoteAddr, trustedProxies)) {
|
||||||
|
return { reason: "trusted_proxy_untrusted_source" };
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check required headers are present
|
||||||
|
const requiredHeaders = trustedProxyConfig.requiredHeaders ?? [];
|
||||||
|
for (const header of requiredHeaders) {
|
||||||
|
const value = headerValue(req.headers[header.toLowerCase()]);
|
||||||
|
if (!value || value.trim() === "") {
|
||||||
|
return { reason: `trusted_proxy_missing_header_${header}` };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Extract user identity from the configured header
|
||||||
|
const userHeaderValue = headerValue(req.headers[trustedProxyConfig.userHeader.toLowerCase()]);
|
||||||
|
if (!userHeaderValue || userHeaderValue.trim() === "") {
|
||||||
|
return { reason: "trusted_proxy_user_missing" };
|
||||||
|
}
|
||||||
|
|
||||||
|
const user = userHeaderValue.trim();
|
||||||
|
|
||||||
|
// Check user allowlist if configured
|
||||||
|
const allowUsers = trustedProxyConfig.allowUsers ?? [];
|
||||||
|
if (allowUsers.length > 0 && !allowUsers.includes(user)) {
|
||||||
|
return { reason: "trusted_proxy_user_not_allowed" };
|
||||||
|
}
|
||||||
|
|
||||||
|
return { user };
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function authorizeGatewayConnect(params: {
|
export async function authorizeGatewayConnect(params: {
|
||||||
@ -159,6 +238,27 @@ export async function authorizeGatewayConnect(params: {
|
|||||||
const { auth, connectAuth, req, trustedProxies } = params;
|
const { auth, connectAuth, req, trustedProxies } = params;
|
||||||
const localDirect = isLocalDirectRequest(req, trustedProxies);
|
const localDirect = isLocalDirectRequest(req, trustedProxies);
|
||||||
|
|
||||||
|
// Handle trusted-proxy auth mode first
|
||||||
|
if (auth.mode === "trusted-proxy") {
|
||||||
|
if (!auth.trustedProxy) {
|
||||||
|
return { ok: false, reason: "trusted_proxy_config_missing" };
|
||||||
|
}
|
||||||
|
if (!trustedProxies || trustedProxies.length === 0) {
|
||||||
|
return { ok: false, reason: "trusted_proxy_no_proxies_configured" };
|
||||||
|
}
|
||||||
|
|
||||||
|
const result = authorizeTrustedProxy({
|
||||||
|
req,
|
||||||
|
trustedProxies,
|
||||||
|
trustedProxyConfig: auth.trustedProxy,
|
||||||
|
});
|
||||||
|
|
||||||
|
if ("user" in result) {
|
||||||
|
return { ok: true, method: "trusted-proxy", user: result.user };
|
||||||
|
}
|
||||||
|
return { ok: false, reason: result.reason };
|
||||||
|
}
|
||||||
|
|
||||||
if (auth.allowTailscale && !localDirect) {
|
if (auth.allowTailscale && !localDirect) {
|
||||||
const tailscaleUser = getTailscaleUser(req);
|
const tailscaleUser = getTailscaleUser(req);
|
||||||
const tailscaleProxy = isTailscaleProxyRequest(req);
|
const tailscaleProxy = isTailscaleProxyRequest(req);
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user