From a6204ae8dac8822dc5cd656d503d2430e5dc4e9d Mon Sep 17 00:00:00 2001 From: Owen Date: Wed, 9 Sep 2026 17:02:47 -0400 Subject: [PATCH] Seperate dns from certificates --- server/private/lib/certificates/index.ts | 27 +++-- server/private/lib/certificates/scheduler.ts | 111 ++++++++++++------- server/private/lib/readConfigFile.ts | 1 + 3 files changed, 88 insertions(+), 51 deletions(-) diff --git a/server/private/lib/certificates/index.ts b/server/private/lib/certificates/index.ts index 399881611..6097629a7 100644 --- a/server/private/lib/certificates/index.ts +++ b/server/private/lib/certificates/index.ts @@ -18,17 +18,26 @@ import { jobScheduler } from "./scheduler"; export async function startCertificateManager() { const acmeConfig = privateConfig.getRawPrivateConfig().acme; - if (!acmeConfig || acmeConfig.cert_mode !== "pangolin") { - return; + if ( + acmeConfig && + acmeConfig.cert_mode === "pangolin" && + acmeConfig.enable_acme_client + ) { + logger.info("Starting certificate management server..."); + + // Initialize ACME client + await acmeClientManager.initialize(); + + // Start certificate issuance/renewal jobs + await jobScheduler.start(); } - logger.info("Starting certificate management server..."); - - // Initialize ACME client - await acmeClientManager.initialize(); - - // Start job scheduler - await jobScheduler.start(); + if (privateConfig.getRawPrivateConfig().flags.use_pangolin_dns) { + // DNS record validation/reverification doesn't require certs, so it + // runs whenever Pangolin is acting as the authoritative DNS server, + // independent of the cert manager above. + await jobScheduler.startDnsJobs(); + } } export async function stopCertificateManager() { diff --git a/server/private/lib/certificates/scheduler.ts b/server/private/lib/certificates/scheduler.ts index 70ec674eb..44d3d5b73 100644 --- a/server/private/lib/certificates/scheduler.ts +++ b/server/private/lib/certificates/scheduler.ts @@ -28,8 +28,10 @@ import { domainReverifier } from "./domain-reverifier"; const RUN_EXCLUSIVE_TIMEOUT_MS = 30 * 60 * 1000; export class JobScheduler { - private intervals: NodeJS.Timeout[] = []; - private running = false; + private certIntervals: NodeJS.Timeout[] = []; + private dnsIntervals: NodeJS.Timeout[] = []; + private certRunning = false; + private dnsRunning = false; // Guards against a slow batch (e.g. 10 certs whose DNS challenges take a // while) still being processed when the next interval tick fires - @@ -59,19 +61,19 @@ export class JobScheduler { }; } + // Certificate issuance/renewal - requires an ACME client, so this is + // only started when Pangolin is actually managing certs. async start(): Promise { - if (this.running) { - logger.warn("Scheduler is already running"); + if (this.certRunning) { + logger.warn("Certificate job scheduler is already running"); return; } - this.running = true; - logger.info("Starting job scheduler"); + this.certRunning = true; + logger.info("Starting certificate job scheduler"); const newCertState = { active: false }; const renewalState = { active: false }; - const dnsValidationState = { active: false }; - const reverifyState = { active: false }; const runNewCertCheck = this.runExclusive( () => certificateService.processPendingCertificates(), @@ -83,16 +85,6 @@ export class JobScheduler { renewalState, "processing renewal candidates" ); - const runDnsValidation = this.runExclusive( - () => dnsValidator.validateAll(), - dnsValidationState, - "validating DNS records" - ); - const runReverify = this.runExclusive( - () => domainReverifier.reverifyAll(), - reverifyState, - "reverifying domains" - ); // Schedule new certificate processing const newCertInterval = setInterval( @@ -106,10 +98,51 @@ export class JobScheduler { config.getRawConfig().acme!.renewal_check_interval_ms ); + this.certIntervals.push(newCertInterval, renewalInterval); + + // Run initial checks + setTimeout(async () => { + try { + await runNewCertCheck(); + // await runRenewalCheck(); + } catch (error) { + logger.error("Error in initial certificate processing:", error); + } + }, 1000); // Wait 1 second after startup + + logger.info("Certificate job scheduler started successfully"); + } + + // DNS record validation/reverification - doesn't touch certs at all, so + // this runs independently whenever Pangolin is acting as the + // authoritative DNS server, regardless of cert_mode. + async startDnsJobs(): Promise { + if (this.dnsRunning) { + logger.warn("DNS validation job scheduler is already running"); + return; + } + + this.dnsRunning = true; + logger.info("Starting DNS validation job scheduler"); + + const dnsValidationState = { active: false }; + const reverifyState = { active: false }; + + const runDnsValidation = this.runExclusive( + () => dnsValidator.validateAll(), + dnsValidationState, + "validating DNS records" + ); + const runReverify = this.runExclusive( + () => domainReverifier.reverifyAll(), + reverifyState, + "reverifying domains" + ); + // Schedule DNS validation const dnsValidationInterval = setInterval( runDnsValidation, - config.getRawConfig().acme?.dns_check_interval_ms + config.getRawConfig().acme?.dns_check_interval_ms ?? 60000 ); // Schedule periodic reverification of already-verified domains @@ -119,44 +152,38 @@ export class JobScheduler { 3600000 ); - this.intervals.push( - newCertInterval, - renewalInterval, - dnsValidationInterval, - reverifyInterval - ); + this.dnsIntervals.push(dnsValidationInterval, reverifyInterval); - // Run initial checks + // Run an initial validation pass shortly after startup setTimeout(async () => { try { - await runNewCertCheck(); - // await runRenewalCheck(); await runDnsValidation(); } catch (error) { - logger.error("Error in initial certificate processing:", error); + logger.error("Error in initial DNS validation:", error); } - }, 1000); // Wait 5 seconds after startup + }, 1000); - logger.info("Job scheduler started successfully"); + logger.info("DNS validation job scheduler started successfully"); } async stop(): Promise { - if (!this.running) { - return; + if (this.certRunning) { + logger.info("Stopping certificate job scheduler"); + this.certRunning = false; + this.certIntervals.forEach((interval) => clearInterval(interval)); + this.certIntervals = []; } - logger.info("Stopping job scheduler"); - this.running = false; - - // Clear all intervals - this.intervals.forEach((interval) => clearInterval(interval)); - this.intervals = []; - - logger.info("Job scheduler stopped"); + if (this.dnsRunning) { + logger.info("Stopping DNS validation job scheduler"); + this.dnsRunning = false; + this.dnsIntervals.forEach((interval) => clearInterval(interval)); + this.dnsIntervals = []; + } } isRunning(): boolean { - return this.running; + return this.certRunning || this.dnsRunning; } } diff --git a/server/private/lib/readConfigFile.ts b/server/private/lib/readConfigFile.ts index 18a80af62..5f1309fc1 100644 --- a/server/private/lib/readConfigFile.ts +++ b/server/private/lib/readConfigFile.ts @@ -195,6 +195,7 @@ export const privateConfigSchema = z .enum(["traefik", "pangolin"]) .optional() .default("traefik"), + enable_acme_client: z.boolean().optional().default(false), // @deprecated Moved to the public config file // (server/lib/readConfigFile.ts). Kept here only so existing private // config files keep parsing; any value set here is migrated into the