diff --git a/app/(dashboard)/certificates/CertificatesClient.tsx b/app/(dashboard)/certificates/CertificatesClient.tsx
index 91f27b76..cf0506b0 100644
--- a/app/(dashboard)/certificates/CertificatesClient.tsx
+++ b/app/(dashboard)/certificates/CertificatesClient.tsx
@@ -12,22 +12,30 @@ import {
CardContent,
Chip,
Divider,
+ IconButton,
Stack,
TextField,
+ Tooltip,
Typography,
} from "@mui/material";
+import AddIcon from "@mui/icons-material/Add";
+import EditIcon from "@mui/icons-material/Edit";
+import DeleteIcon from "@mui/icons-material/Delete";
import { DataTable } from "@/src/components/ui/DataTable";
import {
createCertificateAction,
deleteCertificateAction,
updateCertificateAction,
} from "./actions";
-import type { AcmeHost, CertExpiryStatus, ImportedCertView, ManagedCertView } from "./page";
+import type { AcmeHost, CaCertificate, CertExpiryStatus, ImportedCertView, ManagedCertView } from "./page";
+import { useState } from "react";
+import { CreateCaCertDialog, EditCaCertDialog, DeleteCaCertDialog } from "@/src/components/ca-certificates/CaCertDialogs";
type Props = {
acmeHosts: AcmeHost[];
importedCerts: ImportedCertView[];
managedCerts: ManagedCertView[];
+ caCertificates: CaCertificate[];
acmePagination: { total: number; page: number; perPage: number };
};
@@ -55,7 +63,10 @@ function ExpiryChip({
return ;
}
-export default function CertificatesClient({ acmeHosts, importedCerts, managedCerts, acmePagination }: Props) {
+export default function CertificatesClient({ acmeHosts, importedCerts, managedCerts, caCertificates, acmePagination }: Props) {
+ const [createCaOpen, setCreateCaOpen] = useState(false);
+ const [editCaCert, setEditCaCert] = useState(null);
+ const [deleteCaCert, setDeleteCaCert] = useState(null);
const acmeColumns = [
{
id: 'name',
@@ -420,6 +431,96 @@ export default function CertificatesClient({ acmeHosts, importedCerts, managedCe
)}
+
+ {/* Client CA Certificates */}
+ 0} disableGutters>
+ }>
+
+ Client CA Certificates
+
+
+
+
+
+
+
+ CA certificates used for mTLS — clients must present a certificate signed by one of these CAs.
+
+ }
+ variant="outlined"
+ size="small"
+ onClick={() => setCreateCaOpen(true)}
+ >
+ Add CA Certificate
+
+
+
+ {caCertificates.length === 0 ? (
+
+ No client CA certificates configured.
+
+ ) : (
+ {ca.name},
+ },
+ {
+ id: "created_at",
+ label: "Added",
+ render: (ca: CaCertificate) => (
+
+ {new Date(ca.created_at).toLocaleDateString()}
+
+ ),
+ },
+ {
+ id: "actions",
+ label: "",
+ render: (ca: CaCertificate) => (
+
+
+ setEditCaCert(ca)}>
+
+
+
+
+ setDeleteCaCert(ca)}>
+
+
+
+
+ ),
+ },
+ ]}
+ data={caCertificates}
+ keyField="id"
+ emptyMessage="No CA certificates found"
+ />
+ )}
+
+
+
+
+ {/* CA Cert Dialogs */}
+ setCreateCaOpen(false)} />
+ {editCaCert && (
+ setEditCaCert(null)}
+ />
+ )}
+ {deleteCaCert && (
+ setDeleteCaCert(null)}
+ />
+ )}
);
}
diff --git a/app/(dashboard)/certificates/ca-actions.ts b/app/(dashboard)/certificates/ca-actions.ts
new file mode 100644
index 00000000..2c0fc4f6
--- /dev/null
+++ b/app/(dashboard)/certificates/ca-actions.ts
@@ -0,0 +1,52 @@
+"use server";
+
+import { revalidatePath } from "next/cache";
+import { requireAdmin } from "@/src/lib/auth";
+import { createCaCertificate, deleteCaCertificate, updateCaCertificate } from "@/src/lib/models/ca-certificates";
+import { X509Certificate } from "node:crypto";
+
+function validatePem(pem: string): void {
+ try {
+ new X509Certificate(pem);
+ } catch {
+ throw new Error("Invalid certificate PEM: could not parse as X.509 certificate");
+ }
+}
+
+export async function createCaCertificateAction(formData: FormData) {
+ const session = await requireAdmin();
+ const userId = Number(session.user.id);
+ const name = String(formData.get("name") ?? "").trim();
+ const certificatePem = String(formData.get("certificate_pem") ?? "").trim();
+
+ if (!name) throw new Error("Name is required");
+ if (!certificatePem) throw new Error("Certificate PEM is required");
+ validatePem(certificatePem);
+
+ await createCaCertificate({ name, certificate_pem: certificatePem }, userId);
+ revalidatePath("/certificates");
+}
+
+export async function updateCaCertificateAction(id: number, formData: FormData) {
+ const session = await requireAdmin();
+ const userId = Number(session.user.id);
+ const name = formData.get("name") ? String(formData.get("name")).trim() : undefined;
+ const certificatePem = formData.get("certificate_pem") ? String(formData.get("certificate_pem")).trim() : undefined;
+
+ if (certificatePem) {
+ validatePem(certificatePem);
+ }
+
+ await updateCaCertificate(id, {
+ ...(name ? { name } : {}),
+ ...(certificatePem ? { certificate_pem: certificatePem } : {})
+ }, userId);
+ revalidatePath("/certificates");
+}
+
+export async function deleteCaCertificateAction(id: number) {
+ const session = await requireAdmin();
+ const userId = Number(session.user.id);
+ await deleteCaCertificate(id, userId);
+ revalidatePath("/certificates");
+}
diff --git a/app/(dashboard)/certificates/page.tsx b/app/(dashboard)/certificates/page.tsx
index 578903b6..1c524e08 100644
--- a/app/(dashboard)/certificates/page.tsx
+++ b/app/(dashboard)/certificates/page.tsx
@@ -5,6 +5,9 @@ import { isNull, isNotNull, count } from 'drizzle-orm';
import { requireAdmin } from '@/src/lib/auth';
import CertificatesClient from './CertificatesClient';
import { scanAcmeCerts } from '@/src/lib/acme-certs';
+import { listCaCertificates, type CaCertificate } from '@/src/lib/models/ca-certificates';
+
+export type { CaCertificate };
export type CertExpiryStatus = 'ok' | 'expiring_soon' | 'expired';
@@ -79,6 +82,8 @@ export default async function CertificatesPage({ searchParams }: PageProps) {
const offset = (page - 1) * PER_PAGE;
const acmeCertMap = scanAcmeCerts();
+ const caCerts = await listCaCertificates();
+
const [acmeRows, acmeTotal, certRows, usageRows] = await Promise.all([
db
.select({
@@ -169,6 +174,7 @@ export default async function CertificatesPage({ searchParams }: PageProps) {
acmeHosts={acmeHosts}
importedCerts={importedCerts}
managedCerts={managedCerts}
+ caCertificates={caCerts}
acmePagination={{ total: acmeTotal, page, perPage: PER_PAGE }}
/>
);
diff --git a/app/(dashboard)/proxy-hosts/ProxyHostsClient.tsx b/app/(dashboard)/proxy-hosts/ProxyHostsClient.tsx
index 3698a46f..e4c060ae 100644
--- a/app/(dashboard)/proxy-hosts/ProxyHostsClient.tsx
+++ b/app/(dashboard)/proxy-hosts/ProxyHostsClient.tsx
@@ -9,6 +9,7 @@ import ContentCopyIcon from "@mui/icons-material/ContentCopy";
import type { AccessList } from "@/src/lib/models/access-lists";
import type { Certificate } from "@/src/lib/models/certificates";
import type { ProxyHost } from "@/src/lib/models/proxy-hosts";
+import type { CaCertificate } from "@/src/lib/models/ca-certificates";
import type { AuthentikSettings } from "@/src/lib/settings";
import { toggleProxyHostAction } from "./actions";
import { PageHeader } from "@/src/components/ui/PageHeader";
@@ -20,12 +21,13 @@ type Props = {
hosts: ProxyHost[];
certificates: Certificate[];
accessLists: AccessList[];
+ caCertificates: CaCertificate[];
authentikDefaults: AuthentikSettings | null;
pagination: { total: number; page: number; perPage: number };
initialSearch: string;
};
-export default function ProxyHostsClient({ hosts, certificates, accessLists, authentikDefaults, pagination, initialSearch }: Props) {
+export default function ProxyHostsClient({ hosts, certificates, accessLists, caCertificates, authentikDefaults, pagination, initialSearch }: Props) {
const [createOpen, setCreateOpen] = useState(false);
const [duplicateHost, setDuplicateHost] = useState(null);
const [editHost, setEditHost] = useState(null);
@@ -170,6 +172,7 @@ export default function ProxyHostsClient({ hosts, certificates, accessLists, aut
certificates={certificates}
accessLists={accessLists}
authentikDefaults={authentikDefaults}
+ caCertificates={caCertificates}
/>
{editHost && (
@@ -179,6 +182,7 @@ export default function ProxyHostsClient({ hosts, certificates, accessLists, aut
onClose={() => setEditHost(null)}
certificates={certificates}
accessLists={accessLists}
+ caCertificates={caCertificates}
/>
)}
diff --git a/app/(dashboard)/proxy-hosts/actions.ts b/app/(dashboard)/proxy-hosts/actions.ts
index 88592c09..bc438b06 100644
--- a/app/(dashboard)/proxy-hosts/actions.ts
+++ b/app/(dashboard)/proxy-hosts/actions.ts
@@ -13,7 +13,8 @@ import {
type DnsResolverInput,
type UpstreamDnsResolutionInput,
type GeoBlockMode,
- type WafHostConfig
+ type WafHostConfig,
+ type MtlsConfig
} from "@/src/lib/models/proxy-hosts";
import { getCertificate } from "@/src/lib/models/certificates";
import { getCloudflareSettings, type GeoBlockSettings } from "@/src/lib/settings";
@@ -474,6 +475,14 @@ function parseDnsResolverConfig(formData: FormData): DnsResolverInput | undefine
return Object.keys(result).length > 0 ? result : undefined;
}
+function parseMtlsConfig(formData: FormData): MtlsConfig | null {
+ if (!formData.has("mtls_present")) return null;
+ const enabled = formData.get("mtls_enabled") === "true";
+ if (!enabled) return null;
+ const ids = formData.getAll("mtls_ca_cert_id").map(Number).filter(n => Number.isFinite(n) && n > 0);
+ return { enabled, ca_certificate_ids: ids };
+}
+
function parseUpstreamDnsResolutionConfig(formData: FormData): UpstreamDnsResolutionInput | undefined {
if (!formData.has("upstream_dns_resolution_present")) {
return undefined;
@@ -540,7 +549,8 @@ export async function createProxyHostAction(
dns_resolver: parseDnsResolverConfig(formData),
upstream_dns_resolution: parseUpstreamDnsResolutionConfig(formData),
...parseGeoBlockConfig(formData),
- ...parseWafConfig(formData)
+ ...parseWafConfig(formData),
+ mtls: parseMtlsConfig(formData)
},
userId
);
@@ -612,7 +622,8 @@ export async function updateProxyHostAction(
dns_resolver: parseDnsResolverConfig(formData),
upstream_dns_resolution: parseUpstreamDnsResolutionConfig(formData),
...parseGeoBlockConfig(formData),
- ...parseWafConfig(formData)
+ ...parseWafConfig(formData),
+ mtls: formData.has("mtls_present") ? parseMtlsConfig(formData) : undefined
},
userId
);
diff --git a/app/(dashboard)/proxy-hosts/page.tsx b/app/(dashboard)/proxy-hosts/page.tsx
index 2c99d38e..31513835 100644
--- a/app/(dashboard)/proxy-hosts/page.tsx
+++ b/app/(dashboard)/proxy-hosts/page.tsx
@@ -1,6 +1,7 @@
import ProxyHostsClient from "./ProxyHostsClient";
import { listProxyHostsPaginated, countProxyHosts } from "@/src/lib/models/proxy-hosts";
import { listCertificates } from "@/src/lib/models/certificates";
+import { listCaCertificates } from "@/src/lib/models/ca-certificates";
import { listAccessLists } from "@/src/lib/models/access-lists";
import { getAuthentikSettings } from "@/src/lib/settings";
import { requireAdmin } from "@/src/lib/auth";
@@ -18,10 +19,11 @@ export default async function ProxyHostsPage({ searchParams }: PageProps) {
const search = searchParam?.trim() || undefined;
const offset = (page - 1) * PER_PAGE;
- const [hosts, total, certificates, accessLists, authentikDefaults] = await Promise.all([
+ const [hosts, total, certificates, caCertificates, accessLists, authentikDefaults] = await Promise.all([
listProxyHostsPaginated(PER_PAGE, offset, search),
countProxyHosts(search),
listCertificates(),
+ listCaCertificates(),
listAccessLists(),
getAuthentikSettings(),
]);
@@ -30,6 +32,7 @@ export default async function ProxyHostsPage({ searchParams }: PageProps) {
void;
+}) {
+ const [isPending, startTransition] = useTransition();
+ const formRef = useRef(null);
+
+ function handleSubmit(e: React.FormEvent) {
+ e.preventDefault();
+ const formData = new FormData(formRef.current!);
+ startTransition(async () => {
+ await createCaCertificateAction(formData);
+ onClose();
+ });
+ }
+
+ return (
+
+ );
+}
+
+export function EditCaCertDialog({
+ open,
+ cert,
+ onClose,
+}: {
+ open: boolean;
+ cert: CaCertificate;
+ onClose: () => void;
+}) {
+ const [isPending, startTransition] = useTransition();
+ const formRef = useRef(null);
+
+ function handleSubmit(e: React.FormEvent) {
+ e.preventDefault();
+ const formData = new FormData(formRef.current!);
+ startTransition(async () => {
+ await updateCaCertificateAction(cert.id, formData);
+ onClose();
+ });
+ }
+
+ return (
+
+ );
+}
+
+export function DeleteCaCertDialog({
+ open,
+ cert,
+ onClose,
+}: {
+ open: boolean;
+ cert: CaCertificate;
+ onClose: () => void;
+}) {
+ const [isPending, startTransition] = useTransition();
+ const [error, setError] = useState(null);
+
+ function handleDelete() {
+ setError(null);
+ startTransition(async () => {
+ try {
+ await deleteCaCertificateAction(cert.id);
+ onClose();
+ } catch (e) {
+ setError(e instanceof Error ? e.message : "Failed to delete");
+ }
+ });
+ }
+
+ return (
+
+ );
+}
+
diff --git a/src/components/proxy-hosts/HostDialogs.tsx b/src/components/proxy-hosts/HostDialogs.tsx
index c5ba4620..d7453739 100644
--- a/src/components/proxy-hosts/HostDialogs.tsx
+++ b/src/components/proxy-hosts/HostDialogs.tsx
@@ -21,6 +21,8 @@ import { UpstreamDnsResolutionFields } from "./UpstreamDnsResolutionFields";
import { UpstreamInput } from "./UpstreamInput";
import { GeoBlockFields } from "./GeoBlockFields";
import { WafFields } from "./WafFields";
+import { MtlsFields } from "./MtlsConfig";
+import type { CaCertificate } from "@/src/lib/models/ca-certificates";
export function CreateHostDialog({
open,
@@ -28,7 +30,8 @@ export function CreateHostDialog({
certificates,
accessLists,
authentikDefaults,
- initialData
+ initialData,
+ caCertificates = []
}: {
open: boolean;
onClose: () => void;
@@ -36,6 +39,7 @@ export function CreateHostDialog({
accessLists: AccessList[];
authentikDefaults: AuthentikSettings | null;
initialData?: ProxyHost | null;
+ caCertificates?: CaCertificate[];
}) {
const [state, formAction] = useFormState(createProxyHostAction, INITIAL_ACTION_STATE);
@@ -130,6 +134,7 @@ export function CreateHostDialog({
+
);
@@ -140,13 +145,15 @@ export function EditHostDialog({
host,
onClose,
certificates,
- accessLists
+ accessLists,
+ caCertificates = []
}: {
open: boolean;
host: ProxyHost;
onClose: () => void;
certificates: Certificate[];
accessLists: AccessList[];
+ caCertificates?: CaCertificate[];
}) {
const [state, formAction] = useFormState(updateProxyHostAction.bind(null, host.id), INITIAL_ACTION_STATE);
@@ -234,6 +241,7 @@ export function EditHostDialog({
}}
/>
+
);
diff --git a/src/components/proxy-hosts/MtlsConfig.tsx b/src/components/proxy-hosts/MtlsConfig.tsx
new file mode 100644
index 00000000..69a189f8
--- /dev/null
+++ b/src/components/proxy-hosts/MtlsConfig.tsx
@@ -0,0 +1,126 @@
+"use client";
+
+import {
+ Alert,
+ Box,
+ Checkbox,
+ Collapse,
+ FormControlLabel,
+ Stack,
+ Switch,
+ Typography,
+} from "@mui/material";
+import LockPersonIcon from "@mui/icons-material/LockPerson";
+import { useState } from "react";
+import type { CaCertificate } from "@/src/lib/models/ca-certificates";
+import type { MtlsConfig } from "@/src/lib/models/proxy-hosts";
+
+type Props = {
+ value?: MtlsConfig | null;
+ caCertificates: CaCertificate[];
+};
+
+export function MtlsFields({ value, caCertificates }: Props) {
+ const [enabled, setEnabled] = useState(value?.enabled ?? false);
+ const [selectedIds, setSelectedIds] = useState(value?.ca_certificate_ids ?? []);
+
+ function toggleId(id: number) {
+ setSelectedIds(prev =>
+ prev.includes(id) ? prev.filter(i => i !== id) : [...prev, id]
+ );
+ }
+
+ return (
+
+ theme.palette.mode === "dark" ? "rgba(2,136,209,0.06)" : "rgba(2,136,209,0.04)",
+ p: 2,
+ }}
+ >
+
+
+ {enabled && selectedIds.map(id => (
+
+ ))}
+
+ {/* Header */}
+
+
+
+
+
+
+
+ Mutual TLS (mTLS)
+
+
+ Require clients to present a certificate signed by a trusted CA
+
+
+
+ setEnabled(checked)}
+ sx={{ flexShrink: 0 }}
+ />
+
+
+
+
+
+ mTLS requires TLS to be configured on this host (certificate must be set).
+
+
+
+ Trusted Client CA Certificates
+
+
+ {caCertificates.length === 0 ? (
+
+ No CA certificates configured. Add them on the Certificates page.
+
+ ) : (
+
+ {caCertificates.map(ca => (
+ toggleId(ca.id)}
+ size="small"
+ />
+ }
+ label={
+ {ca.name}
+ }
+ />
+ ))}
+
+ )}
+
+
+
+ );
+}
diff --git a/src/lib/caddy.ts b/src/lib/caddy.ts
index fe59e059..c4c1909b 100644
--- a/src/lib/caddy.ts
+++ b/src/lib/caddy.ts
@@ -27,9 +27,10 @@ import { syncInstances } from "./instance-sync";
import {
accessListEntries,
certificates,
+ caCertificates,
proxyHosts
} from "./db/schema";
-import { type GeoBlockMode, type WafHostConfig } from "./models/proxy-hosts";
+import { type GeoBlockMode, type WafHostConfig, type MtlsConfig } from "./models/proxy-hosts";
const CERTS_DIR = process.env.CERTS_DIRECTORY || join(process.cwd(), "data", "certs");
mkdirSync(CERTS_DIR, { recursive: true, mode: 0o700 });
@@ -687,6 +688,12 @@ function writeCertificateFiles(cert: CertificateRow) {
return { certificate_file: certPath, key_file: keyPath };
}
+function writeCaCertFile(caCert: { id: number; certificatePem: string }): string {
+ const caPath = join(CERTS_DIR, `ca-certificate-${caCert.id}.pem`);
+ writeFileSync(caPath, caCert.certificatePem, { encoding: "utf-8", mode: 0o600 });
+ return caPath;
+}
+
function collectCertificateUsage(rows: ProxyHostRow[], certificates: Map) {
const usage = new Map();
const autoManagedDomains = new Set();
@@ -1311,10 +1318,41 @@ async function buildProxyRoutes(
return routes;
}
+function buildClientAuthentication(
+ domains: string[],
+ mTlsDomainMap: Map,
+ caCertMap: Map
+): Record | null {
+ // Collect all CA cert IDs for any domain in this policy that has mTLS
+ const caCertIds = new Set();
+ for (const domain of domains) {
+ const ids = mTlsDomainMap.get(domain.toLowerCase());
+ if (ids) {
+ for (const id of ids) caCertIds.add(id);
+ }
+ }
+ if (caCertIds.size === 0) return null;
+
+ const pemFiles: string[] = [];
+ for (const id of caCertIds) {
+ const ca = caCertMap.get(id);
+ if (!ca) continue;
+ pemFiles.push(writeCaCertFile(ca));
+ }
+ if (pemFiles.length === 0) return null;
+
+ return {
+ trusted_ca_certs_pem_files: pemFiles,
+ mode: "require_and_verify"
+ };
+}
+
function buildTlsConnectionPolicies(
usage: Map,
managedCertificatesWithAutomation: Set,
- autoManagedDomains: Set
+ autoManagedDomains: Set,
+ mTlsDomainMap: Map,
+ caCertMap: Map
) {
const policies: Record[] = [];
const readyCertificates = new Set();
@@ -1322,11 +1360,26 @@ function buildTlsConnectionPolicies(
// Add policy for auto-managed domains (certificate_id = null)
if (autoManagedDomains.size > 0) {
const domains = Array.from(autoManagedDomains);
- policies.push({
- match: {
- sni: domains
+ const clientAuth = buildClientAuthentication(domains, mTlsDomainMap, caCertMap);
+
+ if (clientAuth) {
+ // Split: mTLS domains get their own policy, non-mTLS get another
+ const mTlsDomains = domains.filter(d => mTlsDomainMap.has(d));
+ const nonMTlsDomains = domains.filter(d => !mTlsDomainMap.has(d));
+
+ if (mTlsDomains.length > 0) {
+ const mTlsAuth = buildClientAuthentication(mTlsDomains, mTlsDomainMap, caCertMap);
+ policies.push({
+ match: { sni: mTlsDomains },
+ client_authentication: mTlsAuth
+ });
}
- });
+ if (nonMTlsDomains.length > 0) {
+ policies.push({ match: { sni: nonMTlsDomains } });
+ }
+ } else {
+ policies.push({ match: { sni: domains } });
+ }
}
for (const [id, entry] of usage.entries()) {
@@ -1340,12 +1393,28 @@ function buildTlsConnectionPolicies(
if (!files) {
continue;
}
- policies.push({
- match: {
- sni: domains
- },
- certificates: [files]
- });
+
+ const mTlsDomains = domains.filter(d => mTlsDomainMap.has(d));
+ const nonMTlsDomains = domains.filter(d => !mTlsDomainMap.has(d));
+
+ if (mTlsDomains.length > 0) {
+ const mTlsAuth = buildClientAuthentication(mTlsDomains, mTlsDomainMap, caCertMap);
+ policies.push({
+ match: { sni: mTlsDomains },
+ certificates: [files],
+ ...(mTlsAuth ? { client_authentication: mTlsAuth } : {})
+ });
+ }
+ if (nonMTlsDomains.length > 0) {
+ policies.push({
+ match: { sni: nonMTlsDomains },
+ certificates: [files]
+ });
+ }
+ if (mTlsDomains.length === 0 && nonMTlsDomains.length === 0) {
+ // all domains handled above
+ }
+
readyCertificates.add(id);
continue;
}
@@ -1354,11 +1423,21 @@ function buildTlsConnectionPolicies(
if (!managedCertificatesWithAutomation.has(id)) {
continue;
}
- policies.push({
- match: {
- sni: domains
- }
- });
+
+ const mTlsDomains = domains.filter(d => mTlsDomainMap.has(d));
+ const nonMTlsDomains = domains.filter(d => !mTlsDomainMap.has(d));
+
+ if (mTlsDomains.length > 0) {
+ const mTlsAuth = buildClientAuthentication(mTlsDomains, mTlsDomainMap, caCertMap);
+ policies.push({
+ match: { sni: mTlsDomains },
+ ...(mTlsAuth ? { client_authentication: mTlsAuth } : {})
+ });
+ }
+ if (nonMTlsDomains.length > 0) {
+ policies.push({ match: { sni: nonMTlsDomains } });
+ }
+
readyCertificates.add(id);
}
}
@@ -1506,7 +1585,7 @@ async function buildTlsAutomation(
}
async function buildCaddyDocument() {
- const [proxyHostRecords, certRows, accessListEntryRecords] = await Promise.all([
+ const [proxyHostRecords, certRows, accessListEntryRecords, caCertRows] = await Promise.all([
db
.select({
id: proxyHosts.id,
@@ -1543,7 +1622,13 @@ async function buildCaddyDocument() {
username: accessListEntries.username,
passwordHash: accessListEntries.passwordHash
})
- .from(accessListEntries)
+ .from(accessListEntries),
+ db
+ .select({
+ id: caCertificates.id,
+ certificatePem: caCertificates.certificatePem
+ })
+ .from(caCertificates)
]);
const proxyHostRows: ProxyHostRow[] = proxyHostRecords.map((h) => ({
@@ -1581,6 +1666,7 @@ async function buildCaddyDocument() {
}));
const certificateMap = new Map(certRowsMapped.map((cert) => [cert.id, cert]));
+ const caCertMap = new Map(caCertRows.map((ca) => [ca.id, ca]));
const accessMap = accessListEntryRows.reduce