@ -125,23 +125,6 @@ func (cfg *Config) getCertificate(hello *tls.ClientHelloInfo) (cert Certificate,
return
return
}
}
}
}
// check the certCache directly to see if the SNI name is
// already the key of the certificate it wants; this implies
// that the SNI can contain the hash of a specific cert
// (chain) it wants and we will still be able to serve it up
// (this behavior, by the way, could be controversial as to
// whether it complies with RFC 6066 about SNI, but I think
// it does, soooo...)
// (this is how we solved the former ACME TLS-SNI challenge)
cfg . certCache . mu . RLock ( )
directCert , ok := cfg . certCache . cache [ name ]
cfg . certCache . mu . RUnlock ( )
if ok {
cert = directCert
matched = true
return
}
}
}
// otherwise, we're bingo on ammo; see issues
// otherwise, we're bingo on ammo; see issues
@ -162,18 +145,48 @@ func (cfg *Config) getCertificate(hello *tls.ClientHelloInfo) (cert Certificate,
// then all certificates in the cache will be passed in
// then all certificates in the cache will be passed in
// for the cfg.CertSelection to make the final decision.
// for the cfg.CertSelection to make the final decision.
func ( cfg * Config ) selectCert ( hello * tls . ClientHelloInfo , name string ) ( Certificate , bool ) {
func ( cfg * Config ) selectCert ( hello * tls . ClientHelloInfo , name string ) ( Certificate , bool ) {
logger := loggerNamed ( cfg . Logger , "handshake" )
choices := cfg . certCache . getAllMatchingCerts ( name )
choices := cfg . certCache . getAllMatchingCerts ( name )
if len ( choices ) == 0 {
if len ( choices ) == 0 {
if cfg . CertSelection == nil {
if cfg . CertSelection == nil {
if logger != nil {
logger . Debug ( "no matching certificates and no custom selection logic" , zap . String ( "identifier" , name ) )
}
return Certificate { } , false
return Certificate { } , false
}
}
if logger != nil {
logger . Debug ( "no matching certificate; will choose from all certificates" , zap . String ( "identifier" , name ) )
}
choices = cfg . certCache . getAllCerts ( )
choices = cfg . certCache . getAllCerts ( )
}
}
if logger != nil {
logger . Debug ( "choosing certificate" ,
zap . String ( "identifier" , name ) ,
zap . Int ( "num_choices" , len ( choices ) ) )
}
if cfg . CertSelection == nil {
if cfg . CertSelection == nil {
cert , err := DefaultCertificateSelector ( hello , choices )
cert , err := DefaultCertificateSelector ( hello , choices )
if logger != nil {
logger . Debug ( "default certificate selection results" ,
zap . Error ( err ) ,
zap . String ( "identifier" , name ) ,
zap . Strings ( "subjects" , cert . Names ) ,
zap . Bool ( "managed" , cert . managed ) ,
zap . String ( "issuer_key" , cert . issuerKey ) ,
zap . String ( "hash" , cert . hash ) )
}
return cert , err == nil
return cert , err == nil
}
}
cert , err := cfg . CertSelection . SelectCertificate ( hello , choices )
cert , err := cfg . CertSelection . SelectCertificate ( hello , choices )
if logger != nil {
logger . Debug ( "custom certificate selection results" ,
zap . Error ( err ) ,
zap . String ( "identifier" , name ) ,
zap . Strings ( "subjects" , cert . Names ) ,
zap . Bool ( "managed" , cert . managed ) ,
zap . String ( "issuer_key" , cert . issuerKey ) ,
zap . String ( "hash" , cert . hash ) )
}
return cert , err == nil
return cert , err == nil
}
}
@ -213,28 +226,54 @@ func DefaultCertificateSelector(hello *tls.ClientHelloInfo, choices []Certificat
//
//
// This function is safe for concurrent use.
// This function is safe for concurrent use.
func ( cfg * Config ) getCertDuringHandshake ( hello * tls . ClientHelloInfo , loadIfNecessary , obtainIfNecessary bool ) ( Certificate , error ) {
func ( cfg * Config ) getCertDuringHandshake ( hello * tls . ClientHelloInfo , loadIfNecessary , obtainIfNecessary bool ) ( Certificate , error ) {
log := loggerNamed ( cfg . Logger , "on_demand " )
log := loggerNamed ( cfg . Logger , "handshake " )
// First check our in-memory cache to see if we've already loaded it
// First check our in-memory cache to see if we've already loaded it
cert , matched , defaulted := cfg . getCertificate ( hello )
cert , matched , defaulted := cfg . getCertificate ( hello )
if matched {
if matched {
if log != nil {
log . Debug ( "matched certificate in cache" ,
zap . Strings ( "subjects" , cert . Names ) ,
zap . Bool ( "managed" , cert . managed ) ,
zap . Time ( "expiration" , cert . Leaf . NotAfter ) ,
zap . String ( "hash" , cert . hash ) )
}
if cert . managed && cfg . OnDemand != nil && obtainIfNecessary {
if cert . managed && cfg . OnDemand != nil && obtainIfNecessary {
// It's been reported before that if the machine goes to sleep (or
// It's been reported before that if the machine goes to sleep (or
// suspends the process) that certs which are already loaded into
// suspends the process) that certs which are already loaded into
// memory won't get renewed in the background, so we need to check
// memory won't get renewed in the background, so we need to check
// expiry on each handshake too, sigh:
// expiry on each handshake too, sigh:
// https://caddy.community/t/local-certificates-not-renewing-on-demand/9482
// https://caddy.community/t/local-certificates-not-renewing-on-demand/9482
return cfg . optionalMaintenance ( log , cert , hello )
return cfg . optionalMaintenance ( loggerNamed ( cfg . L ogger , "on_demand" ) , cert , hello )
}
}
return cert , nil
return cert , nil
}
}
name := cfg . getNameFromClientHello ( hello )
name := cfg . getNameFromClientHello ( hello )
// If OnDemand is enabled, then we might be able to load or
// We might be able to load or obtain a needed certificate. Load from
// obtain a needed certificate
// storage if OnDemand is enabled, or if there is the possibility that
if cfg . OnDemand != nil && loadIfNecessary {
// a statically-managed cert was evicted from a full cache.
cfg . certCache . mu . RLock ( )
cacheSize := len ( cfg . certCache . cache )
cfg . certCache . mu . RUnlock ( )
// A cert might have still been evicted from the cache even if the cache
// is no longer completely full; this happens if the newly-loaded cert is
// itself evicted (perhaps due to being expired or unmanaged at this point).
// Hence, we use an "almost full" metric to allow for the cache to not be
// perfectly full while still being able to load needed certs from storage.
// See https://caddy.community/t/error-tls-alert-internal-error-592-again/13272
// and caddyserver/caddy#4320.
cacheAlmostFull := float64 ( cacheSize ) >= ( float64 ( cfg . certCache . options . Capacity ) * .9 )
loadDynamically := cfg . OnDemand != nil || cacheAlmostFull
if loadDynamically && loadIfNecessary {
// Then check to see if we have one on disk
// Then check to see if we have one on disk
// TODO: As suggested here, https://caddy.community/t/error-tls-alert-internal-error-592-again/13272/30?u=matt,
// it might be a good idea to check with the DecisionFunc or allowlist first before even loading the certificate
// from storage, since if we can't renew it, why should we even try serving it (it will just get evicted after
// we get a return value of false anyway)?
loadedCert , err := cfg . CacheManagedCertificate ( name )
loadedCert , err := cfg . CacheManagedCertificate ( name )
if _ , ok := err . ( ErrNotExist ) ; ok {
if _ , ok := err . ( ErrNotExist ) ; ok {
// If no exact match, try a wildcard variant, which is something we can still use
// If no exact match, try a wildcard variant, which is something we can still use
@ -243,6 +282,13 @@ func (cfg *Config) getCertDuringHandshake(hello *tls.ClientHelloInfo, loadIfNece
loadedCert , err = cfg . CacheManagedCertificate ( strings . Join ( labels , "." ) )
loadedCert , err = cfg . CacheManagedCertificate ( strings . Join ( labels , "." ) )
}
}
if err == nil {
if err == nil {
if log != nil {
log . Debug ( "loaded certificate from storage" ,
zap . Strings ( "subjects" , loadedCert . Names ) ,
zap . Bool ( "managed" , loadedCert . managed ) ,
zap . Time ( "expiration" , loadedCert . Leaf . NotAfter ) ,
zap . String ( "hash" , loadedCert . hash ) )
}
loadedCert , err = cfg . handshakeMaintenance ( hello , loadedCert )
loadedCert , err = cfg . handshakeMaintenance ( hello , loadedCert )
if err != nil {
if err != nil {
if log != nil {
if log != nil {
@ -253,7 +299,7 @@ func (cfg *Config) getCertDuringHandshake(hello *tls.ClientHelloInfo, loadIfNece
}
}
return loadedCert , nil
return loadedCert , nil
}
}
if obtainIfNecessary {
if cfg . OnDemand != nil && obtainIfNecessary {
// By this point, we need to ask the CA for a certificate
// By this point, we need to ask the CA for a certificate
return cfg . obtainOnDemandCertificate ( hello )
return cfg . obtainOnDemandCertificate ( hello )
}
}
@ -261,9 +307,28 @@ func (cfg *Config) getCertDuringHandshake(hello *tls.ClientHelloInfo, loadIfNece
// Fall back to the default certificate if there is one
// Fall back to the default certificate if there is one
if defaulted {
if defaulted {
if log != nil {
log . Debug ( "fell back to default certificate" ,
zap . Strings ( "subjects" , cert . Names ) ,
zap . Bool ( "managed" , cert . managed ) ,
zap . Time ( "expiration" , cert . Leaf . NotAfter ) ,
zap . String ( "hash" , cert . hash ) )
}
return cert , nil
return cert , nil
}
}
if log != nil {
log . Debug ( "no certificate matching TLS ClientHello" ,
zap . String ( "server_name" , hello . ServerName ) ,
zap . String ( "remote" , hello . Conn . RemoteAddr ( ) . String ( ) ) ,
zap . String ( "identifier" , name ) ,
zap . Uint16s ( "cipher_suites" , hello . CipherSuites ) ,
zap . Float64 ( "cert_cache_fill" , float64 ( cacheSize ) / float64 ( cfg . certCache . options . Capacity ) ) , // may be approximate! because we are not within the lock
zap . Bool ( "load_if_necessary" , loadIfNecessary ) ,
zap . Bool ( "obtain_if_necessary" , obtainIfNecessary ) ,
zap . Bool ( "on_demand" , cfg . OnDemand != nil ) )
}
return Certificate { } , fmt . Errorf ( "no certificate available for '%s'" , name )
return Certificate { } , fmt . Errorf ( "no certificate available for '%s'" , name )
}
}
@ -371,7 +436,8 @@ func (cfg *Config) obtainOnDemandCertificate(hello *tls.ClientHelloInfo) (Certif
}
}
// TODO: use a proper context; we use one with timeout because retries are enabled because interactive is false
// TODO: use a proper context; we use one with timeout because retries are enabled because interactive is false
ctx , cancel := context . WithTimeout ( context . TODO ( ) , 90 * time . Second )
// (timeout duration is based on https://caddy.community/t/zerossl-dns-challenge-failing-often-route53-plugin/13822/24?u=matt)
ctx , cancel := context . WithTimeout ( context . TODO ( ) , 180 * time . Second )
defer cancel ( )
defer cancel ( )
// Obtain the certificate
// Obtain the certificate
@ -459,7 +525,7 @@ func (cfg *Config) renewDynamicCertificate(hello *tls.ClientHelloInfo, currentCe
// renewing it, so we might as well serve what we have without blocking
// renewing it, so we might as well serve what we have without blocking
if log != nil {
if log != nil {
log . Debug ( "certificate expires soon but is already being renewed; serving current certificate" ,
log . Debug ( "certificate expires soon but is already being renewed; serving current certificate" ,
zap . Strings ( "identifier s" , currentCert . Names ) ,
zap . Strings ( "subject s" , currentCert . Names ) ,
zap . Duration ( "remaining" , timeLeft ) )
zap . Duration ( "remaining" , timeLeft ) )
}
}
return currentCert , nil
return currentCert , nil
@ -470,7 +536,7 @@ func (cfg *Config) renewDynamicCertificate(hello *tls.ClientHelloInfo, currentCe
if log != nil {
if log != nil {
log . Debug ( "certificate has expired, but is already being renewed; waiting for renewal to complete" ,
log . Debug ( "certificate has expired, but is already being renewed; waiting for renewal to complete" ,
zap . Strings ( "identifier s" , currentCert . Names ) ,
zap . Strings ( "subject s" , currentCert . Names ) ,
zap . Time ( "expired" , currentCert . Leaf . NotAfter ) )
zap . Time ( "expired" , currentCert . Leaf . NotAfter ) )
}
}
@ -501,7 +567,7 @@ func (cfg *Config) renewDynamicCertificate(hello *tls.ClientHelloInfo, currentCe
if log != nil {
if log != nil {
log . Info ( "attempting certificate renewal" ,
log . Info ( "attempting certificate renewal" ,
zap . String ( "server_name" , name ) ,
zap . String ( "server_name" , name ) ,
zap . Strings ( "identifier s" , currentCert . Names ) ,
zap . Strings ( "subject s" , currentCert . Names ) ,
zap . Time ( "expiration" , currentCert . Leaf . NotAfter ) ,
zap . Time ( "expiration" , currentCert . Leaf . NotAfter ) ,
zap . Duration ( "remaining" , timeLeft ) )
zap . Duration ( "remaining" , timeLeft ) )
}
}