optionally stamp ip header for downstream use

This commit is contained in:
Owen
2026-08-13 09:14:37 -04:00
parent 5fbb205044
commit 7291628f86
5 changed files with 91 additions and 3 deletions
+9
View File
@@ -12,6 +12,15 @@ export const AI_GATEWAY_RESOURCE_TYPE_HEADER =
export type AiGatewayResourceType = "resource" | "site-resource";
// Opt-in (server.enable_ai_gateway_client_ip_header): carries the client IP
// that Badger resolved at the Traefik hop, so it survives an intermediary
// proxy between Traefik and the AI gateway that overwrites
// X-Forwarded-For/X-Real-Ip instead of appending to them. Set by a
// disableForwardAuth Badger middleware instance (see getTraefikConfig.ts)
// on the site-resource inference router only, since that's the sole path
// that resolves request identity from the client IP.
export const AI_GATEWAY_CLIENT_IP_HEADER = "X-Pangolin-Client-Ip";
/**
* Derive a Traefik-injected trust token from the server secret.
* Traefik overwrites this header on inference routes so the AI gateway can
+21 -1
View File
@@ -153,6 +153,25 @@ export const configSchema = z
})
.optional(),
trust_proxy: z.int().gte(0).optional().default(1),
// Opt-in: have Traefik/Badger stamp the resolved client IP
// into a dedicated header (X-Pangolin-Client-Ip) on the
// site-resource AI gateway route, so it survives an
// intermediary proxy between Traefik and the gateway that
// overwrites X-Forwarded-For/X-Real-Ip instead of appending
// to them. Off by default since it requires a Badger
// version that supports realIpHeader.
enable_ai_gateway_client_ip_header: z
.boolean()
.optional()
.default(false)
.transform((val) =>
process.env.ENABLE_AI_GATEWAY_CLIENT_IP_HEADER !==
undefined
? process.env
.ENABLE_AI_GATEWAY_CLIENT_IP_HEADER ===
"true"
: val
),
secret: z.string().pipe(z.string().min(8)).optional(),
maxmind_db_path: z.string().optional(),
maxmind_asn_path: z.string().optional()
@@ -183,7 +202,8 @@ export const configSchema = z
"resource_session_request_param",
dashboard_session_length_hours: 720,
resource_session_length_hours: 720,
trust_proxy: 1
trust_proxy: 1,
enable_ai_gateway_client_ip_header: false
}),
postgres: z
.object({
+26
View File
@@ -27,6 +27,7 @@ import regionalCache from "@server/lib/cache";
import {
AI_GATEWAY_TRUST_HEADER,
AI_GATEWAY_RESOURCE_TYPE_HEADER,
AI_GATEWAY_CLIENT_IP_HEADER,
getAiGatewayTrustToken
} from "@server/lib/aiGatewayTrust";
@@ -786,6 +787,28 @@ export async function getTraefikConfig(
}
};
// Opt-in: a Badger instance with forward auth disabled, used only
// to stamp the resolved client IP into a dedicated header before
// the request reaches whatever sits between Traefik and the AI
// gateway. Only the site-resource router below needs this - it's
// the only path that resolves request identity from the client IP
// (see resolveRequestUser in aiGateway/pipeline.ts) - and it's the
// only inference router that doesn't already run Badger.
const aiGatewayClientIpMiddlewareName = "ai-gateway-client-ip";
const enableAiGatewayClientIpHeader =
config.getRawConfig().server.enable_ai_gateway_client_ip_header;
if (enableAiGatewayClientIpHeader) {
config_output.http.middlewares[aiGatewayClientIpMiddlewareName] =
{
plugin: {
badger: {
disableForwardAuth: true,
realIpHeader: AI_GATEWAY_CLIENT_IP_HEADER
}
}
};
}
// Public inference resources: same TLS/cert-resolver handling as
// plain http-mode resources, but the service points at the AI
// gateway instead of any real backend targets.
@@ -958,6 +981,9 @@ export async function getTraefikConfig(
const additionalMiddlewares =
config.getRawConfig().traefik.additional_middlewares || [];
const routerMiddlewares = [
...(enableAiGatewayClientIpHeader
? [aiGatewayClientIpMiddlewareName]
: []),
aiGatewayTrustMiddlewareNameSiteResource,
srHeadersMiddlewareName,
...additionalMiddlewares
@@ -62,6 +62,7 @@ import regionalCache from "#private/lib/cache";
import {
AI_GATEWAY_TRUST_HEADER,
AI_GATEWAY_RESOURCE_TYPE_HEADER,
AI_GATEWAY_CLIENT_IP_HEADER,
getAiGatewayTrustToken
} from "@server/lib/aiGatewayTrust";
@@ -1600,6 +1601,28 @@ export async function getTraefikConfig(
}
};
// Opt-in: a Badger instance with forward auth disabled, used only
// to stamp the resolved client IP into a dedicated header before
// the request reaches whatever sits between Traefik and the AI
// gateway. Only the site-resource router below needs this - it's
// the only path that resolves request identity from the client IP
// (see resolveRequestUser in aiGateway/pipeline.ts) - and it's the
// only inference router that doesn't already run Badger.
const aiGatewayClientIpMiddlewareName = "ai-gateway-client-ip";
const enableAiGatewayClientIpHeader =
config.getRawConfig().server.enable_ai_gateway_client_ip_header;
if (enableAiGatewayClientIpHeader) {
config_output.http.middlewares[aiGatewayClientIpMiddlewareName] =
{
plugin: {
badger: {
disableForwardAuth: true,
realIpHeader: AI_GATEWAY_CLIENT_IP_HEADER
}
}
};
}
// Public inference resources: same TLS/cert-resolver handling as
// plain http-mode resources, but the service points at the AI
// gateway instead of any real backend targets.
@@ -1770,6 +1793,9 @@ export async function getTraefikConfig(
const additionalMiddlewares =
config.getRawConfig().traefik.additional_middlewares || [];
const routerMiddlewares: string[] = [
...(enableAiGatewayClientIpHeader
? [aiGatewayClientIpMiddlewareName]
: []),
aiGatewayTrustMiddlewareNameSiteResource
];
+9 -2
View File
@@ -42,6 +42,7 @@ import { localCache } from "@server/lib/cache";
import {
AI_GATEWAY_TRUST_HEADER,
AI_GATEWAY_RESOURCE_TYPE_HEADER,
AI_GATEWAY_CLIENT_IP_HEADER,
isAiGatewayTrustHeaderValid,
getAiGatewayResourceType,
type AiGatewayResourceType
@@ -303,7 +304,12 @@ async function resolveRequestUser(
return { user: null, virtualApiKeyId: null };
}
const ip = req.ip;
// Prefer the dedicated header Badger stamps at the Traefik hop (opt-in
// via server.enable_ai_gateway_client_ip_header) over req.ip, since an
// intermediary proxy between Traefik and this gateway may overwrite
// X-Forwarded-For/X-Real-Ip rather than appending to them, corrupting
// what req.ip resolves to.
const ip = getRequestHeader(req, AI_GATEWAY_CLIENT_IP_HEADER) || req.ip;
if (!ip) {
return { user: null, virtualApiKeyId: null };
}
@@ -965,7 +971,8 @@ export async function handleAiGatewayProxy(
"content-length",
"accept-encoding",
AI_GATEWAY_TRUST_HEADER.toLowerCase(),
AI_GATEWAY_RESOURCE_TYPE_HEADER.toLowerCase()
AI_GATEWAY_RESOURCE_TYPE_HEADER.toLowerCase(),
AI_GATEWAY_CLIENT_IP_HEADER.toLowerCase()
]);
const headers: Record<string, string> = {};