Commit d18cf12f authored by Matthew Holt's avatar Matthew Holt

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.
parent b143bbdb
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) { ...@@ -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) { ...@@ -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 { ...@@ -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 { ...@@ -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) { ...@@ -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 ...@@ -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 ( ...@@ -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,15 +33,17 @@ func maintainAssets(configs []server.Config, stopChan chan struct{}) { ...@@ -33,15 +33,17 @@ 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)
} }
if n > 0 && OnChange != nil { }
err := OnChange() // even if there was an error, some renewals may have succeeded
if err != nil { if n > 0 && OnChange != nil {
log.Printf("[ERROR] onchange after cert renewal: %v\n", err) err := OnChange()
} if err != nil {
log.Printf("[ERROR] onchange after cert renewal: %v\n", err)
} }
} }
case <-ocspTicker.C: case <-ocspTicker.C:
...@@ -69,11 +71,20 @@ func maintainAssets(configs []server.Config, stopChan chan struct{}) { ...@@ -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) { ...@@ -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) { ...@@ -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) { ...@@ -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)
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment