add wildcard domain

rabbitprincess committed Mar 2, 2026 at 19:43 UTC 8754478f38ff9ad297fd9fd923648ac2ed3d5ad7
2 files changed +68 -6
portal/acme/acme.go
+52 -3
@@ -14,6 +14,7 @@ import (
14 "fmt"
15 "os"
16 "path/filepath"
17 + "strings"
18 "sync"
19
20 "github.com/go-acme/lego/v4/certcrypto"
@@ -148,10 +149,30 @@ func (m *AcmeManager) EnsureSigningKey(ctx context.Context) (string, error) {
149
150 missingTargets := make([]certTarget, 0, len(targets))
151 for _, target := range targets {
151 - if fileExists(target.KeyFile) && fileExists(target.CertFile) {
152 + if !fileExists(target.KeyFile) || !fileExists(target.CertFile) {
153 + missingTargets = append(missingTargets, target)
154 + continue
155 + }
156 +
157 + covered, coverageErr := certCoversDomains(target.CertFile, target.Domains)
158 + if coverageErr != nil {
159 + log.Warn().
160 + Err(coverageErr).
161 + Str("target", target.Name).
162 + Str("cert_file", target.CertFile).
163 + Msg("[signer] failed to validate existing certificate; re-issuing via ACME")
164 + missingTargets = append(missingTargets, target)
165 + continue
166 + }
167 + if !covered {
168 + log.Warn().
169 + Str("target", target.Name).
170 + Str("cert_file", target.CertFile).
171 + Strs("required_domains", target.Domains).
172 + Msg("[signer] existing certificate does not cover required domains; re-issuing via ACME")
173 + missingTargets = append(missingTargets, target)
174 continue
175 }
154 - missingTargets = append(missingTargets, target)
176 }
177 if len(missingTargets) == 0 {
178 return signerKeyFile, nil
@@ -182,7 +203,7 @@ func (m *AcmeManager) EnsureSigningKey(ctx context.Context) (string, error) {
203 Strs("domains", cfg.Domains).
204 Str("key_file", cfg.KeyFile).
205 Str("cert_file", cfg.CertFile).
185 - Msg("[signer] ACME target is missing; issuing certificate with ACME DNS-01 via Cloudflare")
206 + Msg("[signer] ACME target is missing or invalid; issuing certificate with ACME DNS-01 via Cloudflare")
207
208 if err := m.provisionCertificate(cfg); err != nil {
209 return "", err
@@ -262,6 +283,34 @@ func buildCertTargets(baseDomain, configuredKeyDir string) ([]certTarget, error)
283 }, nil
284 }
285
286 +func certCoversDomains(certFile string, domains []string) (bool, error) {
287 + certPEM, err := os.ReadFile(certFile)
288 + if err != nil {
289 + return false, err
290 + }
291 +
292 + cert, err := ParseCertificatePEM(certPEM)
293 + if err != nil {
294 + return false, err
295 + }
296 +
297 + for _, domain := range domains {
298 + if strings.HasPrefix(domain, "*.") {
299 + probeHost := "acme-probe." + strings.TrimPrefix(domain, "*.")
300 + if err := cert.VerifyHostname(probeHost); err != nil {
301 + return false, nil
302 + }
303 + continue
304 + }
305 +
306 + if err := cert.VerifyHostname(domain); err != nil {
307 + return false, nil
308 + }
309 + }
310 +
311 + return true, nil
312 +}
313 +
314 func (m *AcmeManager) provisionCertificate(cfg provisionConfig) error {
315 for _, path := range []string{cfg.KeyFile, cfg.CertFile, cfg.AccountKeyFile, cfg.RegistrationFile} {
316 if err := ensureParentDir(path); err != nil {
portal/acme/renew.go
+16 -3
@@ -88,7 +88,7 @@ func (m *AcmeManager) shouldRenew() bool {
88 }
89
90 for _, target := range targets {
91 - needsRenewal, checkErr := certNeedsRenewal(target.CertFile)
91 + needsRenewal, checkErr := certNeedsRenewal(target.CertFile, target.Domains)
92 if checkErr != nil {
93 log.Debug().
94 Err(checkErr).
@@ -104,7 +104,7 @@ func (m *AcmeManager) shouldRenew() bool {
104 return false
105 }
106
107 -func certNeedsRenewal(certFile string) (bool, error) {
107 +func certNeedsRenewal(certFile string, domains []string) (bool, error) {
108 certPEM, err := os.ReadFile(certFile)
109 if err != nil {
110 return false, err
@@ -117,6 +117,19 @@ func certNeedsRenewal(certFile string) (bool, error) {
117
118 timeUntilExpiry := time.Until(cert.NotAfter)
119 needsRenewal := timeUntilExpiry < RenewalThreshold
120 + if !needsRenewal {
121 + covered, coverageErr := certCoversDomains(certFile, domains)
122 + if coverageErr != nil {
123 + return false, coverageErr
124 + }
125 + if !covered {
126 + log.Warn().
127 + Str("cert_file", certFile).
128 + Strs("required_domains", domains).
129 + Msg("[acme] certificate does not cover required domains; renewal required")
130 + needsRenewal = true
131 + }
132 + }
133 if needsRenewal {
134 log.Info().
135 Time("not_after", cert.NotAfter).
@@ -152,7 +165,7 @@ func (m *AcmeManager) renewCertificate(ctx context.Context) error {
165 }
166
167 for _, target := range targets {
155 - needsRenewal, checkErr := certNeedsRenewal(target.CertFile)
168 + needsRenewal, checkErr := certNeedsRenewal(target.CertFile, target.Domains)
169 if checkErr != nil {
170 continue
171 }