From 3cae55bc4c1d2ec218dab981d21ec0bd5cf1079d Mon Sep 17 00:00:00 2001 From: Pascal Date: Wed, 9 Apr 2025 12:36:29 +0200 Subject: [PATCH] caddytls: Regularly reload static certificates --- .../reverseproxy/httptransport_test.go | 9 +- modules/caddytls/storageloader.go | 15 ++- modules/caddytls/tls.go | 91 +++++-------------- 3 files changed, 36 insertions(+), 79 deletions(-) diff --git a/modules/caddyhttp/reverseproxy/httptransport_test.go b/modules/caddyhttp/reverseproxy/httptransport_test.go index 88ac9d591..55ca3fd33 100644 --- a/modules/caddyhttp/reverseproxy/httptransport_test.go +++ b/modules/caddyhttp/reverseproxy/httptransport_test.go @@ -129,11 +129,11 @@ func TestHTTPTransport_DialTLSContext_ProxyProtocol(t *testing.T) { defer cancel() tests := []struct { - name string - tls *TLSConfig - proxyProtocol string + name string + tls *TLSConfig + proxyProtocol string serverNameHasPlaceholder bool - expectDialTLSContext bool + expectDialTLSContext bool }{ { name: "no TLS, no proxy protocol", @@ -194,4 +194,3 @@ func TestHTTPTransport_DialTLSContext_ProxyProtocol(t *testing.T) { }) } } - 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 366277fd0..70ba20d8a 100644 --- a/modules/caddytls/tls.go +++ b/modules/caddytls/tls.go @@ -131,14 +131,14 @@ type TLS struct { // EXPERIMENTAL: Subject to change. Resolvers []string `json:"resolvers,omitempty"` - dns any // technically, it should be any/all of the libdns interfaces (RecordSetter, RecordAppender, etc.) - certificateLoaders []CertificateLoader - automateNames map[string]struct{} - ctx caddy.Context - storageCleanTicker *time.Ticker - storageCleanStop chan struct{} - logger *zap.Logger - events *caddyevents.App + dns any // technically, it should be any/all of the libdns interfaces (RecordSetter, RecordAppender, etc.) + certificateLoaders []CertificateLoader + automateNames map[string]struct{} + ctx caddy.Context + storageCleanTicker *time.Ticker + storageCleanStop chan struct{} + logger *zap.Logger + events *caddyevents.App magic *certmagic.Config unmanagedCertsTicker *time.Ticker @@ -251,7 +251,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, @@ -262,13 +262,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 @@ -811,58 +811,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 } @@ -971,7 +930,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