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
}