// Copyright 2015 Light Code Labs, LLC // // 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 ( "io/ioutil" "log" "os" "path/filepath" "time" "github.com/mholt/caddy" "golang.org/x/crypto/ocsp" ) func init() { // maintain assets while this package is imported, which is // always. we don't ever stop it, since we need it running. go maintainAssets(make(chan struct{})) } const ( // RenewInterval is how often to check certificates for renewal. RenewInterval = 12 * time.Hour // RenewDurationBefore is how long before expiration to renew certificates. RenewDurationBefore = (24 * time.Hour) * 30 // RenewDurationBeforeAtStartup is how long before expiration to require // a renewed certificate when the process is first starting up (see #1680). // A wider window between RenewDurationBefore and this value will allow // Caddy to start under duress but hopefully this duration will give it // enough time for the blockage to be relieved. RenewDurationBeforeAtStartup = (24 * time.Hour) * 7 // OCSPInterval is how often to check if OCSP stapling needs updating. OCSPInterval = 1 * time.Hour ) // maintainAssets is a permanently-blocking function // that loops indefinitely and, on a regular schedule, checks // certificates for expiration and initiates a renewal of certs // that are expiring soon. It also updates OCSP stapling and // performs other maintenance of assets. It should only be // called once per process. // // You must pass in the channel which you'll close when // maintenance should stop, to allow this goroutine to clean up // after itself and unblock. (Not that you HAVE to stop it...) func maintainAssets(stopChan chan struct{}) { renewalTicker := time.NewTicker(RenewInterval) ocspTicker := time.NewTicker(OCSPInterval) for { select { case <-renewalTicker.C: log.Println("[INFO] Scanning for expiring certificates") RenewManagedCertificates(false) log.Println("[INFO] Done checking certificates") case <-ocspTicker.C: log.Println("[INFO] Scanning for stale OCSP staples") UpdateOCSPStaples() DeleteOldStapleFiles() log.Println("[INFO] Done checking OCSP staples") case <-stopChan: renewalTicker.Stop() ocspTicker.Stop() log.Println("[INFO] Stopped background maintenance routine") return } } } // RenewManagedCertificates renews managed certificates, // including ones loaded on-demand. func RenewManagedCertificates(allowPrompts bool) (err error) { var renewQueue, deleteQueue []Certificate visitedNames := make(map[string]struct{}) certCacheMu.RLock() for name, cert := range certCache { if !cert.Config.Managed || cert.Config.SelfSigned { continue } // the list of names on this cert should never be empty... if cert.Names == nil || len(cert.Names) == 0 { log.Printf("[WARNING] Certificate keyed by '%s' has no names: %v - removing from cache", name, cert.Names) deleteQueue = append(deleteQueue, cert) continue } // skip names whose certificate we've already renewed if _, ok := visitedNames[name]; ok { continue } for _, name := range cert.Names { visitedNames[name] = struct{}{} } // if its time is up or ending soon, we need to try to renew it timeLeft := cert.NotAfter.Sub(time.Now().UTC()) if timeLeft < RenewDurationBefore { log.Printf("[INFO] Certificate for %v expires in %v; attempting renewal", cert.Names, timeLeft) if cert.Config == nil { log.Printf("[ERROR] %s: No associated TLS config; unable to renew", name) continue } // queue for renewal when we aren't in a read lock anymore // (the TLS-SNI challenge will need a write lock in order to // present the certificate, so we renew outside of read lock) renewQueue = append(renewQueue, cert) } } certCacheMu.RUnlock() // Perform renewals that are queued for _, cert := range renewQueue { // Get the name which we should use to renew this certificate; // we only support managing certificates with one name per cert, // so this should be easy. We can't rely on cert.Config.Hostname // because it may be a wildcard value from the Caddyfile (e.g. // *.something.com) which, as of Jan. 2017, is not supported by ACME. var renewName string for _, name := range cert.Names { if name != "" { renewName = name break } } // perform renewal err := cert.Config.RenewCert(renewName, allowPrompts) if err != nil { if allowPrompts { // Certificate renewal failed and the operator is present. See a discussion // about this in issue 642. For a while, we only stopped if the certificate // was expired, but in reality, there is no difference between reporting // it now versus later, except that there's somebody present to deal with // it right now. timeLeft := cert.NotAfter.Sub(time.Now().UTC()) if timeLeft < RenewDurationBeforeAtStartup { // See issue 1680. Only fail at startup if the certificate is dangerously // close to expiration. return err } } log.Printf("[ERROR] %v", err) if cert.Config.OnDemand { // loaded dynamically, removed dynamically deleteQueue = append(deleteQueue, cert) } } else { // successful renewal, so update in-memory cache by loading // renewed certificate so it will be used with handshakes // we must delete all the names this cert services from the cache // so that we can replace the certificate, because replacing names // already in the cache is not allowed, to avoid later conflicts // with renewals. // TODO: It would be nice if this whole operation were idempotent; // i.e. a thread-safe function to replace a certificate in the cache, // see also handshake.go for on-demand maintenance. certCacheMu.Lock() for _, name := range cert.Names { delete(certCache, name) } certCacheMu.Unlock() // put the certificate in the cache _, err := cert.Config.CacheManagedCertificate(cert.Names[0]) if err != nil { if allowPrompts { return err // operator is present, so report error immediately } log.Printf("[ERROR] %v", err) } } } // Apply queued deletion changes to the cache for _, cert := range deleteQueue { certCacheMu.Lock() for _, name := range cert.Names { delete(certCache, name) } certCacheMu.Unlock() } return nil } // UpdateOCSPStaples updates the OCSP stapling in all // eligible, cached certificates. // // OCSP maintenance strives to abide the relevant points on // Ryan Sleevi's recommendations for good OCSP support: // https://gist.github.com/sleevi/5efe9ef98961ecfb4da8 func UpdateOCSPStaples() { // Create a temporary place to store updates // until we release the potentially long-lived // read lock and use a short-lived write lock. type ocspUpdate struct { rawBytes []byte parsed *ocsp.Response } updated := make(map[string]ocspUpdate) // A single SAN certificate maps to multiple names, so we use this // set to make sure we don't waste cycles checking OCSP for the same // certificate multiple times. visited := make(map[string]struct{}) certCacheMu.RLock() for name, cert := range certCache { // skip this certificate if we've already visited it, // and if not, mark all the names as visited if _, ok := visited[name]; ok { continue } for _, n := range cert.Names { visited[n] = struct{}{} } // no point in updating OCSP for expired certificates if time.Now().After(cert.NotAfter) { continue } var lastNextUpdate time.Time if cert.OCSP != nil { lastNextUpdate = cert.OCSP.NextUpdate if freshOCSP(cert.OCSP) { // no need to update staple if ours is still fresh continue } } err := stapleOCSP(&cert, nil) if err != nil { if cert.OCSP != nil { // if there was no staple before, that's fine; otherwise we should log the error log.Printf("[ERROR] Checking OCSP: %v", err) } continue } // By this point, we've obtained the latest OCSP response. // If there was no staple before, or if the response is updated, make // sure we apply the update to all names on the certificate. if cert.OCSP != nil && (lastNextUpdate.IsZero() || lastNextUpdate != cert.OCSP.NextUpdate) { log.Printf("[INFO] Advancing OCSP staple for %v from %s to %s", cert.Names, lastNextUpdate, cert.OCSP.NextUpdate) for _, n := range cert.Names { // BUG: If this certificate has names on it that appear on another // certificate in the cache, AND the other certificate is keyed by // that name in the cache, then this method of 'queueing' the staple // update will cause this certificate's new OCSP to be stapled to // a different certificate! See: // https://caddy.community/t/random-ocsp-response-errors-for-random-clients/2473?u=matt // This problem should be avoided if names on certificates in the // cache don't overlap with regards to the cache keys. // (This is isn't a bug anymore, since we're careful when we add // certificates to the cache by skipping keying when key already exists.) updated[n] = ocspUpdate{rawBytes: cert.Certificate.OCSPStaple, parsed: cert.OCSP} } } } certCacheMu.RUnlock() // This write lock should be brief since we have all the info we need now. certCacheMu.Lock() for name, update := range updated { cert := certCache[name] cert.OCSP = update.parsed cert.Certificate.OCSPStaple = update.rawBytes certCache[name] = cert } certCacheMu.Unlock() } // DeleteOldStapleFiles deletes cached OCSP staples that have expired. // TODO: Should we do this for certificates too? func DeleteOldStapleFiles() { files, err := ioutil.ReadDir(ocspFolder) if err != nil { // maybe just hasn't been created yet; no big deal return } for _, file := range files { if file.IsDir() { // weird, what's a folder doing inside the OCSP cache? continue } stapleFile := filepath.Join(ocspFolder, file.Name()) ocspBytes, err := ioutil.ReadFile(stapleFile) if err != nil { continue } resp, err := ocsp.ParseResponse(ocspBytes, nil) if err != nil { // contents are invalid; delete it err = os.Remove(stapleFile) if err != nil { log.Printf("[ERROR] Purging corrupt staple file %s: %v", stapleFile, err) } } if time.Now().After(resp.NextUpdate) { // response has expired; delete it err = os.Remove(stapleFile) if err != nil { log.Printf("[ERROR] Purging expired staple file %s: %v", stapleFile, err) } } } } // freshOCSP returns true if resp is still fresh, // meaning that it is not expedient to get an // updated response from the OCSP server. func freshOCSP(resp *ocsp.Response) bool { // start checking OCSP staple about halfway through validity period for good measure refreshTime := resp.ThisUpdate.Add(resp.NextUpdate.Sub(resp.ThisUpdate) / 2) return time.Now().Before(refreshTime) } var ocspFolder = filepath.Join(caddy.AssetsPath(), "ocsp")