From b36a7ab9a168e14367e232fdd5b6bf9eaf230c47 Mon Sep 17 00:00:00 2001 From: Pascal Date: Mon, 7 Apr 2025 16:37:09 +0200 Subject: [PATCH 1/2] caddytls: Regularly reload static certificates --- modules/caddytls/tls.go | 93 ++++++++++++++++++++++++++++++++--------- 1 file changed, 73 insertions(+), 20 deletions(-) diff --git a/modules/caddytls/tls.go b/modules/caddytls/tls.go index 0f8433960..a23a51e2f 100644 --- a/modules/caddytls/tls.go +++ b/modules/caddytls/tls.go @@ -122,13 +122,15 @@ type TLS struct { DNSRaw json.RawMessage `json:"dns,omitempty" caddy:"namespace=dns.providers inline_key=name"` dns any // technically, it should be any/all of the libdns interfaces (RecordSetter, RecordAppender, etc.) - certificateLoaders []CertificateLoader - automateNames []string - ctx caddy.Context - storageCleanTicker *time.Ticker - storageCleanStop chan struct{} - logger *zap.Logger - events *caddyevents.App + magic *certmagic.Config + certificateLoaders []CertificateLoader + unmanagedCertsTicker *time.Ticker + automateNames []string + ctx caddy.Context + storageCleanTicker *time.Ticker + storageCleanStop chan struct{} + logger *zap.Logger + events *caddyevents.App serverNames map[string]struct{} serverNamesMu *sync.Mutex @@ -238,7 +240,7 @@ func (t *TLS) Provision(ctx caddy.Context) error { // certificates have been manually loaded, and also so that // commands like validate can be a better test certCacheMu.RLock() - magic := certmagic.New(certCache, certmagic.Config{ + t.magic = certmagic.New(certCache, certmagic.Config{ Storage: ctx.Storage(), Logger: t.logger, OnEvent: t.onEvent, @@ -248,18 +250,14 @@ func (t *TLS) Provision(ctx caddy.Context) error { DisableStorageCheck: t.DisableStorageCheck, }) certCacheMu.RUnlock() - for _, loader := range t.certificateLoaders { - certs, err := loader.LoadCertificates() - if err != nil { - return fmt.Errorf("loading certificates: %v", err) - } - for _, cert := range certs { - hash, err := magic.CacheUnmanagedTLSCertificate(ctx, cert.Certificate, cert.Tags) - if err != nil { - return fmt.Errorf("caching unmanaged certificate: %v", err) - } - t.loaded[hash] = "" - } + + unmanaged, err := t.loadUnmanagedCertificates(ctx) + if err != nil { + return err + } + + if unmanaged > 0 { + t.regularlyReloadUnmanagedCertificates() } // on-demand permission module @@ -720,6 +718,61 @@ func (t *TLS) HasCertificateForSubject(subject string) bool { return false } +func (t *TLS) loadUnmanagedCertificates(ctx caddy.Context) (int, error) { + cached := 0 + for _, loader := range t.certificateLoaders { + certs, err := loader.LoadCertificates() + if err != nil { + return 0, fmt.Errorf("loading certificates: %v", err) + } + for _, cert := range certs { + hash, err := t.magic.CacheUnmanagedTLSCertificate(ctx, cert.Certificate, cert.Tags) + if err != nil { + return 0, fmt.Errorf("caching unmanaged certificate: %v", err) + } + t.loaded[hash] = "" + } + } + return cached, nil +} + +func (t *TLS) regularlyReloadUnmanagedCertificates() { + t.unmanagedCertsTicker = time.NewTicker(2 * time.Hour) + go func() { + defer func() { + if err := recover(); err != nil { + log.Printf("[PANIC] unmanaged certificates reloader: %v\n%s", err, debug.Stack()) + } + }() + t.reloadUnmanagedCertificates() + for { + select { + case <-t.storageCleanStop: + return + case <-t.storageCleanTicker.C: + t.cleanStorageUnits() + } + } + }() +} + +func (t *TLS) reloadUnmanagedCertificates() error { + for _, loader := range t.certificateLoaders { + certs, err := loader.LoadCertificates() + if err != nil { + return fmt.Errorf("loading certificates: %v", err) + } + for _, cert := range certs { + hash, err := t.magic.CacheUnmanagedTLSCertificate(t.ctx, cert.Certificate, cert.Tags) + if err != nil { + return fmt.Errorf("caching unmanaged certificate: %v", err) + } + t.loaded[hash] = "" + } + } + return nil +} + // keepStorageClean starts a goroutine that immediately cleans up all // known storage units if it was not recently done, and then runs the // operation at every tick from t.storageCleanTicker. From a874bb3e4dcfafb1372c310d9d43c321872240e0 Mon Sep 17 00:00:00 2001 From: Pascal Date: Wed, 9 Apr 2025 12:36:29 +0200 Subject: [PATCH 2/2] caddytls: Regularly reload static certificates --- modules/caddytls/storageloader.go | 15 +++-- modules/caddytls/tls.go | 91 ++++++++----------------------- 2 files changed, 31 insertions(+), 75 deletions(-) diff --git a/modules/caddytls/storageloader.go b/modules/caddytls/storageloader.go index c9487e892..46705d426 100644 --- a/modules/caddytls/storageloader.go +++ b/modules/caddytls/storageloader.go @@ -72,17 +72,16 @@ func (sl *StorageLoader) Provision(ctx caddy.Context) error { return nil } -// LoadCertificates returns the certificates to be loaded by sl. -func (sl StorageLoader) LoadCertificates() ([]Certificate, error) { +func (sl StorageLoader) Initialize(updateCertificates func(add []Certificate, remove []string) error) error { certs := make([]Certificate, 0, len(sl.Pairs)) for _, pair := range sl.Pairs { certData, err := sl.storage.Load(sl.ctx, pair.Certificate) if err != nil { - return nil, err + return err } keyData, err := sl.storage.Load(sl.ctx, pair.Key) if err != nil { - return nil, err + return err } var cert tls.Certificate @@ -94,21 +93,21 @@ func (sl StorageLoader) LoadCertificates() ([]Certificate, error) { // if the start of the key file looks like an encrypted private key, // reject it with a helpful error message if strings.Contains(string(keyData[:40]), "ENCRYPTED") { - return nil, fmt.Errorf("encrypted private keys are not supported; please decrypt the key first") + return fmt.Errorf("encrypted private keys are not supported; please decrypt the key first") } cert, err = tls.X509KeyPair(certData, keyData) default: - return nil, fmt.Errorf("unrecognized certificate/key encoding format: %s", pair.Format) + return fmt.Errorf("unrecognized certificate/key encoding format: %s", pair.Format) } if err != nil { - return nil, err + return err } certs = append(certs, Certificate{Certificate: cert, Tags: pair.Tags}) } - return certs, nil + return updateCertificates(certs, []string{}) } // Interface guard diff --git a/modules/caddytls/tls.go b/modules/caddytls/tls.go index a23a51e2f..6cced20e0 100644 --- a/modules/caddytls/tls.go +++ b/modules/caddytls/tls.go @@ -122,15 +122,13 @@ type TLS struct { DNSRaw json.RawMessage `json:"dns,omitempty" caddy:"namespace=dns.providers inline_key=name"` dns any // technically, it should be any/all of the libdns interfaces (RecordSetter, RecordAppender, etc.) - magic *certmagic.Config - certificateLoaders []CertificateLoader - unmanagedCertsTicker *time.Ticker - automateNames []string - ctx caddy.Context - storageCleanTicker *time.Ticker - storageCleanStop chan struct{} - logger *zap.Logger - events *caddyevents.App + certificateLoaders []CertificateLoader + automateNames []string + ctx caddy.Context + storageCleanTicker *time.Ticker + storageCleanStop chan struct{} + logger *zap.Logger + events *caddyevents.App serverNames map[string]struct{} serverNamesMu *sync.Mutex @@ -240,7 +238,7 @@ func (t *TLS) Provision(ctx caddy.Context) error { // certificates have been manually loaded, and also so that // commands like validate can be a better test certCacheMu.RLock() - t.magic = certmagic.New(certCache, certmagic.Config{ + magic := certmagic.New(certCache, certmagic.Config{ Storage: ctx.Storage(), Logger: t.logger, OnEvent: t.onEvent, @@ -251,13 +249,13 @@ func (t *TLS) Provision(ctx caddy.Context) error { }) certCacheMu.RUnlock() - unmanaged, err := t.loadUnmanagedCertificates(ctx) - if err != nil { - return err - } - - if unmanaged > 0 { - t.regularlyReloadUnmanagedCertificates() + for _, loader := range t.certificateLoaders { + err := loader.Initialize(func(add []Certificate, remove []string) error { + return t.updateCertificates(ctx, magic, add, remove) + }) + if err != nil { + return fmt.Errorf("loading certificates: %v", err) + } } // on-demand permission module @@ -718,58 +716,17 @@ func (t *TLS) HasCertificateForSubject(subject string) bool { return false } -func (t *TLS) loadUnmanagedCertificates(ctx caddy.Context) (int, error) { - cached := 0 - for _, loader := range t.certificateLoaders { - certs, err := loader.LoadCertificates() +func (t *TLS) updateCertificates(ctx caddy.Context, magic *certmagic.Config, add []Certificate, remove []string) error { + for _, cert := range add { + hash, err := magic.CacheUnmanagedTLSCertificate(ctx, cert.Certificate, cert.Tags) if err != nil { - return 0, fmt.Errorf("loading certificates: %v", err) - } - for _, cert := range certs { - hash, err := t.magic.CacheUnmanagedTLSCertificate(ctx, cert.Certificate, cert.Tags) - if err != nil { - return 0, fmt.Errorf("caching unmanaged certificate: %v", err) - } - t.loaded[hash] = "" - } - } - return cached, nil -} - -func (t *TLS) regularlyReloadUnmanagedCertificates() { - t.unmanagedCertsTicker = time.NewTicker(2 * time.Hour) - go func() { - defer func() { - if err := recover(); err != nil { - log.Printf("[PANIC] unmanaged certificates reloader: %v\n%s", err, debug.Stack()) - } - }() - t.reloadUnmanagedCertificates() - for { - select { - case <-t.storageCleanStop: - return - case <-t.storageCleanTicker.C: - t.cleanStorageUnits() - } - } - }() -} - -func (t *TLS) reloadUnmanagedCertificates() error { - for _, loader := range t.certificateLoaders { - certs, err := loader.LoadCertificates() - if err != nil { - return fmt.Errorf("loading certificates: %v", err) - } - for _, cert := range certs { - hash, err := t.magic.CacheUnmanagedTLSCertificate(t.ctx, cert.Certificate, cert.Tags) - if err != nil { - return fmt.Errorf("caching unmanaged certificate: %v", err) - } - t.loaded[hash] = "" + return fmt.Errorf("caching unmanaged certificate: %v", err) } + t.loaded[hash] = "" } + certCacheMu.Lock() + certCache.Remove(remove) + certCacheMu.Unlock() return nil } @@ -878,7 +835,7 @@ func (t *TLS) onEvent(ctx context.Context, eventName string, data map[string]any // CertificateLoader is a type that can load certificates. // Certificates can optionally be associated with tags. type CertificateLoader interface { - LoadCertificates() ([]Certificate, error) + Initialize(updateCertificates func(add []Certificate, remove []string) error) error } // Certificate is a TLS certificate, optionally