From 8626fa370382d9b37c9732cb15730ed29d5f7168 Mon Sep 17 00:00:00 2001 From: Jens-Uwe Mager Date: Sat, 5 Sep 2026 15:37:57 +0200 Subject: [PATCH] caddytls: synchronize storage cleaner with TLS.Stop via context and WaitGroup (#7954) * caddytls: synchronize storage cleaner with TLS.Stop via context and WaitGroup * Unify the storage cleanup synchronization to both tls and ech. * Cannot embed the sync.WaitGroup directly as the TLS struct is copied. * caddytls: propagate cancellable context to ECH rotation and add sync tests Pass cancellable context to ECH key rotation so in-flight storage locks and operations unblock when TLS.Stop is invoked. Add comprehensive tests verifying TLS.Stop cleanly unblocks and waits for storage cleaner and ECH workers. --------- Co-authored-by: Zen Dodd --- modules/caddytls/ech.go | 48 ++- modules/caddytls/storage_clean_test.go | 471 +++++++++++++++++++++++++ modules/caddytls/tls.go | 141 ++++++-- 3 files changed, 622 insertions(+), 38 deletions(-) create mode 100644 modules/caddytls/storage_clean_test.go diff --git a/modules/caddytls/ech.go b/modules/caddytls/ech.go index 4a48769d8..299789e32 100644 --- a/modules/caddytls/ech.go +++ b/modules/caddytls/ech.go @@ -101,8 +101,10 @@ func (ech *ECH) Provision(ctx caddy.Context) ([]string, error) { return nil, err } defer func() { - if err := storage.Unlock(ctx, echStorageLockName); err != nil { - logger.Error("unable to unlock ECH provisioning in storage", zap.Error(err)) + if err := storage.Unlock(context.WithoutCancel(ctx), echStorageLockName); err != nil { + if !errors.Is(err, context.Canceled) && ctx.Err() == nil { + logger.Error("unable to unlock ECH provisioning in storage", zap.Error(err)) + } } }() @@ -188,7 +190,16 @@ func (ech *ECH) setConfigsFromStorage(ctx caddy.Context, logger *zap.Logger) ([] // // This function sets/updates the stdlib-ready key list only if a rotation occurs. func (ech *ECH) rotateECHKeys(ctx caddy.Context, logger *zap.Logger, storageSynced bool) error { + if err := ctx.Err(); err != nil { + return err + } + if ctx.Context == nil { + return nil + } storage := ctx.Storage() + if storage == nil { + return nil + } // all existing configs are now loaded; rotate keys "regularly" as recommended by the spec // (also: "Rotating too frequently limits the client anonymity set." - but the more server @@ -208,8 +219,10 @@ func (ech *ECH) rotateECHKeys(ctx caddy.Context, logger *zap.Logger, storageSync return err } defer func() { - if err := storage.Unlock(ctx, echStorageLockName); err != nil { - logger.Error("unable to unlock ECH rotation in storage", zap.Error(err)) + if err := storage.Unlock(context.WithoutCancel(ctx), echStorageLockName); err != nil { + if !errors.Is(err, context.Canceled) && ctx.Err() == nil { + logger.Error("unable to unlock ECH rotation in storage", zap.Error(err)) + } } }() } @@ -221,7 +234,13 @@ func (ech *ECH) rotateECHKeys(ctx caddy.Context, logger *zap.Logger, storageSync // iterate the updated list and do any updates as needed for publicName := range ech.configs { + if err := ctx.Err(); err != nil { + return err + } for i := 0; i < len(ech.configs[publicName]); i++ { + if err := ctx.Err(); err != nil { + return err + } cfg := ech.configs[publicName][i] if time.Since(cfg.meta.Created) >= rotationInterval && cfg.meta.Replaced.IsZero() { // key is due for rotation and it hasn't been replaced yet; do that now @@ -311,16 +330,27 @@ func (ech *ECH) updateKeyList() { } // publishECHConfigs publishes any configs that are configured for publication and which haven't been published already. -func (t *TLS) publishECHConfigs(logger *zap.Logger) error { +func (t *TLS) publishECHConfigs(ctx context.Context, logger *zap.Logger) error { + if err := ctx.Err(); err != nil { + return err + } + if t.ctx.Context == nil { + return nil + } // make publication exclusive, since we don't need to repeat this unnecessarily storage := t.ctx.Storage() + if storage == nil { + return nil + } const echLockName = "ech_publish" - if err := storage.Lock(t.ctx, echLockName); err != nil { + if err := storage.Lock(ctx, echLockName); err != nil { return err } defer func() { - if err := storage.Unlock(t.ctx, echLockName); err != nil { - logger.Error("unable to unlock ECH provisioning in storage", zap.Error(err)) + if err := storage.Unlock(context.WithoutCancel(ctx), echLockName); err != nil { + if !errors.Is(err, context.Canceled) && ctx.Err() == nil { + logger.Error("unable to unlock ECH provisioning in storage", zap.Error(err)) + } } }() @@ -446,7 +476,7 @@ func (t *TLS) publishECHConfigs(logger *zap.Logger) error { // publish this ECH config list with this publisher pubTime := time.Now() - err := publisher.PublishECHConfigList(t.ctx, dnsNamesToPublish, echCfgListBin) + err := publisher.PublishECHConfigList(ctx, dnsNamesToPublish, echCfgListBin) var publishErrs PublishECHConfigListErrors if errors.As(err, &publishErrs) { diff --git a/modules/caddytls/storage_clean_test.go b/modules/caddytls/storage_clean_test.go new file mode 100644 index 000000000..4b10068ca --- /dev/null +++ b/modules/caddytls/storage_clean_test.go @@ -0,0 +1,471 @@ +// Copyright 2015 Matthew Holt and The Caddy Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package caddytls + +import ( + "context" + "io/fs" + "sync" + "testing" + "time" + + "github.com/caddyserver/caddy/v2" + "github.com/caddyserver/certmagic" + "github.com/cloudflare/circl/hpke" + "go.uber.org/zap" +) + +var ( + testStorageMu sync.Mutex + activeTestStorage certmagic.Storage +) + +type testStorageModule struct{} + +func (testStorageModule) CaddyModule() caddy.ModuleInfo { + return caddy.ModuleInfo{ + ID: "caddy.storage.test_blocking", + New: func() caddy.Module { return new(testStorageModule) }, + } +} + +func (testStorageModule) CertMagicStorage() (certmagic.Storage, error) { + testStorageMu.Lock() + defer testStorageMu.Unlock() + return activeTestStorage, nil +} + +func init() { + caddy.RegisterModule(testStorageModule{}) +} + +func newTestContextWithStorage(t *testing.T, storage certmagic.Storage) (caddy.Context, context.CancelFunc) { + testStorageMu.Lock() + activeTestStorage = storage + testStorageMu.Unlock() + + cfg := &caddy.Config{ + StorageRaw: []byte(`{"module": "test_blocking"}`), + } + ctx, err := caddy.ProvisionContext(cfg) + if err != nil { + t.Fatalf("caddy.ProvisionContext failed: %v", err) + } + return ctx, func() { + testStorageMu.Lock() + activeTestStorage = nil + testStorageMu.Unlock() + } +} + +type blockingStorage struct { + lockFunc func(ctx context.Context, key string) error + unlockFunc func(ctx context.Context, key string) error + storeFunc func(ctx context.Context, key string, value []byte) error + loadFunc func(ctx context.Context, key string) ([]byte, error) + deleteFunc func(ctx context.Context, key string) error + existsFunc func(ctx context.Context, key string) bool + listFunc func(ctx context.Context, path string, recursive bool) ([]string, error) + statFunc func(ctx context.Context, key string) (certmagic.KeyInfo, error) +} + +func (s *blockingStorage) Lock(ctx context.Context, key string) error { + if s.lockFunc != nil { + return s.lockFunc(ctx, key) + } + return nil +} + +func (s *blockingStorage) Unlock(ctx context.Context, key string) error { + if s.unlockFunc != nil { + return s.unlockFunc(ctx, key) + } + return nil +} + +func (s *blockingStorage) Store(ctx context.Context, key string, value []byte) error { + if s.storeFunc != nil { + return s.storeFunc(ctx, key, value) + } + return nil +} + +func (s *blockingStorage) Load(ctx context.Context, key string) ([]byte, error) { + if s.loadFunc != nil { + return s.loadFunc(ctx, key) + } + return nil, fs.ErrNotExist +} + +func (s *blockingStorage) Delete(ctx context.Context, key string) error { + if s.deleteFunc != nil { + return s.deleteFunc(ctx, key) + } + return nil +} + +func (s *blockingStorage) Exists(ctx context.Context, key string) bool { + if s.existsFunc != nil { + return s.existsFunc(ctx, key) + } + return false +} + +func (s *blockingStorage) List(ctx context.Context, path string, recursive bool) ([]string, error) { + if s.listFunc != nil { + return s.listFunc(ctx, path, recursive) + } + return nil, nil +} + +func (s *blockingStorage) Stat(ctx context.Context, key string) (certmagic.KeyInfo, error) { + if s.statFunc != nil { + return s.statFunc(ctx, key) + } + return certmagic.KeyInfo{}, fs.ErrNotExist +} + +type blockingPublisher struct { + publishFunc func(ctx context.Context, innerNames []string, echConfigList []byte) error +} + +func (p *blockingPublisher) PublisherKey() string { + return "blocking_test_publisher" +} + +func (p *blockingPublisher) PublishECHConfigList(ctx context.Context, innerNames []string, echConfigList []byte) error { + if p.publishFunc != nil { + return p.publishFunc(ctx, innerNames, echConfigList) + } + return nil +} + +func TestTLSStorageCleanStopSynchronization(t *testing.T) { + ctx, cancel := caddy.NewContext(caddy.Context{Context: context.Background()}) + defer cancel() + + tlsApp := &TLS{ + ctx: ctx, + logger: zap.NewNop(), + Automation: &AutomationConfig{ + StorageCleanInterval: caddy.Duration(1 * time.Hour), + }, + } + + // Start storage cleaner + tlsApp.keepStorageClean() + + // Stop must cancel cleaner context and wait for completion cleanly + err := tlsApp.Stop() + if err != nil { + t.Fatalf("TLS.Stop failed: %v", err) + } +} + +func TestTLSStorageCleanUnitsCanceledContext(t *testing.T) { + ctx, cancel := caddy.NewContext(caddy.Context{Context: context.Background()}) + cancel() // canceled immediately + + tlsApp := &TLS{ + ctx: ctx, + logger: zap.NewNop(), + } + + // cleanStorageUnits with canceled context should return immediately + tlsApp.cleanStorageUnits(ctx.Context) +} + +func TestTLSECHStopSynchronization(t *testing.T) { + ctx, cancel := caddy.NewContext(caddy.Context{Context: context.Background()}) + defer cancel() + + tlsApp := &TLS{ + ctx: ctx, + logger: zap.NewNop(), + Automation: &AutomationConfig{}, + EncryptedClientHello: &ECH{}, + DisableStorageClean: true, + } + + err := tlsApp.Start() + if err != nil { + t.Fatalf("TLS.Start failed: %v", err) + } + + // Stop must cancel ECH context and wait for background ECH worker to exit + err = tlsApp.Stop() + if err != nil { + t.Fatalf("TLS.Stop failed: %v", err) + } +} + +func TestTLSStorageCleanBlockingStorageStopSynchronization(t *testing.T) { + startedLock := make(chan struct{}) + storage := &blockingStorage{ + lockFunc: func(ctx context.Context, key string) error { + if key == "storage_clean" { + close(startedLock) + <-ctx.Done() + return ctx.Err() + } + return nil + }, + } + + ctx, cleanup := newTestContextWithStorage(t, storage) + defer cleanup() + + tlsApp := &TLS{ + ctx: ctx, + logger: zap.NewNop(), + Automation: &AutomationConfig{ + StorageCleanInterval: caddy.Duration(1 * time.Hour), + }, + } + + // Reset storageClean timestamp so cleaning runs + storageCleanMu.Lock() + storageClean = time.Time{} + storageCleanMu.Unlock() + + tlsApp.keepStorageClean() + + select { + case <-startedLock: + case <-time.After(5 * time.Second): + t.Fatal("timed out waiting for storage lock to be attempted") + } + + stopped := make(chan error, 1) + go func() { + stopped <- tlsApp.Stop() + }() + + select { + case err := <-stopped: + if err != nil { + t.Fatalf("TLS.Stop failed: %v", err) + } + case <-time.After(5 * time.Second): + t.Fatal("TLS.Stop hung while storage cleaner was blocked on storage lock") + } +} + +func TestTLSECHPublishBlockingStorageStopSynchronization(t *testing.T) { + startedLock := make(chan struct{}) + storage := &blockingStorage{ + lockFunc: func(ctx context.Context, key string) error { + if key == "ech_publish" { + close(startedLock) + <-ctx.Done() + return ctx.Err() + } + return nil + }, + } + + ctx, cleanup := newTestContextWithStorage(t, storage) + defer cleanup() + + ech := &ECH{ + configsMu: new(sync.RWMutex), + Configs: []ECHConfiguration{{PublicName: "example.com"}}, + } + tlsApp := &TLS{ + ctx: ctx, + logger: zap.NewNop(), + Automation: &AutomationConfig{}, + EncryptedClientHello: ech, + DisableStorageClean: true, + } + + err := tlsApp.Start() + if err != nil { + t.Fatalf("TLS.Start failed: %v", err) + } + + select { + case <-startedLock: + case <-time.After(5 * time.Second): + t.Fatal("timed out waiting for ech_publish storage lock to be attempted") + } + + stopped := make(chan error, 1) + go func() { + stopped <- tlsApp.Stop() + }() + + select { + case err := <-stopped: + if err != nil { + t.Fatalf("TLS.Stop failed: %v", err) + } + case <-time.After(5 * time.Second): + t.Fatal("TLS.Stop hung while ECH publisher was blocked on ech_publish storage lock") + } +} + +func TestTLSECHPublishBlockingPublisherStopSynchronization(t *testing.T) { + startedPublish := make(chan struct{}) + publisher := &blockingPublisher{ + publishFunc: func(ctx context.Context, innerNames []string, echConfigList []byte) error { + close(startedPublish) + <-ctx.Done() + return ctx.Err() + }, + } + + storage := &blockingStorage{} + ctx, cleanup := newTestContextWithStorage(t, storage) + defer cleanup() + + publicKey, _, err := hpke.KEM_X25519_HKDF_SHA256.Scheme().GenerateKeyPair() + if err != nil { + t.Fatalf("GenerateKeyPair failed: %v", err) + } + + ech := &ECH{ + configsMu: new(sync.RWMutex), + Configs: []ECHConfiguration{{PublicName: "example.com"}}, + Publication: []*ECHPublication{ + { + Domains: []string{"example.com"}, + publishers: []ECHPublisher{publisher}, + }, + }, + configs: map[string][]echConfig{ + "example.com": { + { + PublicKey: publicKey, + Version: draftTLSESNI25, + ConfigID: 1, + RawPublicName: "example.com", + KEMID: hpke.KEM_X25519_HKDF_SHA256, + CipherSuites: []hpkeSymmetricCipherSuite{ + { + KDFID: hpke.KDF_HKDF_SHA256, + AEADID: hpke.AEAD_AES128GCM, + }, + }, + meta: echConfigMeta{ + Publications: make(publicationHistory), + }, + }, + }, + }, + } + tlsApp := &TLS{ + ctx: ctx, + logger: zap.NewNop(), + Automation: &AutomationConfig{}, + EncryptedClientHello: ech, + DisableStorageClean: true, + serverNames: map[string]serverNameRegistration{ + "example.com": {}, + }, + } + + err = tlsApp.Start() + if err != nil { + t.Fatalf("TLS.Start failed: %v", err) + } + + select { + case <-startedPublish: + case <-time.After(5 * time.Second): + t.Fatal("timed out waiting for ECH publication to be attempted") + } + + stopped := make(chan error, 1) + go func() { + stopped <- tlsApp.Stop() + }() + + select { + case err := <-stopped: + if err != nil { + t.Fatalf("TLS.Stop failed: %v", err) + } + case <-time.After(5 * time.Second): + t.Fatal("TLS.Stop hung while ECH publisher was blocked inside PublishECHConfigList") + } +} + +func TestTLSECHRotateBlockingStorageStopSynchronization(t *testing.T) { + startedLock := make(chan struct{}) + storage := &blockingStorage{ + lockFunc: func(ctx context.Context, key string) error { + if key == echStorageLockName { + close(startedLock) + <-ctx.Done() + return ctx.Err() + } + return nil + }, + } + + ctx, cleanup := newTestContextWithStorage(t, storage) + defer cleanup() + + ech := &ECH{ + configsMu: new(sync.RWMutex), + Configs: []ECHConfiguration{{PublicName: "example.com"}}, + configs: map[string][]echConfig{ + "example.com": { + { + ConfigID: 1, + RawPublicName: "example.com", + meta: echConfigMeta{ + Created: time.Now().Add(-24 * time.Hour * 31), // > 30 days old so rotationNeeded returns true + Publications: make(publicationHistory), + }, + }, + }, + }, + } + tlsApp := &TLS{ + ctx: ctx, + logger: zap.NewNop(), + Automation: &AutomationConfig{}, + EncryptedClientHello: ech, + DisableStorageClean: true, + echRotateInterval: 5 * time.Millisecond, + } + + err := tlsApp.Start() + if err != nil { + t.Fatalf("TLS.Start failed: %v", err) + } + + select { + case <-startedLock: + case <-time.After(5 * time.Second): + t.Fatal("timed out waiting for echStorageLockName storage lock to be attempted during rotation") + } + + stopped := make(chan error, 1) + go func() { + stopped <- tlsApp.Stop() + }() + + select { + case err := <-stopped: + if err != nil { + t.Fatalf("TLS.Stop failed: %v", err) + } + case <-time.After(5 * time.Second): + t.Fatal("TLS.Stop hung while ECH key rotation was blocked on storage lock (context was likely ignored)") + } +} diff --git a/modules/caddytls/tls.go b/modules/caddytls/tls.go index b993cba6e..333fc146e 100644 --- a/modules/caddytls/tls.go +++ b/modules/caddytls/tls.go @@ -18,6 +18,7 @@ import ( "context" "crypto/tls" "encoding/json" + "errors" "fmt" "log" "net" @@ -136,8 +137,11 @@ type TLS struct { certificateLoaders []CertificateLoader automateNames map[string]struct{} ctx caddy.Context + bgCtx context.Context + bgCancel context.CancelFunc + bgWg *sync.WaitGroup storageCleanTicker *time.Ticker - storageCleanStop chan struct{} + echRotateInterval time.Duration logger *zap.Logger events *caddyevents.App @@ -411,6 +415,17 @@ func (t *TLS) Start() error { } } + if t.bgCtx == nil { + parentCtx := t.ctx.Context + if parentCtx == nil { + parentCtx = context.Background() + } + t.bgCtx, t.bgCancel = context.WithCancel(parentCtx) + } + if t.bgWg == nil { + t.bgWg = new(sync.WaitGroup) + } + // now that we are running, and all manual certificates have // been loaded, time to load the automated/managed certificates err := t.Manage(t.automateNames) @@ -423,34 +438,57 @@ func (t *TLS) Start() error { // publish ECH configs in the background; does not need to block // server startup, as it could take a while; then keep keys rotated - go func() { + t.bgWg.Add(1) + go func(ctx context.Context) { + defer t.bgWg.Done() + defer func() { + if err := recover(); err != nil { + log.Printf("[PANIC] tls ech publisher: %v\n%s", err, debug.Stack()) + } + }() + // publish immediately first - if err := t.publishECHConfigs(echLogger); err != nil { - echLogger.Error("publication(s) failed", zap.Error(err)) + if err := t.publishECHConfigs(ctx, echLogger); err != nil { + if !errors.Is(err, context.Canceled) && ctx.Err() == nil { + echLogger.Error("publication(s) failed", zap.Error(err)) + } } + ticker := time.NewTicker(t.echRotationInterval()) + defer ticker.Stop() + // then every so often, rotate and publish if needed // (both of these functions only do something if needed) for { select { - case <-time.After(1 * time.Hour): + case <-ticker.C: // ensure old keys are rotated out t.EncryptedClientHello.configsMu.Lock() - err = t.EncryptedClientHello.rotateECHKeys(t.ctx, echLogger, false) + err := t.EncryptedClientHello.rotateECHKeys(t.caddyContext(ctx), echLogger, false) t.EncryptedClientHello.configsMu.Unlock() if err != nil { - echLogger.Error("rotating ECH configs failed", zap.Error(err)) + if !errors.Is(err, context.Canceled) && ctx.Err() == nil { + echLogger.Error("rotating ECH configs failed", zap.Error(err)) + } + if ctx.Err() != nil { + return + } continue } - err := t.publishECHConfigs(echLogger) - if err != nil { - echLogger.Error("publication(s) failed", zap.Error(err)) + if ctx.Err() != nil { + return } - case <-t.ctx.Done(): + err = t.publishECHConfigs(ctx, echLogger) + if err != nil { + if !errors.Is(err, context.Canceled) && ctx.Err() == nil { + echLogger.Error("publication(s) failed", zap.Error(err)) + } + } + case <-ctx.Done(): return } } - }() + }(t.bgCtx) } if !t.DisableStorageClean { @@ -464,13 +502,18 @@ func (t *TLS) Start() error { // Stop stops the TLS module and cleans up any allocations. func (t *TLS) Stop() error { - // stop the storage cleaner goroutine and ticker - if t.storageCleanStop != nil { - close(t.storageCleanStop) + // cancel all background goroutines (storage cleaner, ECH rotation/publication, etc.) + if t.bgCancel != nil { + t.bgCancel() } if t.storageCleanTicker != nil { t.storageCleanTicker.Stop() } + // wait for all background goroutines to finish before returning, + // ensuring no background storage users are active when module Cleanup() runs + if t.bgWg != nil { + t.bgWg.Wait() + } return nil } @@ -907,30 +950,45 @@ func (t *TLS) HasCertificateForSubject(subject string) bool { // known storage units if it was not recently done, and then runs the // operation at every tick from t.storageCleanTicker. func (t *TLS) keepStorageClean() { + if t.bgCtx == nil { + parentCtx := t.ctx.Context + if parentCtx == nil { + parentCtx = context.Background() + } + t.bgCtx, t.bgCancel = context.WithCancel(parentCtx) + } + if t.bgWg == nil { + t.bgWg = new(sync.WaitGroup) + } t.storageCleanTicker = time.NewTicker(t.storageCleanInterval()) - t.storageCleanStop = make(chan struct{}) - go func() { + t.bgWg.Add(1) + go func(ctx context.Context) { + defer t.bgWg.Done() defer func() { if err := recover(); err != nil { log.Printf("[PANIC] storage cleaner: %v\n%s", err, debug.Stack()) } }() - t.cleanStorageUnits() + t.cleanStorageUnits(ctx) for { select { - case <-t.storageCleanStop: + case <-ctx.Done(): return case <-t.storageCleanTicker.C: - t.cleanStorageUnits() + t.cleanStorageUnits(ctx) } } - }() + }(t.bgCtx) } -func (t *TLS) cleanStorageUnits() { +func (t *TLS) cleanStorageUnits(ctx context.Context) { storageCleanMu.Lock() defer storageCleanMu.Unlock() + if err := ctx.Err(); err != nil { + return + } + // TODO: This check might not be needed anymore now that CertMagic syncs // and throttles storage cleaning globally across the cluster. // The original comment below might be outdated: @@ -963,12 +1021,17 @@ func (t *TLS) cleanStorageUnits() { } // start with the default/global storage - err = certmagic.CleanStorage(t.ctx, t.ctx.Storage(), options) - if err != nil { - // probably don't want to return early, since we should still - // see if any other storages can get cleaned up - if c := t.logger.Check(zapcore.ErrorLevel, "could not clean default/global storage"); c != nil { - c.Write(zap.Error(err)) + if storage := t.ctx.Storage(); storage != nil { + err = certmagic.CleanStorage(ctx, storage, options) + if err != nil { + if errors.Is(err, context.Canceled) || ctx.Err() != nil { + return + } + // probably don't want to return early, since we should still + // see if any other storages can get cleaned up + if c := t.logger.Check(zapcore.ErrorLevel, "could not clean default/global storage"); c != nil { + c.Write(zap.Error(err)) + } } } @@ -978,7 +1041,10 @@ func (t *TLS) cleanStorageUnits() { if ap.storage == nil { continue } - if err := certmagic.CleanStorage(t.ctx, ap.storage, options); err != nil { + if err := certmagic.CleanStorage(ctx, ap.storage, options); err != nil { + if errors.Is(err, context.Canceled) || ctx.Err() != nil { + return + } if c := t.logger.Check(zapcore.ErrorLevel, "could not clean storage configured in automation policy"); c != nil { c.Write(zap.Error(err)) } @@ -986,6 +1052,10 @@ func (t *TLS) cleanStorageUnits() { } } + if err := ctx.Err(); err != nil { + return + } + // remember last time storage was finished cleaning storageClean = time.Now() @@ -999,6 +1069,19 @@ func (t *TLS) storageCleanInterval() time.Duration { return defaultStorageCleanInterval } +func (t *TLS) echRotationInterval() time.Duration { + if t.echRotateInterval > 0 { + return t.echRotateInterval + } + return 1 * time.Hour +} + +func (t *TLS) caddyContext(ctx context.Context) caddy.Context { + caddyCtx := t.ctx + caddyCtx.Context = ctx + return caddyCtx +} + // onEvent translates CertMagic events into Caddy events then dispatches them. func (t *TLS) onEvent(ctx context.Context, eventName string, data map[string]any) error { evt := t.events.Emit(t.ctx, eventName, data)