@marcel91200 / caddy Mirror · read-only / commits / 0e2c7e1d
Fork 0

Fast and extensible multi-platform HTTP/1-2-3 web server with automatic HTTPS

Mirrored from https://github.com/Slazee/caddy.git

Synced Nov 13, 00:30 UTC

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

Matt Holt committed Jul 11, 2023 at 13:10 UTC 0e2c7e1d35b287fc0e56d6db2951f791e09b5a37
10 files changed +115 -38
admin.go
+1 -1
@@ -1018,7 +1018,7 @@ func handleConfigID(w http.ResponseWriter, r *http.Request) error {
1018 // map the ID to the expanded path
1019 currentCtxMu.RLock()
1020 expanded, ok := rawCfgIndex[id]
1021 - defer currentCtxMu.RUnlock()
1021 + currentCtxMu.RUnlock()
1022 if !ok {
1023 return APIError{
1024 HTTPStatus: http.StatusNotFound,
caddy.go
+3 -2
@@ -959,8 +959,9 @@ func Version() (simple, full string) {
959 // This function is experimental and might be changed
960 // or removed in the future.
961 func ActiveContext() Context {
962 - currentCtxMu.RLock()
963 - defer currentCtxMu.RUnlock()
962 + // TODO: This locking might still be needed; more investigation is required (deadlock during Cleanup for the caddytls.TLS module).
963 + // currentCtxMu.RLock()
964 + // defer currentCtxMu.RUnlock()
965 return currentCtx
966 }
967
context.go
+14 -12
@@ -410,6 +410,11 @@ func (ctx Context) loadModuleInline(moduleNameKey, moduleScope string, raw json.
410 // called during the Provision/Validate phase to reference a
411 // module's own host app (since the parent app module is still
412 // in the process of being provisioned, it is not yet ready).
413 +//
414 +// We return any type instead of the App type because it is NOT
415 +// intended for the caller of this method to be the one to start
416 +// or stop App modules. The caller is expected to assert to the
417 +// concrete type.
418 func (ctx Context) App(name string) (any, error) {
419 if app, ok := ctx.cfg.apps[name]; ok {
420 return app, nil
@@ -428,18 +433,15 @@ func (ctx Context) App(name string) (any, error) {
433
434 // AppIfConfigured returns an app by its name if it has been
435 // configured. Can be called instead of App() to avoid
431 -// instantiating an empty app when that's not desirable.
432 -func (ctx Context) AppIfConfigured(name string) (any, error) {
433 - app, ok := ctx.cfg.apps[name]
434 - if !ok || app == nil {
435 - return nil, nil
436 - }
437 -
438 - appModule, err := ctx.App(name)
439 - if err != nil {
440 - return nil, err
441 - }
442 - return appModule, nil
436 +// instantiating an empty app when that's not desirable. If
437 +// the app has not been loaded, nil is returned.
438 +//
439 +// We return any type instead of the App type because it is not
440 +// intended for the caller of this method to be the one to start
441 +// or stop App modules. The caller is expected to assert to the
442 +// concrete type.
443 +func (ctx Context) AppIfConfigured(name string) any {
444 + return ctx.cfg.apps[name]
445 }
446
447 // Storage returns the configured Caddy storage implementation.
go.mod
+2 -1
@@ -7,7 +7,7 @@ require (
7 github.com/Masterminds/sprig/v3 v3.2.3
8 github.com/alecthomas/chroma/v2 v2.7.0
9 github.com/aryann/difflib v0.0.0-20210328193216-ff5ff6dc229b
10 - github.com/caddyserver/certmagic v0.18.2
10 + github.com/caddyserver/certmagic v0.19.0
11 github.com/dustin/go-humanize v1.0.1
12 github.com/go-chi/chi v4.1.2+incompatible
13 github.com/google/cel-go v0.15.1
@@ -61,6 +61,7 @@ require (
61 github.com/quic-go/qtls-go1-20 v0.2.2 // indirect
62 github.com/smallstep/go-attestation v0.4.4-0.20230509120429-e17291421738 // indirect
63 github.com/x448/float16 v0.8.4 // indirect
64 + github.com/zeebo/blake3 v0.2.3 // indirect
65 go.opentelemetry.io/contrib/propagators/aws v1.17.0 // indirect
66 go.opentelemetry.io/contrib/propagators/b3 v1.17.0 // indirect
67 go.opentelemetry.io/contrib/propagators/jaeger v1.17.0 // indirect
go.sum
+9 -2
@@ -170,8 +170,8 @@ github.com/bketelsen/crypt v0.0.3-0.20200106085610-5cbc8cc4026c/go.mod h1:MKsuJm
170 github.com/blakesmith/ar v0.0.0-20190502131153-809d4375e1fb/go.mod h1:PkYb9DJNAwrSvRx5DYA+gUcOIgTGVMNkfSCbZM8cWpI=
171 github.com/boltdb/bolt v1.3.1/go.mod h1:clJnj/oiGkjum5o1McbSZDSLxVThjynRyGBgiAx27Ps=
172 github.com/caarlos0/ctrlc v1.0.0/go.mod h1:CdXpj4rmq0q/1Eb44M9zi2nKB0QraNKuRGYGrrHhcQw=
173 -github.com/caddyserver/certmagic v0.18.2 h1:Nj2+M+A2Ho9IF6n1wUSbra4mX1X6ALzWpul9HooprHA=
174 -github.com/caddyserver/certmagic v0.18.2/go.mod h1:cLsgYXecH1iVUPjDXw15/1SKjZk/TK+aFfQk5FnugGQ=
173 +github.com/caddyserver/certmagic v0.19.0 h1:HuJ1Yf1H1jAfmBGrSSQN1XRkafnWcpDtyIiyMV6vmpM=
174 +github.com/caddyserver/certmagic v0.19.0/go.mod h1:fsL01NomQ6N+kE2j37ZCnig2MFosG+MIO4ztnmG/zz8=
175 github.com/campoy/unique v0.0.0-20180121183637-88950e537e7e/go.mod h1:9IOqJGCPMSc6E5ydlp5NIonxObaeu/Iub/X03EKPVYo=
176 github.com/casbin/casbin/v2 v2.1.2/go.mod h1:YcPU1XXisHhLzuxH9coDNf2FbKpjGlbCg3n9yuLkIJQ=
177 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
650 github.com/klauspost/compress v1.12.3/go.mod h1:8dP1Hq4DHOhN9w426knH3Rhby4rFm6D8eO+e+Dq5Gzg=
651 github.com/klauspost/compress v1.16.6 h1:91SKEy4K37vkp255cJ8QesJhjyRO0hn9i9G0GoUwLsk=
652 github.com/klauspost/compress v1.16.6/go.mod h1:ntbaceVETuRiXiv4DpjP66DpAtAGkEQskQzEyD//IeE=
653 +github.com/klauspost/cpuid/v2 v2.0.12/go.mod h1:g2LTdtYhdyuGPqyWyv7qRAmj1WBqxuObKfj5c0PQa7c=
654 github.com/klauspost/cpuid/v2 v2.2.5 h1:0E5MSMDEoAulmXNFquVs//DdoomxaoTY1kUhbc/qbZg=
655 github.com/klauspost/cpuid/v2 v2.2.5/go.mod h1:Lcz8mBdAVJIBVzewtcLocK12l3Y+JytZYpaMropDUws=
656 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=
1013 github.com/yuin/goldmark v1.5.4/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
1014 github.com/yuin/goldmark-highlighting/v2 v2.0.0-20220924101305-151362477c87 h1:Py16JEzkSdKAtEFJjiaYLYBOWGXc1r/xHj/Q/5lA37k=
1015 github.com/yuin/goldmark-highlighting/v2 v2.0.0-20220924101305-151362477c87/go.mod h1:ovIvrum6DQJA4QsJSovrkC4saKHQVs7TvcaeO8AIl5I=
1016 +github.com/zeebo/assert v1.1.0 h1:hU1L1vLTHsnO8x8c9KAR5GmM5QscxHg5RNU5z5qbUWY=
1017 +github.com/zeebo/assert v1.1.0/go.mod h1:Pq9JiuJQpG8JLJdtkwrJESF0Foym2/D9XMU5ciN/wJ0=
1018 +github.com/zeebo/blake3 v0.2.3 h1:TFoLXsjeXqRNFxSbk35Dk4YtszE/MQQGK10BH4ptoTg=
1019 +github.com/zeebo/blake3 v0.2.3/go.mod h1:mjJjZpnsyIVtVgTOSpJ9vmRE4wgDeyt2HU3qXvvKCaQ=
1020 +github.com/zeebo/pcg v1.0.1 h1:lyqfGeWiv4ahac6ttHs+I5hwtH/+1mrhlCtVNQM2kHo=
1021 +github.com/zeebo/pcg v1.0.1/go.mod h1:09F0S9iiKrwn9rlI5yjLkmrug154/YRW6KnnXVDM/l4=
1022 github.com/zenazn/goji v0.9.0/go.mod h1:7S9M489iMyHBNxwZnk9/EHS098H4/F6TATF2mIxtB1Q=
1023 go.etcd.io/bbolt v1.3.2/go.mod h1:IbVyRI1SCnLcuJnV2u8VeU0CEYM7e686BmAb1XKL+uU=
1024 go.etcd.io/bbolt v1.3.3/go.mod h1:IbVyRI1SCnLcuJnV2u8VeU0CEYM7e686BmAb1XKL+uU=
modules/caddyhttp/autohttps.go
+1 -2
@@ -196,8 +196,7 @@ func (app *App) automaticHTTPSPhase1(ctx caddy.Context, repl *caddy.Replacer) er
196 // if a certificate for this name is already loaded,
197 // don't obtain another one for it, unless we are
198 // supposed to ignore loaded certificates
199 - if !srv.AutoHTTPS.IgnoreLoadedCerts &&
200 - len(app.tlsApp.AllMatchingCertificates(d)) > 0 {
199 + if !srv.AutoHTTPS.IgnoreLoadedCerts && app.tlsApp.HasCertificateForSubject(d) {
200 logger.Info("skipping automatic certificate management because one or more matching certificates are already loaded",
201 zap.String("domain", d),
202 zap.String("server_name", srvName),
modules/caddyhttp/reverseproxy/httptransport.go
+1 -1
@@ -525,7 +525,7 @@ func (t TLSConfig) MakeTLSClientConfig(ctx caddy.Context) (*tls.Config, error) {
525 return nil, fmt.Errorf("managing client certificate: %v", err)
526 }
527 cfg.GetClientCertificate = func(cri *tls.CertificateRequestInfo) (*tls.Certificate, error) {
528 - certs := tlsApp.AllMatchingCertificates(t.ClientCertificateAutomate)
528 + certs := caddytls.AllMatchingCertificates(t.ClientCertificateAutomate)
529 var err error
530 for _, cert := range certs {
531 err = cri.SupportsCertificate(&cert.Certificate)
modules/caddypki/adminapi.go
+1 -5
@@ -50,11 +50,7 @@ func (a *adminAPI) Provision(ctx caddy.Context) error {
50 a.log = ctx.Logger(a) // TODO: passing in 'a' is a hack until the admin API is officially extensible (see #5032)
51
52 // Avoid initializing PKI if it wasn't configured
53 - pkiApp, err := a.ctx.AppIfConfigured("pki")
54 - if err != nil {
55 - return err
56 - }
57 - if pkiApp != nil {
53 + if pkiApp := a.ctx.AppIfConfigured("pki"); pkiApp != nil {
54 a.pkiApp = pkiApp.(*PKI)
55 }
56
modules/caddytls/automation.go
+3 -1
@@ -294,7 +294,9 @@ func (ap *AutomationPolicy) Provision(tlsApp *TLS) error {
294 Issuers: issuers,
295 Logger: tlsApp.logger,
296 }
297 - ap.magic = certmagic.New(tlsApp.certCache, template)
297 + certCacheMu.RLock()
298 + ap.magic = certmagic.New(certCache, template)
299 + certCacheMu.RUnlock()
300
301 // sometimes issuers may need the parent certmagic.Config in
302 // order to function properly (for example, ACMEIssuer needs
modules/caddytls/tls.go
+80 -11
@@ -36,6 +36,11 @@ func init() {
36 caddy.RegisterModule(AutomateLoader{})
37 }
38
39 +var (
40 + certCache *certmagic.Cache
41 + certCacheMu sync.RWMutex
42 +)
43 +
44 // TLS provides TLS facilities including certificate
45 // loading and management, client auth, and more.
46 type TLS struct {
@@ -77,12 +82,15 @@ type TLS struct {
82
83 certificateLoaders []CertificateLoader
84 automateNames []string
80 - certCache *certmagic.Cache
85 ctx caddy.Context
86 storageCleanTicker *time.Ticker
87 storageCleanStop chan struct{}
88 logger *zap.Logger
89 events *caddyevents.App
90 +
91 + // set of subjects with managed certificates,
92 + // and hashes of manually-loaded certificates
93 + managing, loaded map[string]struct{}
94 }
95
96 // CaddyModule returns the Caddy module information.
@@ -103,6 +111,7 @@ func (t *TLS) Provision(ctx caddy.Context) error {
111 t.ctx = ctx
112 t.logger = ctx.Logger()
113 repl := caddy.NewReplacer()
114 + t.managing, t.loaded = make(map[string]struct{}), make(map[string]struct{})
115
116 // set up a new certificate cache; this (re)loads all certificates
117 cacheOpts := certmagic.CacheOptions{
@@ -121,7 +130,14 @@ func (t *TLS) Provision(ctx caddy.Context) error {
130 if cacheOpts.Capacity <= 0 {
131 cacheOpts.Capacity = 10000
132 }
124 - t.certCache = certmagic.NewCache(cacheOpts)
133 +
134 + certCacheMu.Lock()
135 + if certCache == nil {
136 + certCache = certmagic.NewCache(cacheOpts)
137 + } else {
138 + certCache.SetOptions(cacheOpts)
139 + }
140 + certCacheMu.Unlock()
141
142 // certificate loaders
143 val, err := ctx.LoadModule(t, "CertificatesRaw")
@@ -209,7 +225,8 @@ func (t *TLS) Provision(ctx caddy.Context) error {
225 // provision so that other apps (such as http) can know which
226 // certificates have been manually loaded, and also so that
227 // commands like validate can be a better test
212 - magic := certmagic.New(t.certCache, certmagic.Config{
228 + certCacheMu.RLock()
229 + magic := certmagic.New(certCache, certmagic.Config{
230 Storage: ctx.Storage(),
231 Logger: t.logger,
232 OnEvent: t.onEvent,
@@ -217,16 +234,18 @@ func (t *TLS) Provision(ctx caddy.Context) error {
234 DisableStapling: t.DisableOCSPStapling,
235 },
236 })
237 + certCacheMu.RUnlock()
238 for _, loader := range t.certificateLoaders {
239 certs, err := loader.LoadCertificates()
240 if err != nil {
241 return fmt.Errorf("loading certificates: %v", err)
242 }
243 for _, cert := range certs {
226 - err := magic.CacheUnmanagedTLSCertificate(ctx, cert.Certificate, cert.Tags)
244 + hash, err := magic.CacheUnmanagedTLSCertificate(ctx, cert.Certificate, cert.Tags)
245 if err != nil {
246 return fmt.Errorf("caching unmanaged certificate: %v", err)
247 }
248 + t.loaded[hash] = struct{}{}
249 }
250 }
251
@@ -305,16 +324,44 @@ func (t *TLS) Stop() error {
324
325 // Cleanup frees up resources allocated during Provision.
326 func (t *TLS) Cleanup() error {
308 - // stop the certificate cache
309 - if t.certCache != nil {
310 - t.certCache.Stop()
311 - }
312 -
327 // stop the session ticket rotation goroutine
328 if t.SessionTickets != nil {
329 t.SessionTickets.stop()
330 }
331
332 + // if a new TLS app was loaded, remove certificates from the cache that are no longer
333 + // being managed or loaded by the new config; if there is no more TLS app running,
334 + // then stop cert maintenance and let the cert cache be GC'ed
335 + if nextTLS := caddy.ActiveContext().AppIfConfigured("tls"); nextTLS != nil {
336 + nextTLSApp := nextTLS.(*TLS)
337 +
338 + // compute which certificates were managed or loaded into the cert cache by this
339 + // app instance (which is being stopped) that are not managed or loaded by the
340 + // new app instance (which just started), and remove them from the cache
341 + var noLongerManaged, noLongerLoaded []string
342 + for subj := range t.managing {
343 + if _, ok := nextTLSApp.managing[subj]; !ok {
344 + noLongerManaged = append(noLongerManaged, subj)
345 + }
346 + }
347 + for hash := range t.loaded {
348 + if _, ok := nextTLSApp.loaded[hash]; !ok {
349 + noLongerLoaded = append(noLongerLoaded, hash)
350 + }
351 + }
352 +
353 + certCacheMu.RLock()
354 + certCache.RemoveManaged(noLongerManaged)
355 + certCache.Remove(noLongerLoaded)
356 + certCacheMu.RUnlock()
357 + } else {
358 + // no more TLS app running, so delete in-memory cert cache
359 + certCache.Stop()
360 + certCacheMu.Lock()
361 + certCache = nil
362 + certCacheMu.Unlock()
363 + }
364 +
365 return nil
366 }
367
@@ -339,6 +386,9 @@ func (t *TLS) Manage(names []string) error {
386 if err != nil {
387 return fmt.Errorf("automate: manage %v: %v", names, err)
388 }
389 + for _, name := range names {
390 + t.managing[name] = struct{}{}
391 + }
392 }
393
394 return nil
@@ -449,8 +499,27 @@ func (t *TLS) getAutomationPolicyForName(name string) *AutomationPolicy {
499
500 // AllMatchingCertificates returns the list of all certificates in
501 // the cache which could be used to satisfy the given SAN.
452 -func (t *TLS) AllMatchingCertificates(san string) []certmagic.Certificate {
453 - return t.certCache.AllMatchingCertificates(san)
502 +func AllMatchingCertificates(san string) []certmagic.Certificate {
503 + return certCache.AllMatchingCertificates(san)
504 +}
505 +
506 +func (t *TLS) HasCertificateForSubject(subject string) bool {
507 + certCacheMu.RLock()
508 + allMatchingCerts := certCache.AllMatchingCertificates(subject)
509 + certCacheMu.RUnlock()
510 + for _, cert := range allMatchingCerts {
511 + // check if the cert is manually loaded by this config
512 + if _, ok := t.loaded[cert.Hash()]; ok {
513 + return true
514 + }
515 + // check if the cert is automatically managed by this config
516 + for _, name := range cert.Names {
517 + if _, ok := t.managing[name]; ok {
518 + return true
519 + }
520 + }
521 + }
522 + return false
523 }
524
525 // keepStorageClean starts a goroutine that immediately cleans up all