mirror of
				https://github.com/caddyserver/caddy.git
				synced 2025-10-31 10:37:24 -04:00 
			
		
		
		
	Simplify timing mechanism for checking renewals
This commit is contained in:
		
							parent
							
								
									c626774da2
								
							
						
					
					
						commit
						38885e4301
					
				| @ -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…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user