Make sure the MTLS is enforced for redirects

This commit is contained in:
Owen
2026-10-20 11:53:44 -04:00
parent 3510054a92
commit d02d2f79ba
3 changed files with 49 additions and 6 deletions
+10 -2
View File
@@ -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) =>
+29 -2
View File
@@ -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<number, string[]>;
}): 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,
+10 -2
View File
@@ -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) => {