mirror of
https://github.com/fosrl/pangolin.git
synced 2026-10-15 00:59:04 +02:00
Merge branch 'mtls' into dev
This commit is contained in:
@@ -0,0 +1,19 @@
|
||||
/*
|
||||
* This file is part of a proprietary work.
|
||||
*
|
||||
* Copyright (c) 2025-2026 Fossorial, Inc.
|
||||
* All rights reserved.
|
||||
*
|
||||
* This file is licensed under the Fossorial Commercial License.
|
||||
* You may not use this file except in compliance with the License.
|
||||
* Unauthorized use, copying, modification, or distribution is strictly prohibited.
|
||||
*
|
||||
* This file is not licensed under the AGPLv3.
|
||||
*/
|
||||
|
||||
import { z } from "zod";
|
||||
|
||||
export const MtlsSchema = z.object({
|
||||
enabled: z.boolean().optional(),
|
||||
"ca-certificates": z.array(z.string().min(1)).optional()
|
||||
});
|
||||
@@ -18,6 +18,7 @@ import {
|
||||
domains,
|
||||
exitNodes,
|
||||
loginPage,
|
||||
resourceMtlsCertificates,
|
||||
SiteResource,
|
||||
targetHealthCheck
|
||||
} from "@server/db";
|
||||
@@ -146,6 +147,8 @@ export async function getTraefikConfig(
|
||||
maintenanceMessage: resources.maintenanceMessage,
|
||||
maintenanceEstimatedTime: resources.maintenanceEstimatedTime,
|
||||
|
||||
mtlsEnabled: resources.mtlsEnabled,
|
||||
|
||||
// Target fields
|
||||
targetId: targets.targetId,
|
||||
targetEnabled: targets.enabled,
|
||||
@@ -293,7 +296,9 @@ export async function getTraefikConfig(
|
||||
maintenanceModeType: row.maintenanceModeType,
|
||||
maintenanceTitle: row.maintenanceTitle,
|
||||
maintenanceMessage: row.maintenanceMessage,
|
||||
maintenanceEstimatedTime: row.maintenanceEstimatedTime
|
||||
maintenanceEstimatedTime: row.maintenanceEstimatedTime,
|
||||
|
||||
mtlsEnabled: row.mtlsEnabled
|
||||
});
|
||||
}
|
||||
|
||||
@@ -317,6 +322,31 @@ export async function getTraefikConfig(
|
||||
});
|
||||
}
|
||||
|
||||
// Trusted client-CA certificates for resources that require mTLS.
|
||||
const mtlsResourceIds = [
|
||||
...new Set(
|
||||
[...resourcesMap.values()]
|
||||
.filter((r) => r.mode === "http" && r.mtlsEnabled)
|
||||
.map((r) => r.resourceId as number)
|
||||
)
|
||||
];
|
||||
const mtlsCaCertsByResource = new Map<number, string[]>();
|
||||
if (mtlsResourceIds.length > 0) {
|
||||
const caRows = await db
|
||||
.select({
|
||||
resourceId: resourceMtlsCertificates.resourceId,
|
||||
certificate: resourceMtlsCertificates.certificate
|
||||
})
|
||||
.from(resourceMtlsCertificates)
|
||||
.where(inArray(resourceMtlsCertificates.resourceId, mtlsResourceIds))
|
||||
.orderBy(resourceMtlsCertificates.mtlsCertificateId);
|
||||
for (const row of caRows) {
|
||||
const list = mtlsCaCertsByResource.get(row.resourceId) ?? [];
|
||||
list.push(row.certificate);
|
||||
mtlsCaCertsByResource.set(row.resourceId, list);
|
||||
}
|
||||
}
|
||||
|
||||
// Group browser gateway targets by resource
|
||||
const browserGatewayResourcesMap = browserGatewayUiUrl
|
||||
? buildBrowserGatewayResourcesMap(
|
||||
@@ -531,7 +561,7 @@ export async function getTraefikConfig(
|
||||
resource.pathMatchType
|
||||
);
|
||||
|
||||
let tls = {};
|
||||
let tls: Record<string, any> = {};
|
||||
if (!pangolinCertModeEnabled) {
|
||||
tls = buildWildcardTls({
|
||||
fullDomain,
|
||||
@@ -553,6 +583,59 @@ export async function getTraefikConfig(
|
||||
}
|
||||
}
|
||||
|
||||
// mTLS: Traefik resolves TLS options per hostname, and routers on
|
||||
// the same host that reference different options make it fall back
|
||||
// to the defaults, so the options are keyed on the resource (not
|
||||
// the per-path router key) and every TLS router below (including
|
||||
// the maintenance ones) references the same name.
|
||||
const mtlsCaCerts =
|
||||
resource.mtlsEnabled && resource.ssl
|
||||
? mtlsCaCertsByResource.get(resource.resourceId)
|
||||
: undefined;
|
||||
if (mtlsCaCerts && mtlsCaCerts.length > 0) {
|
||||
const mtlsOptionsName = `resource-${resource.resourceId}-mtls`;
|
||||
const mtlsHeadersMiddlewareName = `resource-${resource.resourceId}-mtls-headers`;
|
||||
|
||||
if (!config_output.tls) {
|
||||
config_output.tls = {};
|
||||
}
|
||||
if (!config_output.tls.options) {
|
||||
config_output.tls.options = {};
|
||||
}
|
||||
// caFiles entries are Traefik FileOrContent values: anything
|
||||
// that isn't an existing file path is read as inline PEM, so
|
||||
// the CA certs travel in this config to any exit node.
|
||||
config_output.tls.options[mtlsOptionsName] = {
|
||||
clientAuth: {
|
||||
caFiles: mtlsCaCerts,
|
||||
clientAuthType: "RequireAndVerifyClientCert"
|
||||
}
|
||||
};
|
||||
|
||||
config_output.http.middlewares[mtlsHeadersMiddlewareName] = {
|
||||
passTLSClientCert: {
|
||||
pem: false,
|
||||
info: {
|
||||
notAfter: true,
|
||||
notBefore: true,
|
||||
serialNumber: true,
|
||||
sans: true,
|
||||
subject: {
|
||||
commonName: true,
|
||||
organization: true
|
||||
},
|
||||
issuer: {
|
||||
commonName: true,
|
||||
organization: true
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
routerMiddlewares.unshift(mtlsHeadersMiddlewareName);
|
||||
|
||||
tls = { ...tls, options: mtlsOptionsName };
|
||||
}
|
||||
|
||||
config_output.http.services![serviceName] = {
|
||||
loadBalancer: {
|
||||
servers: buildHttpLoadBalancerServers(targets),
|
||||
|
||||
@@ -303,6 +303,10 @@ async function disableFeature(
|
||||
await disablemaintenancePage(orgId);
|
||||
break;
|
||||
|
||||
case TierFeature.Mtls:
|
||||
await disableMtls(orgId);
|
||||
break;
|
||||
|
||||
case TierFeature.DevicePosture:
|
||||
await disableDevicePosture(orgId);
|
||||
break;
|
||||
@@ -519,6 +523,18 @@ async function disablemaintenancePage(orgId: string): Promise<void> {
|
||||
logger.info(`Disabled maintenance page on all resources for org ${orgId}`);
|
||||
}
|
||||
|
||||
// The uploaded CA certificates are kept so mTLS can be re-enabled on upgrade.
|
||||
async function disableMtls(orgId: string): Promise<void> {
|
||||
await db
|
||||
.update(resources)
|
||||
.set({
|
||||
mtlsEnabled: false
|
||||
})
|
||||
.where(eq(resources.orgId, orgId));
|
||||
|
||||
logger.info(`Disabled mTLS on all resources for org ${orgId}`);
|
||||
}
|
||||
|
||||
async function disableDevicePosture(orgId: string): Promise<void> {}
|
||||
|
||||
async function disableTwoFactorEnforcement(orgId: string): Promise<void> {
|
||||
|
||||
Reference in New Issue
Block a user