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) {
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:
certificates, failures := obtainCertificates(client, serverConfigs)
if len(failures) > 0 {
......@@ -128,7 +128,7 @@ func Activate(configs []server.Config) ([]server.Config, error) {
}
// renew all certificates that need renewal
renewCertificates(configs)
renewCertificates(configs, false)
// keep certificates renewed and OCSP stapling updated
go maintainAssets(configs, stopChan)
......@@ -167,8 +167,8 @@ func configQualifies(cfg server.Config, allConfigs []server.Config) bool {
cfg.Host != "" &&
cfg.Host != "0.0.0.0" &&
cfg.Host != "::1" &&
!strings.HasPrefix(cfg.Host, "127.") &&
// TODO: Also exclude 10.* and 192.168.* addresses?
!strings.HasPrefix(cfg.Host, "127.") && // to use a boulder on your own machine, add fake domain to hosts file
// 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
!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
// and saved to the file system for next time.
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
leUser, err := getUser(leEmail)
if err != nil {
......@@ -222,7 +230,7 @@ func newClient(leEmail string) (*acme.Client, error) {
}
// 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 {
return nil, err
}
......@@ -325,6 +333,17 @@ func autoConfigure(cfg *server.Config, allConfigs []server.Config) []server.Conf
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
// a http counterpart in the configs
if !hostHasOtherScheme(cfg.Host, "http", allConfigs) {
......@@ -440,6 +459,11 @@ const (
// then port 443 must be forwarded to exposePort.
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.
renewInterval = 24 * time.Hour
......
......@@ -33,15 +33,17 @@ func maintainAssets(configs []server.Config, stopChan chan struct{}) {
for {
select {
case <-renewalTicker.C:
if n, errs := renewCertificates(configs); len(errs) > 0 {
n, errs := renewCertificates(configs, true)
if len(errs) > 0 {
for _, err := range errs {
log.Printf("[ERROR] cert renewal: %v\n", err)
}
if n > 0 && OnChange != nil {
err := OnChange()
if err != nil {
log.Printf("[ERROR] onchange after cert renewal: %v\n", err)
}
}
// even if there was an error, some renewals may have succeeded
if n > 0 && OnChange != nil {
err := OnChange()
if err != nil {
log.Printf("[ERROR] onchange after cert renewal: %v\n", err)
}
}
case <-ocspTicker.C:
......@@ -69,11 +71,20 @@ func maintainAssets(configs []server.Config, stopChan chan struct{}) {
// through this function; all changes happen directly on disk.
// It returns the number of certificates renewed and any errors
// 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...")
var errs []error
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 {
// Host must be TLS-enabled and have existing assets managed by LE
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.
if daysLeft <= 14 {
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 {
errs = append(errs, err)
continue
......@@ -124,6 +140,10 @@ func renewCertificates(configs []server.Config) (int, []error) {
certMeta.Certificate = certBytes
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.
// TODO: revokeOld should be an option in the caddyfile
// 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})
n++
} else if daysLeft <= 14 {
// Warn on 14 days remaining
log.Printf("[WARN] There are %d days left on the certificate for %s. Will renew when 7 days remain.\n", daysLeft, cfg.Host)
} else if daysLeft <= 30 {
// 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 14 days remain.\n", daysLeft, cfg.Host)
}
}
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