From d02d2f79ba20f7a2b7562dc3e5cbbbc9853259e1 Mon Sep 17 00:00:00 2001 From: Owen Date: Tue, 20 Oct 2026 11:53:44 -0400 Subject: [PATCH] Make sure the MTLS is enforced for redirects --- server/lib/traefik/getTraefikConfig.ts | 12 +++++-- server/lib/traefik/redirect.ts | 31 +++++++++++++++++-- .../private/lib/traefik/getTraefikConfig.ts | 12 +++++-- 3 files changed, 49 insertions(+), 6 deletions(-) diff --git a/server/lib/traefik/getTraefikConfig.ts b/server/lib/traefik/getTraefikConfig.ts index aadb65b6a..a6e76464a 100644 --- a/server/lib/traefik/getTraefikConfig.ts +++ b/server/lib/traefik/getTraefikConfig.ts @@ -407,6 +407,7 @@ export async function getTraefikConfig( wildcard: row.resourceWildcard, ssl: attachedToResource ? !!row.resourceSsl : row.ssl, attachedTo: attachedToResource ? "resource" : "domain", + resourceId: row.resourceId, matchPath: row.matchPath, pathMatchType: row.pathMatchType, priority: row.priority, @@ -416,7 +417,8 @@ export async function getTraefikConfig( } // Trusted client-CA certificates for resources that require mTLS: HTTP, - // browser-gateway (SSH/RDP/VNC) and inference resources served over SSL. + // browser-gateway (SSH/RDP/VNC) and inference resources served over SSL, + // and the redirects attached to them. const mtlsCaCertsByResource = await getMtlsCaCertificatesByResource([ ...[...resourcesMap.values()] .filter((r) => r.mode === "http" && r.ssl) @@ -424,7 +426,12 @@ export async function getTraefikConfig( ...[...browserGatewayResourcesMap.values()] .filter((r) => r.ssl) .map((r) => r.resourceId), - ...inferenceResources.filter((r) => r.ssl).map((r) => r.resourceId) + ...inferenceResources.filter((r) => r.ssl).map((r) => r.resourceId), + // redirects attached to a resource share its host, and are emitted + // even on exit nodes that don't serve the resource itself + ...redirectRoutes + .filter((r) => r.attachedTo === "resource" && r.ssl) + .map((r) => r.resourceId!) ]); // make sure we have at least one resource @@ -639,6 +646,7 @@ export async function getTraefikConfig( buildRedirectConfig({ config_output, redirects: redirectRoutes, + mtlsCaCertsByResource, badgerMiddlewareName, redirectHttpsMiddlewareName, resolveTls: (redirect) => diff --git a/server/lib/traefik/redirect.ts b/server/lib/traefik/redirect.ts index 05f6eae00..ecaee1102 100644 --- a/server/lib/traefik/redirect.ts +++ b/server/lib/traefik/redirect.ts @@ -1,5 +1,6 @@ import logger from "@server/logger"; import config from "@server/lib/config"; +import { applyMtlsConfig } from "@server/lib/traefik/mtls"; import { buildHostRule, appendPathMatch, @@ -12,6 +13,8 @@ export type RedirectRouteRow = { fullDomain: string; hasSubdomain: boolean; attachedTo: "resource" | "domain"; + /** Set when attachedTo is "resource". */ + resourceId: number | null; enabled: boolean; name: string; wildcard: boolean | null; @@ -35,13 +38,16 @@ export function buildRedirectConfig(params: { badgerMiddlewareName: string; redirectHttpsMiddlewareName: string; resolveTls: (row: RedirectRouteRow) => any | null; + // trusted client-CA PEMs for resources that require mTLS + mtlsCaCertsByResource: Map; }): void { const { config_output, redirects, badgerMiddlewareName, redirectHttpsMiddlewareName, - resolveTls + resolveTls, + mtlsCaCertsByResource } = params; if (redirects.length === 0) { @@ -72,6 +78,27 @@ export function buildRedirectConfig(params: { } } + // A redirect attached to a resource listens on the resource's host, + // and Traefik resolves TLS options per host: unless it references the + // resource's mTLS options too, Traefik falls back to the defaults and + // the resource's mTLS is no longer enforced. + const redirectMiddlewares = [...routerMiddlewares]; + if ( + redirect.ssl && + redirect.attachedTo === "resource" && + redirect.resourceId !== null + ) { + const mtlsOptionsName = applyMtlsConfig( + config_output, + redirect.resourceId, + mtlsCaCertsByResource.get(redirect.resourceId), + redirectMiddlewares + ); + if (mtlsOptionsName) { + tls = { ...tls, options: mtlsOptionsName }; + } + } + if (!config_output.http.routers) { config_output.http.routers = {}; } @@ -113,7 +140,7 @@ export function buildRedirectConfig(params: { config_output.http.routers[routerName] = { entryPoints: [redirect.ssl ? httpsEntrypoint : httpEntrypoint], - middlewares: routerMiddlewares, + middlewares: redirectMiddlewares, service: NOOP_SERVICE, rule, priority, diff --git a/server/private/lib/traefik/getTraefikConfig.ts b/server/private/lib/traefik/getTraefikConfig.ts index 6794078e4..77b099c2b 100644 --- a/server/private/lib/traefik/getTraefikConfig.ts +++ b/server/private/lib/traefik/getTraefikConfig.ts @@ -488,6 +488,7 @@ export async function getTraefikConfig( wildcard: row.resourceWildcard, ssl: attachedToResource ? !!row.resourceSsl : row.ssl, attachedTo: attachedToResource ? "resource" : "domain", + resourceId: row.resourceId, matchPath: row.matchPath, pathMatchType: row.pathMatchType, priority: row.priority, @@ -547,7 +548,8 @@ export async function getTraefikConfig( } // Trusted client-CA certificates for resources that require mTLS: HTTP, - // browser-gateway (SSH/RDP/VNC) and inference resources served over SSL. + // browser-gateway (SSH/RDP/VNC) and inference resources served over SSL, + // and the redirects attached to them. const mtlsCaCertsByResource = await getMtlsCaCertificatesByResource([ ...[...resourcesMap.values()] .filter((r) => r.mode === "http" && r.ssl) @@ -555,7 +557,12 @@ export async function getTraefikConfig( ...[...browserGatewayResourcesMap.values()] .filter((r) => r.ssl) .map((r) => r.resourceId), - ...inferenceResources.filter((r) => r.ssl).map((r) => r.resourceId) + ...inferenceResources.filter((r) => r.ssl).map((r) => r.resourceId), + // redirects attached to a resource share its host, and are emitted + // even on exit nodes that don't serve the resource itself + ...redirectRoutes + .filter((r) => r.attachedTo === "resource" && r.ssl) + .map((r) => r.resourceId!) ]); const traefikRateLimit = config.getRawConfig().traefik.rate_limit; @@ -920,6 +927,7 @@ export async function getTraefikConfig( buildRedirectConfig({ config_output, redirects: redirectRoutes, + mtlsCaCertsByResource, badgerMiddlewareName, redirectHttpsMiddlewareName, resolveTls: (redirect) => {