mirror of
https://github.com/caddyserver/caddy.git
synced 2024-12-23 22:27:38 -05:00
letsencrypt: Fixed renewals
By chaining in a middleware handler and using newly exposed hooks from the acme package, we're able to proxy ACME requests on port 443 to the ACME client listening on a different port.
This commit is contained in:
parent
b143bbdbaa
commit
d18cf12f14
3 changed files with 132 additions and 16 deletions
67
caddy/letsencrypt/handler.go
Normal file
67
caddy/letsencrypt/handler.go
Normal file
|
@ -0,0 +1,67 @@
|
||||||
|
package letsencrypt
|
||||||
|
|
||||||
|
import (
|
||||||
|
"crypto/tls"
|
||||||
|
"net/http"
|
||||||
|
"net/http/httputil"
|
||||||
|
"net/url"
|
||||||
|
"sync"
|
||||||
|
"sync/atomic"
|
||||||
|
|
||||||
|
"github.com/mholt/caddy/middleware"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Handler is a Caddy middleware that can proxy ACME requests
|
||||||
|
// to the real ACME endpoint. This is necessary to renew certificates
|
||||||
|
// while the server is running. Obviously, a site served on port
|
||||||
|
// 443 (HTTPS) binds to that port, so another listener created by
|
||||||
|
// our acme client can't bind successfully and solve the challenge.
|
||||||
|
// Thus, we chain this handler in so that it can, when activated,
|
||||||
|
// proxy ACME requests to an ACME client listening on an alternate
|
||||||
|
// port.
|
||||||
|
type Handler struct {
|
||||||
|
sync.Mutex // protects the ChallengePath property
|
||||||
|
Next middleware.Handler
|
||||||
|
ChallengeActive int32 // use sync/atomic for speed to set/get this flag
|
||||||
|
ChallengePath string // the exact request path to match before proxying
|
||||||
|
}
|
||||||
|
|
||||||
|
// ServeHTTP is basically a no-op unless an ACME challenge is active on this host
|
||||||
|
// and the request path matches the expected path exactly.
|
||||||
|
func (h *Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) (int, error) {
|
||||||
|
// Only if challenge is active
|
||||||
|
if atomic.LoadInt32(&h.ChallengeActive) == 1 {
|
||||||
|
h.Lock()
|
||||||
|
path := h.ChallengePath
|
||||||
|
h.Unlock()
|
||||||
|
|
||||||
|
// Request path must be correct; if so, proxy to ACME client
|
||||||
|
if r.URL.Path == path {
|
||||||
|
upstream, err := url.Parse("https://" + r.Host + ":" + alternatePort)
|
||||||
|
if err != nil {
|
||||||
|
return http.StatusInternalServerError, err
|
||||||
|
}
|
||||||
|
proxy := httputil.NewSingleHostReverseProxy(upstream)
|
||||||
|
proxy.Transport = &http.Transport{
|
||||||
|
TLSClientConfig: &tls.Config{InsecureSkipVerify: true}, // client uses self-signed cert
|
||||||
|
}
|
||||||
|
proxy.ServeHTTP(w, r)
|
||||||
|
return 0, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return h.Next.ServeHTTP(w, r)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ChallengeOn enables h to proxy ACME requests.
|
||||||
|
func (h *Handler) ChallengeOn(challengePath string) {
|
||||||
|
h.Lock()
|
||||||
|
h.ChallengePath = challengePath
|
||||||
|
h.Unlock()
|
||||||
|
atomic.StoreInt32(&h.ChallengeActive, 1)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ChallengeOff disables ACME proxying from this h.
|
||||||
|
func (h *Handler) ChallengeOff(success bool) {
|
||||||
|
atomic.StoreInt32(&h.ChallengeActive, 0)
|
||||||
|
}
|
|
@ -82,7 +82,7 @@ func Activate(configs []server.Config) ([]server.Config, error) {
|
||||||
return configs, errors.New("error creating client: " + err.Error())
|
return configs, errors.New("error creating client: " + err.Error())
|
||||||
}
|
}
|
||||||
|
|
||||||
// client is ready, so let's get free, trusted SSL certificates! yeah!
|
// client is ready, so let's get free, trusted SSL certificates!
|
||||||
Obtain:
|
Obtain:
|
||||||
certificates, failures := obtainCertificates(client, serverConfigs)
|
certificates, failures := obtainCertificates(client, serverConfigs)
|
||||||
if len(failures) > 0 {
|
if len(failures) > 0 {
|
||||||
|
@ -128,7 +128,7 @@ func Activate(configs []server.Config) ([]server.Config, error) {
|
||||||
}
|
}
|
||||||
|
|
||||||
// renew all certificates that need renewal
|
// renew all certificates that need renewal
|
||||||
renewCertificates(configs)
|
renewCertificates(configs, false)
|
||||||
|
|
||||||
// keep certificates renewed and OCSP stapling updated
|
// keep certificates renewed and OCSP stapling updated
|
||||||
go maintainAssets(configs, stopChan)
|
go maintainAssets(configs, stopChan)
|
||||||
|
@ -167,8 +167,8 @@ func configQualifies(cfg server.Config, allConfigs []server.Config) bool {
|
||||||
cfg.Host != "" &&
|
cfg.Host != "" &&
|
||||||
cfg.Host != "0.0.0.0" &&
|
cfg.Host != "0.0.0.0" &&
|
||||||
cfg.Host != "::1" &&
|
cfg.Host != "::1" &&
|
||||||
!strings.HasPrefix(cfg.Host, "127.") &&
|
!strings.HasPrefix(cfg.Host, "127.") && // to use a boulder on your own machine, add fake domain to hosts file
|
||||||
// TODO: Also exclude 10.* and 192.168.* addresses?
|
// not excluding 10.* and 192.168.* hosts for possibility of running internal Boulder instance
|
||||||
|
|
||||||
// make sure an HTTPS version of this config doesn't exist in the list already
|
// make sure an HTTPS version of this config doesn't exist in the list already
|
||||||
!hostHasOtherScheme(cfg.Host, "https", allConfigs)
|
!hostHasOtherScheme(cfg.Host, "https", allConfigs)
|
||||||
|
@ -215,6 +215,14 @@ func existingCertAndKey(host string) bool {
|
||||||
// disk (if already exists) or created new and registered via ACME
|
// disk (if already exists) or created new and registered via ACME
|
||||||
// and saved to the file system for next time.
|
// and saved to the file system for next time.
|
||||||
func newClient(leEmail string) (*acme.Client, error) {
|
func newClient(leEmail string) (*acme.Client, error) {
|
||||||
|
return newClientPort(leEmail, exposePort)
|
||||||
|
}
|
||||||
|
|
||||||
|
// newClientPort does the same thing as newClient, except it creates a
|
||||||
|
// new client with a custom port used for ACME transactions instead of
|
||||||
|
// the default port. This is important if the default port is already in
|
||||||
|
// use or is not exposed to the public, etc.
|
||||||
|
func newClientPort(leEmail, port string) (*acme.Client, error) {
|
||||||
// Look up or create the LE user account
|
// Look up or create the LE user account
|
||||||
leUser, err := getUser(leEmail)
|
leUser, err := getUser(leEmail)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -222,7 +230,7 @@ func newClient(leEmail string) (*acme.Client, error) {
|
||||||
}
|
}
|
||||||
|
|
||||||
// The client facilitates our communication with the CA server.
|
// The client facilitates our communication with the CA server.
|
||||||
client, err := acme.NewClient(CAUrl, &leUser, rsaKeySizeToUse, exposePort)
|
client, err := acme.NewClient(CAUrl, &leUser, rsaKeySizeToUse, port)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
@ -325,6 +333,17 @@ func autoConfigure(cfg *server.Config, allConfigs []server.Config) []server.Conf
|
||||||
cfg.Port = "https"
|
cfg.Port = "https"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Chain in ACME middleware proxy if we use up the SSL port
|
||||||
|
if cfg.Port == "https" || cfg.Port == "443" {
|
||||||
|
handler := new(Handler)
|
||||||
|
mid := func(next middleware.Handler) middleware.Handler {
|
||||||
|
handler.Next = next
|
||||||
|
return handler
|
||||||
|
}
|
||||||
|
cfg.Middleware["/"] = append(cfg.Middleware["/"], mid)
|
||||||
|
acmeHandlers[cfg.Host] = handler
|
||||||
|
}
|
||||||
|
|
||||||
// Set up http->https redirect as long as there isn't already
|
// Set up http->https redirect as long as there isn't already
|
||||||
// a http counterpart in the configs
|
// a http counterpart in the configs
|
||||||
if !hostHasOtherScheme(cfg.Host, "http", allConfigs) {
|
if !hostHasOtherScheme(cfg.Host, "http", allConfigs) {
|
||||||
|
@ -440,6 +459,11 @@ const (
|
||||||
// then port 443 must be forwarded to exposePort.
|
// then port 443 must be forwarded to exposePort.
|
||||||
exposePort = "443"
|
exposePort = "443"
|
||||||
|
|
||||||
|
// If port 443 is in use by a Caddy server instance, then this is
|
||||||
|
// port on which the acme client will solve challenges. (Whatever is
|
||||||
|
// listening on port 443 must proxy ACME requests to this port.)
|
||||||
|
alternatePort = "5033"
|
||||||
|
|
||||||
// How often to check certificates for renewal.
|
// How often to check certificates for renewal.
|
||||||
renewInterval = 24 * time.Hour
|
renewInterval = 24 * time.Hour
|
||||||
|
|
||||||
|
|
|
@ -33,17 +33,19 @@ func maintainAssets(configs []server.Config, stopChan chan struct{}) {
|
||||||
for {
|
for {
|
||||||
select {
|
select {
|
||||||
case <-renewalTicker.C:
|
case <-renewalTicker.C:
|
||||||
if n, errs := renewCertificates(configs); len(errs) > 0 {
|
n, errs := renewCertificates(configs, true)
|
||||||
|
if len(errs) > 0 {
|
||||||
for _, err := range errs {
|
for _, err := range errs {
|
||||||
log.Printf("[ERROR] cert renewal: %v\n", err)
|
log.Printf("[ERROR] cert renewal: %v\n", err)
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
// even if there was an error, some renewals may have succeeded
|
||||||
if n > 0 && OnChange != nil {
|
if n > 0 && OnChange != nil {
|
||||||
err := OnChange()
|
err := OnChange()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Printf("[ERROR] onchange after cert renewal: %v\n", err)
|
log.Printf("[ERROR] onchange after cert renewal: %v\n", err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
case <-ocspTicker.C:
|
case <-ocspTicker.C:
|
||||||
for bundle, oldStatus := range ocspStatus {
|
for bundle, oldStatus := range ocspStatus {
|
||||||
_, newStatus, err := acme.GetOCSPForCert(*bundle)
|
_, newStatus, err := acme.GetOCSPForCert(*bundle)
|
||||||
|
@ -69,11 +71,20 @@ func maintainAssets(configs []server.Config, stopChan chan struct{}) {
|
||||||
// through this function; all changes happen directly on disk.
|
// through this function; all changes happen directly on disk.
|
||||||
// It returns the number of certificates renewed and any errors
|
// It returns the number of certificates renewed and any errors
|
||||||
// that occurred. It only performs a renewal if necessary.
|
// that occurred. It only performs a renewal if necessary.
|
||||||
func renewCertificates(configs []server.Config) (int, []error) {
|
// If useCustomPort is true, a custom port will be used, and
|
||||||
|
// whatever is listening at 443 better proxy ACME requests to it.
|
||||||
|
// Otherwise, the acme package will create its own listener on 443.
|
||||||
|
func renewCertificates(configs []server.Config, useCustomPort bool) (int, []error) {
|
||||||
log.Print("[INFO] Processing certificate renewals...")
|
log.Print("[INFO] Processing certificate renewals...")
|
||||||
var errs []error
|
var errs []error
|
||||||
var n int
|
var n int
|
||||||
|
|
||||||
|
defer func() {
|
||||||
|
// reset these so as to not interfere with other challenges
|
||||||
|
acme.OnSimpleHTTPStart = nil
|
||||||
|
acme.OnSimpleHTTPEnd = nil
|
||||||
|
}()
|
||||||
|
|
||||||
for _, cfg := range configs {
|
for _, cfg := range configs {
|
||||||
// Host must be TLS-enabled and have existing assets managed by LE
|
// Host must be TLS-enabled and have existing assets managed by LE
|
||||||
if !cfg.TLS.Enabled || !existingCertAndKey(cfg.Host) {
|
if !cfg.TLS.Enabled || !existingCertAndKey(cfg.Host) {
|
||||||
|
@ -100,7 +111,12 @@ func renewCertificates(configs []server.Config) (int, []error) {
|
||||||
// Renew with two weeks or less remaining.
|
// Renew with two weeks or less remaining.
|
||||||
if daysLeft <= 14 {
|
if daysLeft <= 14 {
|
||||||
log.Printf("[INFO] There are %d days left on the certificate of %s. Trying to renew now.", daysLeft, cfg.Host)
|
log.Printf("[INFO] There are %d days left on the certificate of %s. Trying to renew now.", daysLeft, cfg.Host)
|
||||||
client, err := newClient("") // email not used for renewal
|
var client *acme.Client
|
||||||
|
if useCustomPort {
|
||||||
|
client, err = newClientPort("", alternatePort) // email not used for renewal
|
||||||
|
} else {
|
||||||
|
client, err = newClient("")
|
||||||
|
}
|
||||||
if err != nil {
|
if err != nil {
|
||||||
errs = append(errs, err)
|
errs = append(errs, err)
|
||||||
continue
|
continue
|
||||||
|
@ -124,6 +140,10 @@ func renewCertificates(configs []server.Config) (int, []error) {
|
||||||
certMeta.Certificate = certBytes
|
certMeta.Certificate = certBytes
|
||||||
certMeta.PrivateKey = privBytes
|
certMeta.PrivateKey = privBytes
|
||||||
|
|
||||||
|
// Tell the handler to accept and proxy acme request in order to solve challenge
|
||||||
|
acme.OnSimpleHTTPStart = acmeHandlers[cfg.Host].ChallengeOn
|
||||||
|
acme.OnSimpleHTTPEnd = acmeHandlers[cfg.Host].ChallengeOff
|
||||||
|
|
||||||
// Renew certificate.
|
// Renew certificate.
|
||||||
// TODO: revokeOld should be an option in the caddyfile
|
// TODO: revokeOld should be an option in the caddyfile
|
||||||
// TODO: bundle should be an option in the caddyfile as well :)
|
// TODO: bundle should be an option in the caddyfile as well :)
|
||||||
|
@ -148,11 +168,16 @@ func renewCertificates(configs []server.Config) (int, []error) {
|
||||||
|
|
||||||
saveCertsAndKeys([]acme.CertificateResource{newCertMeta})
|
saveCertsAndKeys([]acme.CertificateResource{newCertMeta})
|
||||||
n++
|
n++
|
||||||
} else if daysLeft <= 14 {
|
} else if daysLeft <= 30 {
|
||||||
// Warn on 14 days remaining
|
// Warn on 30 days remaining. TODO: Just do this once...
|
||||||
log.Printf("[WARN] There are %d days left on the certificate for %s. Will renew when 7 days remain.\n", daysLeft, cfg.Host)
|
log.Printf("[WARN] There are %d days left on the certificate for %s. Will renew when 14 days remain.\n", daysLeft, cfg.Host)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return n, errs
|
return n, errs
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// acmeHandlers is a map of host to ACME handler. These
|
||||||
|
// are used to proxy ACME requests to the ACME client
|
||||||
|
// when port 443 is in use.
|
||||||
|
var acmeHandlers = make(map[string]*Handler)
|
||||||
|
|
Loading…
Reference in a new issue