From 69740229060bcd467c2da7be649de3e46d1d598b Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 4 Nov 2025 20:36:59 +0000 Subject: [PATCH] Fix Caddy Auto certificate management for proxy hosts Previously, proxy hosts with "Managed by Caddy (Auto)" (certificate_id = null) were being skipped during Caddy configuration generation, causing the feature to not work at all. This commit adds full support for automatic certificate management: 1. Modified collectCertificateUsage() to track domains with null certificate_id separately as auto-managed domains 2. Updated buildTlsAutomation() to create ACME automation policies for auto-managed domains (supports both HTTP-01 and DNS-01 challenges) 3. Modified buildTlsConnectionPolicies() to include TLS connection policies for auto-managed domains 4. Updated buildProxyRoutes() to allow proxy hosts with null certificate_id to be included in the route configuration The configuration now automatically updates when domains are changed, as applyCaddyConfig() is already called on create/update/delete operations. Caddy will now automatically obtain and manage Let's Encrypt certificates for all domains when "Managed by Caddy (Auto)" is selected. --- src/lib/caddy.ts | 97 ++++++++++++++++++++++++++++++++++++++---------- 1 file changed, 78 insertions(+), 19 deletions(-) diff --git a/src/lib/caddy.ts b/src/lib/caddy.ts index 7e1ee291..90cc9b9e 100644 --- a/src/lib/caddy.ts +++ b/src/lib/caddy.ts @@ -191,14 +191,10 @@ function writeCertificateFiles(cert: CertificateRow) { function collectCertificateUsage(rows: ProxyHostRow[], certificates: Map) { const usage = new Map(); + const autoManagedDomains = new Set(); for (const row of rows) { - if (!row.enabled || !row.certificate_id) { - continue; - } - - const cert = certificates.get(row.certificate_id); - if (!cert) { + if (!row.enabled) { continue; } @@ -208,6 +204,19 @@ function collectCertificateUsage(rows: ProxyHostRow[], certificates: Map, - tlsReadyCertificates: Set + tlsReadyCertificates: Set, + autoManagedDomains: Set ): CaddyHttpRoute[] { const routes: CaddyHttpRoute[] = []; @@ -236,7 +246,11 @@ function buildProxyRoutes( continue; } - if (!row.certificate_id || !tlsReadyCertificates.has(row.certificate_id)) { + // Allow hosts with certificate_id = null (Caddy Auto) or with valid certificate IDs + const isAutoManaged = !row.certificate_id; + const hasValidCertificate = row.certificate_id && tlsReadyCertificates.has(row.certificate_id); + + if (!isAutoManaged && !hasValidCertificate) { continue; } @@ -471,11 +485,22 @@ function buildDeadRoutes(rows: DeadHostRow[]): CaddyHttpRoute[] { function buildTlsConnectionPolicies( usage: Map, - managedCertificatesWithAutomation: Set + managedCertificatesWithAutomation: Set, + autoManagedDomains: Set ) { const policies: Record[] = []; const readyCertificates = new Set(); + // Add policy for auto-managed domains (certificate_id = null) + if (autoManagedDomains.size > 0) { + const domains = Array.from(autoManagedDomains); + policies.push({ + match: { + sni: domains + } + }); + } + for (const [id, entry] of usage.entries()) { const domains = Array.from(entry.domains); if (domains.length === 0) { @@ -518,13 +543,16 @@ function buildTlsConnectionPolicies( async function buildTlsAutomation( usage: Map, + autoManagedDomains: Set, options: { acmeEmail?: string } ) { const managedEntries = Array.from(usage.values()).filter( (entry) => entry.certificate.type === "managed" && Boolean(entry.certificate.auto_renew) ); - if (managedEntries.length === 0) { + const hasAutoManagedDomains = autoManagedDomains.size > 0; + + if (managedEntries.length === 0 && !hasAutoManagedDomains) { return { managedCertificateIds: new Set() }; @@ -536,6 +564,40 @@ async function buildTlsAutomation( const managedCertificateIds = new Set(); const policies: Record[] = []; + // Add policy for auto-managed domains (certificate_id = null) + if (hasAutoManagedDomains) { + const subjects = Array.from(autoManagedDomains); + + // Build issuer configuration + const issuer: Record = { + module: "acme" + }; + + if (options.acmeEmail) { + issuer.email = options.acmeEmail; + } + + // Use DNS-01 challenge if Cloudflare is configured, otherwise use HTTP-01 + if (hasCloudflare) { + const providerConfig: Record = { + name: "cloudflare", + api_token: cloudflare.apiToken + }; + + issuer.challenges = { + dns: { + provider: providerConfig + } + }; + } + + policies.push({ + subjects, + issuers: [issuer] + }); + } + + // Add policies for explicitly managed certificates for (const entry of managedEntries) { const subjects = Array.from(entry.domains); if (subjects.length === 0) { @@ -555,13 +617,10 @@ async function buildTlsAutomation( // Use DNS-01 challenge if Cloudflare is configured, otherwise use HTTP-01 if (hasCloudflare) { - // The caddy-dns/cloudflare module only accepts api_token - // See: https://github.com/caddy-dns/cloudflare const providerConfig: Record = { name: "cloudflare", api_token: cloudflare.apiToken }; - // Note: zone_id and account_id are not supported by caddy-dns/cloudflare module issuer.challenges = { dns: { @@ -569,7 +628,6 @@ async function buildTlsAutomation( } }; } - // If no Cloudflare, Caddy will use HTTP-01 challenge by default policies.push({ subjects, @@ -718,18 +776,19 @@ async function buildCaddyDocument() { return map; }, new Map()); - const certificateUsage = collectCertificateUsage(proxyHostRows, certificateMap); + const { usage: certificateUsage, autoManagedDomains } = collectCertificateUsage(proxyHostRows, certificateMap); const generalSettings = await getGeneralSettings(); - const { tlsApp, managedCertificateIds } = await buildTlsAutomation(certificateUsage, { + const { tlsApp, managedCertificateIds } = await buildTlsAutomation(certificateUsage, autoManagedDomains, { acmeEmail: generalSettings?.acmeEmail }); const { policies: tlsConnectionPolicies, readyCertificates } = buildTlsConnectionPolicies( certificateUsage, - managedCertificateIds + managedCertificateIds, + autoManagedDomains ); const httpRoutes: CaddyHttpRoute[] = [ - ...buildProxyRoutes(proxyHostRows, accessMap, readyCertificates), + ...buildProxyRoutes(proxyHostRows, accessMap, readyCertificates, autoManagedDomains), ...buildRedirectRoutes(redirectHostRows), ...buildDeadRoutes(deadHostRows) ];