mirror of
https://github.com/caddyserver/caddy.git
synced 2025-01-13 22:51:08 -05:00
Simplify timing mechanism for checking renewals
This commit is contained in:
parent
c626774da2
commit
38885e4301
3 changed files with 100 additions and 145 deletions
|
@ -7,7 +7,6 @@ import (
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"errors"
|
"errors"
|
||||||
"io/ioutil"
|
"io/ioutil"
|
||||||
"log"
|
|
||||||
"net/http"
|
"net/http"
|
||||||
"os"
|
"os"
|
||||||
"strings"
|
"strings"
|
||||||
|
@ -40,7 +39,8 @@ func Activate(configs []server.Config) ([]server.Config, error) {
|
||||||
configs = autoConfigure(&configs[i], configs)
|
configs = autoConfigure(&configs[i], configs)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// Handle cert renewal on Startup
|
|
||||||
|
// First renew any existing certificates that need it
|
||||||
processCertificateRenewal(configs)
|
processCertificateRenewal(configs)
|
||||||
|
|
||||||
// Group configs by LE email address; this will help us
|
// 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
|
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 (
|
var (
|
||||||
// Let's Encrypt account email to use if none provided
|
// Let's Encrypt account email to use if none provided
|
||||||
DefaultEmail string
|
DefaultEmail string
|
||||||
|
@ -431,13 +296,14 @@ var (
|
||||||
const (
|
const (
|
||||||
// The base URL to the Let's Encrypt CA
|
// The base URL to the Let's Encrypt CA
|
||||||
// TODO: Staging API URL is: https://acme-staging.api.letsencrypt.org
|
// 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"
|
caURL = "http://192.168.99.100:4000"
|
||||||
|
|
||||||
// The port to expose to the CA server for Simple HTTP Challenge
|
// The port to expose to the CA server for Simple HTTP Challenge
|
||||||
exposePort = "5001"
|
exposePort = "5001"
|
||||||
|
|
||||||
// Renewal Timer - Check renewals every x hours.
|
// How often to check certificates for renewal
|
||||||
renewTimer = 24
|
renewInterval = 24 * time.Hour
|
||||||
)
|
)
|
||||||
|
|
||||||
// KeySize represents the length of a key in bits.
|
// KeySize represents the length of a key in bits.
|
||||||
|
|
94
config/letsencrypt/renew.go
Normal file
94
config/letsencrypt/renew.go
Normal file
|
@ -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
|
||||||
|
}
|
|
@ -16,11 +16,6 @@ var storage = Storage(filepath.Join(app.DataFolder(), "letsencrypt"))
|
||||||
// forming file paths derived from it.
|
// forming file paths derived from it.
|
||||||
type Storage string
|
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.
|
// Sites gets the directory that stores site certificate and keys.
|
||||||
func (s Storage) Sites() string {
|
func (s Storage) Sites() string {
|
||||||
return filepath.Join(string(s), "sites")
|
return filepath.Join(string(s), "sites")
|
||||||
|
|
Loading…
Reference in a new issue