diff --git a/server/db/pg/schema/schema.ts b/server/db/pg/schema/schema.ts index 01fdd337..33b2ae28 100644 --- a/server/db/pg/schema/schema.ts +++ b/server/db/pg/schema/schema.ts @@ -19,7 +19,7 @@ export const domains = pgTable("domains", { verified: boolean("verified").notNull().default(false), failed: boolean("failed").notNull().default(false), tries: integer("tries").notNull().default(0), - certResolver: varchar("certResolver").default("letsencrypt"), + certResolver: varchar("certResolver"), customCertResolver: varchar("customCertResolver") }); diff --git a/server/db/sqlite/schema/schema.ts b/server/db/sqlite/schema/schema.ts index d0d972ff..ebfe7bcf 100644 --- a/server/db/sqlite/schema/schema.ts +++ b/server/db/sqlite/schema/schema.ts @@ -12,7 +12,7 @@ export const domains = sqliteTable("domains", { verified: integer("verified", { mode: "boolean" }).notNull().default(false), failed: integer("failed", { mode: "boolean" }).notNull().default(false), tries: integer("tries").notNull().default(0), - certResolver: text("certResolver").default("letsencrypt"), + certResolver: text("certResolver"), customCertResolver: text("customCertResolver") }); diff --git a/server/lib/readConfigFile.ts b/server/lib/readConfigFile.ts index 9aee8531..a1899f4e 100644 --- a/server/lib/readConfigFile.ts +++ b/server/lib/readConfigFile.ts @@ -51,6 +51,7 @@ export const configSchema = z .nonempty("base_domain must not be empty") .transform((url) => url.toLowerCase()), cert_resolver: z.string().optional().default("letsencrypt"), + custom_cert_resolver: z.string().optional(), prefer_wildcard_cert: z.boolean().optional().default(false) }) ) @@ -187,6 +188,7 @@ export const configSchema = z https_entrypoint: z.string().optional().default("websecure"), additional_middlewares: z.array(z.string()).optional(), cert_resolver: z.string().optional().default("letsencrypt"), + custom_cert_resolver: z.string().optional(), prefer_wildcard_cert: z.boolean().optional().default(false), certificates_path: z.string().default("/var/certificates"), monitor_interval: z.number().default(5000), diff --git a/server/lib/traefik/getTraefikConfig.ts b/server/lib/traefik/getTraefikConfig.ts index 75ea907f..734327e2 100644 --- a/server/lib/traefik/getTraefikConfig.ts +++ b/server/lib/traefik/getTraefikConfig.ts @@ -1,4 +1,4 @@ -import { db, targetHealthCheck } from "@server/db"; +import { db, targetHealthCheck, domains } from "@server/db"; import { and, eq, @@ -75,11 +75,15 @@ export async function getTraefikConfig( siteType: sites.type, siteOnline: sites.online, subnet: sites.subnet, - exitNodeId: sites.exitNodeId + exitNodeId: sites.exitNodeId, + // Domain cert resolver fields + domainCertResolver: domains.certResolver, + domainCustomCertResolver: domains.customCertResolver }) .from(sites) .innerJoin(targets, eq(targets.siteId, sites.siteId)) .innerJoin(resources, eq(resources.resourceId, targets.resourceId)) + .leftJoin(domains, eq(domains.domainId, resources.domainId)) .leftJoin( targetHealthCheck, eq(targetHealthCheck.targetId, targets.targetId) @@ -161,7 +165,10 @@ export async function getTraefikConfig( pathMatchType: row.pathMatchType, // the targets will all have the same pathMatchType rewritePath: row.rewritePath, rewritePathType: row.rewritePathType, - priority: priority // may be null, we fallback later + priority: priority, + // Store domain cert resolver fields + domainCertResolver: row.domainCertResolver, + domainCustomCertResolver: row.domainCustomCertResolver }); } @@ -242,18 +249,73 @@ export async function getTraefikConfig( const configDomain = config.getDomain(resource.domainId); - let certResolver: string, preferWildcardCert: boolean; - if (!configDomain) { - certResolver = config.getRawConfig().traefik.cert_resolver; - preferWildcardCert = - config.getRawConfig().traefik.prefer_wildcard_cert; + let certResolverFromConfig: string | undefined; + let preferWildcardCert = false; + + const rawTraefikCfg = config.getRawConfig().traefik || {}; + const globalDefaultResolver: string | undefined = rawTraefikCfg.cert_resolver; + const availableResolvers = rawTraefikCfg.custom_cert_resolver + ? Object.keys(rawTraefikCfg.custom_cert_resolver) + : []; + + // Priority 1: Read from YAML config (if exists) + if (configDomain) { + certResolverFromConfig = + configDomain.cert_resolver ?? + configDomain.custom_cert_resolver; + preferWildcardCert = !!(configDomain.prefer_wildcard_cert); + } + + // Priority 2: Override with database domain settings (if exists) + let finalCertResolver: string | undefined; + let finalCustomCertResolver: string | undefined; + + if (resource.domainCertResolver) { + finalCertResolver = resource.domainCertResolver; + if (resource.domainCertResolver === "custom" && resource.domainCustomCertResolver) { + finalCustomCertResolver = resource.domainCustomCertResolver; + } } else { - certResolver = configDomain.cert_resolver; - preferWildcardCert = configDomain.prefer_wildcard_cert; + // Fall back to config + finalCertResolver = certResolverFromConfig; + } + + // Resolve the final resolver name + let resolverName: string | undefined; + + if (finalCertResolver) { + if (finalCertResolver === "custom") { + // Check database custom resolver first, then config + const customResolver = finalCustomCertResolver || configDomain?.custom_cert_resolver; + + if (customResolver && typeof customResolver === "string" && customResolver.trim()) { + resolverName = customResolver.trim(); + } else { + resolverName = globalDefaultResolver; + logger.warn( + `Domain ${resource.domainId} requested custom cert resolver but none set; falling back to global resolver ${resolverName}` + ); + } + } else { + // Validate against available resolvers + if ( + availableResolvers.length === 0 || + availableResolvers.includes(finalCertResolver) + ) { + resolverName = finalCertResolver; + } else { + logger.warn( + `Unknown cert resolver "${finalCertResolver}" for domain ${resource.domainId}; falling back to global resolver.` + ); + resolverName = globalDefaultResolver; + } + } + } else { + resolverName = globalDefaultResolver; } const tls = { - certResolver: certResolver, + certResolver: resolverName, ...(preferWildcardCert ? { domains: [