From 38885e4301299d496c2e244b2fffa1844c12e2d0 Mon Sep 17 00:00:00 2001 From: Matthew Holt Date: Tue, 20 Oct 2015 20:16:01 -0600 Subject: [PATCH] Simplify timing mechanism for checking renewals --- config/letsencrypt/letsencrypt.go | 146 ++---------------------------- config/letsencrypt/renew.go | 94 +++++++++++++++++++ config/letsencrypt/storage.go | 5 - 3 files changed, 100 insertions(+), 145 deletions(-) create mode 100644 config/letsencrypt/renew.go diff --git a/config/letsencrypt/letsencrypt.go b/config/letsencrypt/letsencrypt.go index 4f33cf74..2d9b3cf1 100644 --- a/config/letsencrypt/letsencrypt.go +++ b/config/letsencrypt/letsencrypt.go @@ -7,7 +7,6 @@ import ( "encoding/json" "errors" "io/ioutil" - "log" "net/http" "os" "strings" @@ -40,7 +39,8 @@ func Activate(configs []server.Config) ([]server.Config, error) { configs = autoConfigure(&configs[i], configs) } } - // Handle cert renewal on Startup + + // First renew any existing certificates that need it processCertificateRenewal(configs) // Group configs by LE email address; this will help us @@ -77,7 +77,7 @@ func Activate(configs []server.Config) ([]server.Config, error) { } } - go renewalFunc(configs) + go keepCertificatesRenewed(configs) return configs, nil } @@ -284,141 +284,6 @@ func redirPlaintextHost(cfg server.Config) server.Config { } } -func renewalFunc(configs []server.Config) { - nextRun, err := processCertificateRenewal(configs) - if err != nil { - log.Printf("[ERROR] Could not start renewal routine. %v", err) - return - } - - for { - timer := time.NewTimer(time.Duration(nextRun) * time.Hour) - <-timer.C - nextRun, err = processCertificateRenewal(configs) - if err != nil { - log.Printf("[ERROR] Renewal routing stopped. %v", err) - return - } - } -} - -// checkCertificateRenewal loops through all configured -// sites and looks for certificates to renew. Nothing is mutated -// through this function. The changes happen directly on disk. -func processCertificateRenewal(configs []server.Config) (int, error) { - log.Print("[INFO] Processing certificate renewals...") - // Check if we should run. If not, get out of here. - next, err := getNextRenewalShedule() - if err != nil { - return 0, err - } - - if next > 0 { - return next, nil - } - - // We are executing. Write the current timestamp into the file. - err = ioutil.WriteFile(storage.RenewTimerFile(), []byte(time.Now().UTC().Format(time.RFC3339)), 0600) - if err != nil { - return 0, err - } - next = renewTimer - - for _, cfg := range configs { - // Check if this entry is TLS enabled and managed by LE - if !cfg.TLS.Enabled || !existingCertAndKey(cfg.Host) { - continue - } - - // Read the certificate and get the NotAfter time. - certBytes, err := ioutil.ReadFile(storage.SiteCertFile(cfg.Host)) - if err != nil { - return 0, err - } - expTime, err := acme.GetPEMCertExpiration(certBytes) - if err != nil { - return 0, err - } - - // The time returned from the certificate is always in UTC. - // So calculate the time left with local time as UTC. - // Directly convert it to days for the following checks. - daysLeft := int(expTime.Sub(time.Now().UTC()).Hours() / 24) - - // Renew on two or less days remaining. - if daysLeft <= 2 { - log.Printf("[WARN] There are %d days left on the certificate of %s. Trying to renew now.", daysLeft, cfg.Host) - client, err := newClient(getEmail(cfg)) - if err != nil { - return 0, err - } - - // Read metadata - metaBytes, err := ioutil.ReadFile(storage.SiteMetaFile(cfg.Host)) - if err != nil { - return 0, err - } - - privBytes, err := ioutil.ReadFile(storage.SiteKeyFile(cfg.Host)) - if err != nil { - return 0, err - } - - var certMeta acme.CertificateResource - err = json.Unmarshal(metaBytes, &certMeta) - certMeta.Certificate = certBytes - certMeta.PrivateKey = privBytes - - // Renew certificate. - // TODO: revokeOld should be an option in the caddyfile - newCertMeta, err := client.RenewCertificate(certMeta, true) - if err != nil { - return 0, err - } - - saveCertsAndKeys([]acme.CertificateResource{newCertMeta}) - } - - // Warn on 14 days remaining - if daysLeft <= 14 { - log.Printf("[WARN] There are %d days left on the certificate of %s. Will renew on two days left.\n", daysLeft, cfg.Host) - } - } - - return next, nil -} - -// getNextRenewalShedule calculates the offset in hours the renew process should -// run from the current time. If the file the time is in does not exists, the -// function returns zero to trigger a renew asap. -func getNextRenewalShedule() (int, error) { - - // Check if the file exists. If it does not, return 0 to indicate immediate processing. - if _, err := os.Stat(storage.RenewTimerFile()); os.IsNotExist(err) { - return 0, nil - } - - renewTimeBytes, err := ioutil.ReadFile(storage.RenewTimerFile()) - if err != nil { - return 0, err - } - - renewalTime, err := time.Parse(time.RFC3339, string(renewTimeBytes)) - if err != nil { - return 0, err - } - - // The time read from the file was equal or more then 24 hours in the past, - // write the current time to the file and return true. - hoursSinceRenew := int(time.Now().UTC().Sub(renewalTime).Hours()) - - if hoursSinceRenew >= renewTimer { - return 0, nil - } - - return hoursSinceRenew, nil -} - var ( // Let's Encrypt account email to use if none provided DefaultEmail string @@ -431,13 +296,14 @@ var ( const ( // The base URL to the Let's Encrypt CA // TODO: Staging API URL is: https://acme-staging.api.letsencrypt.org + // TODO: Production endpoint is: https://acme-v01.api.letsencrypt.org caURL = "http://192.168.99.100:4000" // The port to expose to the CA server for Simple HTTP Challenge exposePort = "5001" - // Renewal Timer - Check renewals every x hours. - renewTimer = 24 + // How often to check certificates for renewal + renewInterval = 24 * time.Hour ) // KeySize represents the length of a key in bits. diff --git a/config/letsencrypt/renew.go b/config/letsencrypt/renew.go new file mode 100644 index 00000000..4bc992ba --- /dev/null +++ b/config/letsencrypt/renew.go @@ -0,0 +1,94 @@ +package letsencrypt + +import ( + "encoding/json" + "io/ioutil" + "log" + "time" + + "github.com/mholt/caddy/server" + "github.com/xenolf/lego/acme" +) + +// keepCertificatesRenewed 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. +func keepCertificatesRenewed(configs []server.Config) { + ticker := time.Tick(renewInterval) + for range ticker { + if err := processCertificateRenewal(configs); err != nil { + log.Printf("[ERROR] cert renewal: %v", err) + } + } +} + +// checkCertificateRenewal loops through all configured +// sites and looks for certificates to renew. Nothing is mutated +// through this function. The changes happen directly on disk. +func processCertificateRenewal(configs []server.Config) error { + log.Print("[INFO] Processing certificate renewals...") + + for _, cfg := range configs { + // Check if this entry is TLS enabled and managed by LE + if !cfg.TLS.Enabled || !existingCertAndKey(cfg.Host) { + continue + } + + // Read the certificate and get the NotAfter time. + certBytes, err := ioutil.ReadFile(storage.SiteCertFile(cfg.Host)) + if err != nil { + return err + } + expTime, err := acme.GetPEMCertExpiration(certBytes) + if err != nil { + return err + } + + // The time returned from the certificate is always in UTC. + // So calculate the time left with local time as UTC. + // Directly convert it to days for the following checks. + daysLeft := int(expTime.Sub(time.Now().UTC()).Hours() / 24) + + // Renew on two or less days remaining. + if daysLeft <= 2 { + log.Printf("[WARN] There are %d days left on the certificate of %s. Trying to renew now.", daysLeft, cfg.Host) + client, err := newClient(getEmail(cfg)) + if err != nil { + return err + } + + // Read metadata + metaBytes, err := ioutil.ReadFile(storage.SiteMetaFile(cfg.Host)) + if err != nil { + return err + } + + privBytes, err := ioutil.ReadFile(storage.SiteKeyFile(cfg.Host)) + if err != nil { + return err + } + + var certMeta acme.CertificateResource + err = json.Unmarshal(metaBytes, &certMeta) + certMeta.Certificate = certBytes + certMeta.PrivateKey = privBytes + + // Renew certificate. + // TODO: revokeOld should be an option in the caddyfile + newCertMeta, err := client.RenewCertificate(certMeta, true) + if err != nil { + return err + } + + saveCertsAndKeys([]acme.CertificateResource{newCertMeta}) + } + + // Warn on 14 days remaining + if daysLeft <= 14 { + log.Printf("[WARN] There are %d days left on the certificate of %s. Will renew on two days left.\n", daysLeft, cfg.Host) + } + } + + return nil +} diff --git a/config/letsencrypt/storage.go b/config/letsencrypt/storage.go index 05279820..ca4405a8 100644 --- a/config/letsencrypt/storage.go +++ b/config/letsencrypt/storage.go @@ -16,11 +16,6 @@ var storage = Storage(filepath.Join(app.DataFolder(), "letsencrypt")) // forming file paths derived from it. type Storage string -// RenewTimerFile returns the path to the file used for renewal timing. -func (s Storage) RenewTimerFile() string { - return filepath.Join(string(s), "lastrenew") -} - // Sites gets the directory that stores site certificate and keys. func (s Storage) Sites() string { return filepath.Join(string(s), "sites")