caddytls: Reuse certificate cache through reloads (#5623)

* caddytls: Don't purge cert cache on config reload

* Update CertMagic

This actually avoids reloading managed certs from storage
when already in the cache, d'oh.

* Fix bug; re-implement HasCertificateForSubject

* Update go.mod: CertMagic tag
This commit is contained in:
Matt Holt 2023-07-11 13:10:58 -06:00 committed by GitHub
parent 7ceef91295
commit 0e2c7e1d35
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
10 changed files with 115 additions and 38 deletions

View file

@ -1018,7 +1018,7 @@ func handleConfigID(w http.ResponseWriter, r *http.Request) error {
// map the ID to the expanded path // map the ID to the expanded path
currentCtxMu.RLock() currentCtxMu.RLock()
expanded, ok := rawCfgIndex[id] expanded, ok := rawCfgIndex[id]
defer currentCtxMu.RUnlock() currentCtxMu.RUnlock()
if !ok { if !ok {
return APIError{ return APIError{
HTTPStatus: http.StatusNotFound, HTTPStatus: http.StatusNotFound,

View file

@ -959,8 +959,9 @@ func Version() (simple, full string) {
// This function is experimental and might be changed // This function is experimental and might be changed
// or removed in the future. // or removed in the future.
func ActiveContext() Context { func ActiveContext() Context {
currentCtxMu.RLock() // TODO: This locking might still be needed; more investigation is required (deadlock during Cleanup for the caddytls.TLS module).
defer currentCtxMu.RUnlock() // currentCtxMu.RLock()
// defer currentCtxMu.RUnlock()
return currentCtx return currentCtx
} }

View file

@ -410,6 +410,11 @@ func (ctx Context) loadModuleInline(moduleNameKey, moduleScope string, raw json.
// called during the Provision/Validate phase to reference a // called during the Provision/Validate phase to reference a
// module's own host app (since the parent app module is still // module's own host app (since the parent app module is still
// in the process of being provisioned, it is not yet ready). // in the process of being provisioned, it is not yet ready).
//
// We return any type instead of the App type because it is NOT
// intended for the caller of this method to be the one to start
// or stop App modules. The caller is expected to assert to the
// concrete type.
func (ctx Context) App(name string) (any, error) { func (ctx Context) App(name string) (any, error) {
if app, ok := ctx.cfg.apps[name]; ok { if app, ok := ctx.cfg.apps[name]; ok {
return app, nil return app, nil
@ -428,18 +433,15 @@ func (ctx Context) App(name string) (any, error) {
// AppIfConfigured returns an app by its name if it has been // AppIfConfigured returns an app by its name if it has been
// configured. Can be called instead of App() to avoid // configured. Can be called instead of App() to avoid
// instantiating an empty app when that's not desirable. // instantiating an empty app when that's not desirable. If
func (ctx Context) AppIfConfigured(name string) (any, error) { // the app has not been loaded, nil is returned.
app, ok := ctx.cfg.apps[name] //
if !ok || app == nil { // We return any type instead of the App type because it is not
return nil, nil // intended for the caller of this method to be the one to start
} // or stop App modules. The caller is expected to assert to the
// concrete type.
appModule, err := ctx.App(name) func (ctx Context) AppIfConfigured(name string) any {
if err != nil { return ctx.cfg.apps[name]
return nil, err
}
return appModule, nil
} }
// Storage returns the configured Caddy storage implementation. // Storage returns the configured Caddy storage implementation.

3
go.mod
View file

@ -7,7 +7,7 @@ require (
github.com/Masterminds/sprig/v3 v3.2.3 github.com/Masterminds/sprig/v3 v3.2.3
github.com/alecthomas/chroma/v2 v2.7.0 github.com/alecthomas/chroma/v2 v2.7.0
github.com/aryann/difflib v0.0.0-20210328193216-ff5ff6dc229b github.com/aryann/difflib v0.0.0-20210328193216-ff5ff6dc229b
github.com/caddyserver/certmagic v0.18.2 github.com/caddyserver/certmagic v0.19.0
github.com/dustin/go-humanize v1.0.1 github.com/dustin/go-humanize v1.0.1
github.com/go-chi/chi v4.1.2+incompatible github.com/go-chi/chi v4.1.2+incompatible
github.com/google/cel-go v0.15.1 github.com/google/cel-go v0.15.1
@ -61,6 +61,7 @@ require (
github.com/quic-go/qtls-go1-20 v0.2.2 // indirect github.com/quic-go/qtls-go1-20 v0.2.2 // indirect
github.com/smallstep/go-attestation v0.4.4-0.20230509120429-e17291421738 // indirect github.com/smallstep/go-attestation v0.4.4-0.20230509120429-e17291421738 // indirect
github.com/x448/float16 v0.8.4 // indirect github.com/x448/float16 v0.8.4 // indirect
github.com/zeebo/blake3 v0.2.3 // indirect
go.opentelemetry.io/contrib/propagators/aws v1.17.0 // indirect go.opentelemetry.io/contrib/propagators/aws v1.17.0 // indirect
go.opentelemetry.io/contrib/propagators/b3 v1.17.0 // indirect go.opentelemetry.io/contrib/propagators/b3 v1.17.0 // indirect
go.opentelemetry.io/contrib/propagators/jaeger v1.17.0 // indirect go.opentelemetry.io/contrib/propagators/jaeger v1.17.0 // indirect

11
go.sum
View file

@ -170,8 +170,8 @@ github.com/bketelsen/crypt v0.0.3-0.20200106085610-5cbc8cc4026c/go.mod h1:MKsuJm
github.com/blakesmith/ar v0.0.0-20190502131153-809d4375e1fb/go.mod h1:PkYb9DJNAwrSvRx5DYA+gUcOIgTGVMNkfSCbZM8cWpI= github.com/blakesmith/ar v0.0.0-20190502131153-809d4375e1fb/go.mod h1:PkYb9DJNAwrSvRx5DYA+gUcOIgTGVMNkfSCbZM8cWpI=
github.com/boltdb/bolt v1.3.1/go.mod h1:clJnj/oiGkjum5o1McbSZDSLxVThjynRyGBgiAx27Ps= github.com/boltdb/bolt v1.3.1/go.mod h1:clJnj/oiGkjum5o1McbSZDSLxVThjynRyGBgiAx27Ps=
github.com/caarlos0/ctrlc v1.0.0/go.mod h1:CdXpj4rmq0q/1Eb44M9zi2nKB0QraNKuRGYGrrHhcQw= github.com/caarlos0/ctrlc v1.0.0/go.mod h1:CdXpj4rmq0q/1Eb44M9zi2nKB0QraNKuRGYGrrHhcQw=
github.com/caddyserver/certmagic v0.18.2 h1:Nj2+M+A2Ho9IF6n1wUSbra4mX1X6ALzWpul9HooprHA= github.com/caddyserver/certmagic v0.19.0 h1:HuJ1Yf1H1jAfmBGrSSQN1XRkafnWcpDtyIiyMV6vmpM=
github.com/caddyserver/certmagic v0.18.2/go.mod h1:cLsgYXecH1iVUPjDXw15/1SKjZk/TK+aFfQk5FnugGQ= github.com/caddyserver/certmagic v0.19.0/go.mod h1:fsL01NomQ6N+kE2j37ZCnig2MFosG+MIO4ztnmG/zz8=
github.com/campoy/unique v0.0.0-20180121183637-88950e537e7e/go.mod h1:9IOqJGCPMSc6E5ydlp5NIonxObaeu/Iub/X03EKPVYo= github.com/campoy/unique v0.0.0-20180121183637-88950e537e7e/go.mod h1:9IOqJGCPMSc6E5ydlp5NIonxObaeu/Iub/X03EKPVYo=
github.com/casbin/casbin/v2 v2.1.2/go.mod h1:YcPU1XXisHhLzuxH9coDNf2FbKpjGlbCg3n9yuLkIJQ= github.com/casbin/casbin/v2 v2.1.2/go.mod h1:YcPU1XXisHhLzuxH9coDNf2FbKpjGlbCg3n9yuLkIJQ=
github.com/cavaliercoder/go-cpio v0.0.0-20180626203310-925f9528c45e/go.mod h1:oDpT4efm8tSYHXV5tHSdRvBet/b/QzxZ+XyyPehvm3A= github.com/cavaliercoder/go-cpio v0.0.0-20180626203310-925f9528c45e/go.mod h1:oDpT4efm8tSYHXV5tHSdRvBet/b/QzxZ+XyyPehvm3A=
@ -650,6 +650,7 @@ github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+o
github.com/klauspost/compress v1.12.3/go.mod h1:8dP1Hq4DHOhN9w426knH3Rhby4rFm6D8eO+e+Dq5Gzg= github.com/klauspost/compress v1.12.3/go.mod h1:8dP1Hq4DHOhN9w426knH3Rhby4rFm6D8eO+e+Dq5Gzg=
github.com/klauspost/compress v1.16.6 h1:91SKEy4K37vkp255cJ8QesJhjyRO0hn9i9G0GoUwLsk= github.com/klauspost/compress v1.16.6 h1:91SKEy4K37vkp255cJ8QesJhjyRO0hn9i9G0GoUwLsk=
github.com/klauspost/compress v1.16.6/go.mod h1:ntbaceVETuRiXiv4DpjP66DpAtAGkEQskQzEyD//IeE= github.com/klauspost/compress v1.16.6/go.mod h1:ntbaceVETuRiXiv4DpjP66DpAtAGkEQskQzEyD//IeE=
github.com/klauspost/cpuid/v2 v2.0.12/go.mod h1:g2LTdtYhdyuGPqyWyv7qRAmj1WBqxuObKfj5c0PQa7c=
github.com/klauspost/cpuid/v2 v2.2.5 h1:0E5MSMDEoAulmXNFquVs//DdoomxaoTY1kUhbc/qbZg= github.com/klauspost/cpuid/v2 v2.2.5 h1:0E5MSMDEoAulmXNFquVs//DdoomxaoTY1kUhbc/qbZg=
github.com/klauspost/cpuid/v2 v2.2.5/go.mod h1:Lcz8mBdAVJIBVzewtcLocK12l3Y+JytZYpaMropDUws= github.com/klauspost/cpuid/v2 v2.2.5/go.mod h1:Lcz8mBdAVJIBVzewtcLocK12l3Y+JytZYpaMropDUws=
github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
@ -1012,6 +1013,12 @@ github.com/yuin/goldmark v1.5.4 h1:2uY/xC0roWy8IBEGLgB1ywIoEJFGmRrX21YQcvGZzjU=
github.com/yuin/goldmark v1.5.4/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= github.com/yuin/goldmark v1.5.4/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
github.com/yuin/goldmark-highlighting/v2 v2.0.0-20220924101305-151362477c87 h1:Py16JEzkSdKAtEFJjiaYLYBOWGXc1r/xHj/Q/5lA37k= github.com/yuin/goldmark-highlighting/v2 v2.0.0-20220924101305-151362477c87 h1:Py16JEzkSdKAtEFJjiaYLYBOWGXc1r/xHj/Q/5lA37k=
github.com/yuin/goldmark-highlighting/v2 v2.0.0-20220924101305-151362477c87/go.mod h1:ovIvrum6DQJA4QsJSovrkC4saKHQVs7TvcaeO8AIl5I= github.com/yuin/goldmark-highlighting/v2 v2.0.0-20220924101305-151362477c87/go.mod h1:ovIvrum6DQJA4QsJSovrkC4saKHQVs7TvcaeO8AIl5I=
github.com/zeebo/assert v1.1.0 h1:hU1L1vLTHsnO8x8c9KAR5GmM5QscxHg5RNU5z5qbUWY=
github.com/zeebo/assert v1.1.0/go.mod h1:Pq9JiuJQpG8JLJdtkwrJESF0Foym2/D9XMU5ciN/wJ0=
github.com/zeebo/blake3 v0.2.3 h1:TFoLXsjeXqRNFxSbk35Dk4YtszE/MQQGK10BH4ptoTg=
github.com/zeebo/blake3 v0.2.3/go.mod h1:mjJjZpnsyIVtVgTOSpJ9vmRE4wgDeyt2HU3qXvvKCaQ=
github.com/zeebo/pcg v1.0.1 h1:lyqfGeWiv4ahac6ttHs+I5hwtH/+1mrhlCtVNQM2kHo=
github.com/zeebo/pcg v1.0.1/go.mod h1:09F0S9iiKrwn9rlI5yjLkmrug154/YRW6KnnXVDM/l4=
github.com/zenazn/goji v0.9.0/go.mod h1:7S9M489iMyHBNxwZnk9/EHS098H4/F6TATF2mIxtB1Q= github.com/zenazn/goji v0.9.0/go.mod h1:7S9M489iMyHBNxwZnk9/EHS098H4/F6TATF2mIxtB1Q=
go.etcd.io/bbolt v1.3.2/go.mod h1:IbVyRI1SCnLcuJnV2u8VeU0CEYM7e686BmAb1XKL+uU= go.etcd.io/bbolt v1.3.2/go.mod h1:IbVyRI1SCnLcuJnV2u8VeU0CEYM7e686BmAb1XKL+uU=
go.etcd.io/bbolt v1.3.3/go.mod h1:IbVyRI1SCnLcuJnV2u8VeU0CEYM7e686BmAb1XKL+uU= go.etcd.io/bbolt v1.3.3/go.mod h1:IbVyRI1SCnLcuJnV2u8VeU0CEYM7e686BmAb1XKL+uU=

View file

@ -196,8 +196,7 @@ func (app *App) automaticHTTPSPhase1(ctx caddy.Context, repl *caddy.Replacer) er
// if a certificate for this name is already loaded, // if a certificate for this name is already loaded,
// don't obtain another one for it, unless we are // don't obtain another one for it, unless we are
// supposed to ignore loaded certificates // supposed to ignore loaded certificates
if !srv.AutoHTTPS.IgnoreLoadedCerts && if !srv.AutoHTTPS.IgnoreLoadedCerts && app.tlsApp.HasCertificateForSubject(d) {
len(app.tlsApp.AllMatchingCertificates(d)) > 0 {
logger.Info("skipping automatic certificate management because one or more matching certificates are already loaded", logger.Info("skipping automatic certificate management because one or more matching certificates are already loaded",
zap.String("domain", d), zap.String("domain", d),
zap.String("server_name", srvName), zap.String("server_name", srvName),

View file

@ -525,7 +525,7 @@ func (t TLSConfig) MakeTLSClientConfig(ctx caddy.Context) (*tls.Config, error) {
return nil, fmt.Errorf("managing client certificate: %v", err) return nil, fmt.Errorf("managing client certificate: %v", err)
} }
cfg.GetClientCertificate = func(cri *tls.CertificateRequestInfo) (*tls.Certificate, error) { cfg.GetClientCertificate = func(cri *tls.CertificateRequestInfo) (*tls.Certificate, error) {
certs := tlsApp.AllMatchingCertificates(t.ClientCertificateAutomate) certs := caddytls.AllMatchingCertificates(t.ClientCertificateAutomate)
var err error var err error
for _, cert := range certs { for _, cert := range certs {
err = cri.SupportsCertificate(&cert.Certificate) err = cri.SupportsCertificate(&cert.Certificate)

View file

@ -50,11 +50,7 @@ func (a *adminAPI) Provision(ctx caddy.Context) error {
a.log = ctx.Logger(a) // TODO: passing in 'a' is a hack until the admin API is officially extensible (see #5032) a.log = ctx.Logger(a) // TODO: passing in 'a' is a hack until the admin API is officially extensible (see #5032)
// Avoid initializing PKI if it wasn't configured // Avoid initializing PKI if it wasn't configured
pkiApp, err := a.ctx.AppIfConfigured("pki") if pkiApp := a.ctx.AppIfConfigured("pki"); pkiApp != nil {
if err != nil {
return err
}
if pkiApp != nil {
a.pkiApp = pkiApp.(*PKI) a.pkiApp = pkiApp.(*PKI)
} }

View file

@ -294,7 +294,9 @@ func (ap *AutomationPolicy) Provision(tlsApp *TLS) error {
Issuers: issuers, Issuers: issuers,
Logger: tlsApp.logger, Logger: tlsApp.logger,
} }
ap.magic = certmagic.New(tlsApp.certCache, template) certCacheMu.RLock()
ap.magic = certmagic.New(certCache, template)
certCacheMu.RUnlock()
// sometimes issuers may need the parent certmagic.Config in // sometimes issuers may need the parent certmagic.Config in
// order to function properly (for example, ACMEIssuer needs // order to function properly (for example, ACMEIssuer needs

View file

@ -36,6 +36,11 @@ func init() {
caddy.RegisterModule(AutomateLoader{}) caddy.RegisterModule(AutomateLoader{})
} }
var (
certCache *certmagic.Cache
certCacheMu sync.RWMutex
)
// TLS provides TLS facilities including certificate // TLS provides TLS facilities including certificate
// loading and management, client auth, and more. // loading and management, client auth, and more.
type TLS struct { type TLS struct {
@ -77,12 +82,15 @@ type TLS struct {
certificateLoaders []CertificateLoader certificateLoaders []CertificateLoader
automateNames []string automateNames []string
certCache *certmagic.Cache
ctx caddy.Context ctx caddy.Context
storageCleanTicker *time.Ticker storageCleanTicker *time.Ticker
storageCleanStop chan struct{} storageCleanStop chan struct{}
logger *zap.Logger logger *zap.Logger
events *caddyevents.App events *caddyevents.App
// set of subjects with managed certificates,
// and hashes of manually-loaded certificates
managing, loaded map[string]struct{}
} }
// CaddyModule returns the Caddy module information. // CaddyModule returns the Caddy module information.
@ -103,6 +111,7 @@ func (t *TLS) Provision(ctx caddy.Context) error {
t.ctx = ctx t.ctx = ctx
t.logger = ctx.Logger() t.logger = ctx.Logger()
repl := caddy.NewReplacer() repl := caddy.NewReplacer()
t.managing, t.loaded = make(map[string]struct{}), make(map[string]struct{})
// set up a new certificate cache; this (re)loads all certificates // set up a new certificate cache; this (re)loads all certificates
cacheOpts := certmagic.CacheOptions{ cacheOpts := certmagic.CacheOptions{
@ -121,7 +130,14 @@ func (t *TLS) Provision(ctx caddy.Context) error {
if cacheOpts.Capacity <= 0 { if cacheOpts.Capacity <= 0 {
cacheOpts.Capacity = 10000 cacheOpts.Capacity = 10000
} }
t.certCache = certmagic.NewCache(cacheOpts)
certCacheMu.Lock()
if certCache == nil {
certCache = certmagic.NewCache(cacheOpts)
} else {
certCache.SetOptions(cacheOpts)
}
certCacheMu.Unlock()
// certificate loaders // certificate loaders
val, err := ctx.LoadModule(t, "CertificatesRaw") val, err := ctx.LoadModule(t, "CertificatesRaw")
@ -209,7 +225,8 @@ func (t *TLS) Provision(ctx caddy.Context) error {
// provision so that other apps (such as http) can know which // provision so that other apps (such as http) can know which
// certificates have been manually loaded, and also so that // certificates have been manually loaded, and also so that
// commands like validate can be a better test // commands like validate can be a better test
magic := certmagic.New(t.certCache, certmagic.Config{ certCacheMu.RLock()
magic := certmagic.New(certCache, certmagic.Config{
Storage: ctx.Storage(), Storage: ctx.Storage(),
Logger: t.logger, Logger: t.logger,
OnEvent: t.onEvent, OnEvent: t.onEvent,
@ -217,16 +234,18 @@ func (t *TLS) Provision(ctx caddy.Context) error {
DisableStapling: t.DisableOCSPStapling, DisableStapling: t.DisableOCSPStapling,
}, },
}) })
certCacheMu.RUnlock()
for _, loader := range t.certificateLoaders { for _, loader := range t.certificateLoaders {
certs, err := loader.LoadCertificates() certs, err := loader.LoadCertificates()
if err != nil { if err != nil {
return fmt.Errorf("loading certificates: %v", err) return fmt.Errorf("loading certificates: %v", err)
} }
for _, cert := range certs { for _, cert := range certs {
err := magic.CacheUnmanagedTLSCertificate(ctx, cert.Certificate, cert.Tags) hash, err := magic.CacheUnmanagedTLSCertificate(ctx, cert.Certificate, cert.Tags)
if err != nil { if err != nil {
return fmt.Errorf("caching unmanaged certificate: %v", err) return fmt.Errorf("caching unmanaged certificate: %v", err)
} }
t.loaded[hash] = struct{}{}
} }
} }
@ -305,16 +324,44 @@ func (t *TLS) Stop() error {
// Cleanup frees up resources allocated during Provision. // Cleanup frees up resources allocated during Provision.
func (t *TLS) Cleanup() error { func (t *TLS) Cleanup() error {
// stop the certificate cache
if t.certCache != nil {
t.certCache.Stop()
}
// stop the session ticket rotation goroutine // stop the session ticket rotation goroutine
if t.SessionTickets != nil { if t.SessionTickets != nil {
t.SessionTickets.stop() t.SessionTickets.stop()
} }
// if a new TLS app was loaded, remove certificates from the cache that are no longer
// being managed or loaded by the new config; if there is no more TLS app running,
// then stop cert maintenance and let the cert cache be GC'ed
if nextTLS := caddy.ActiveContext().AppIfConfigured("tls"); nextTLS != nil {
nextTLSApp := nextTLS.(*TLS)
// compute which certificates were managed or loaded into the cert cache by this
// app instance (which is being stopped) that are not managed or loaded by the
// new app instance (which just started), and remove them from the cache
var noLongerManaged, noLongerLoaded []string
for subj := range t.managing {
if _, ok := nextTLSApp.managing[subj]; !ok {
noLongerManaged = append(noLongerManaged, subj)
}
}
for hash := range t.loaded {
if _, ok := nextTLSApp.loaded[hash]; !ok {
noLongerLoaded = append(noLongerLoaded, hash)
}
}
certCacheMu.RLock()
certCache.RemoveManaged(noLongerManaged)
certCache.Remove(noLongerLoaded)
certCacheMu.RUnlock()
} else {
// no more TLS app running, so delete in-memory cert cache
certCache.Stop()
certCacheMu.Lock()
certCache = nil
certCacheMu.Unlock()
}
return nil return nil
} }
@ -339,6 +386,9 @@ func (t *TLS) Manage(names []string) error {
if err != nil { if err != nil {
return fmt.Errorf("automate: manage %v: %v", names, err) return fmt.Errorf("automate: manage %v: %v", names, err)
} }
for _, name := range names {
t.managing[name] = struct{}{}
}
} }
return nil return nil
@ -449,8 +499,27 @@ func (t *TLS) getAutomationPolicyForName(name string) *AutomationPolicy {
// AllMatchingCertificates returns the list of all certificates in // AllMatchingCertificates returns the list of all certificates in
// the cache which could be used to satisfy the given SAN. // the cache which could be used to satisfy the given SAN.
func (t *TLS) AllMatchingCertificates(san string) []certmagic.Certificate { func AllMatchingCertificates(san string) []certmagic.Certificate {
return t.certCache.AllMatchingCertificates(san) return certCache.AllMatchingCertificates(san)
}
func (t *TLS) HasCertificateForSubject(subject string) bool {
certCacheMu.RLock()
allMatchingCerts := certCache.AllMatchingCertificates(subject)
certCacheMu.RUnlock()
for _, cert := range allMatchingCerts {
// check if the cert is manually loaded by this config
if _, ok := t.loaded[cert.Hash()]; ok {
return true
}
// check if the cert is automatically managed by this config
for _, name := range cert.Names {
if _, ok := t.managing[name]; ok {
return true
}
}
}
return false
} }
// keepStorageClean starts a goroutine that immediately cleans up all // keepStorageClean starts a goroutine that immediately cleans up all