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())
|
||||
}
|
||||
|
||||
// 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,17 +33,19 @@ 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)
|
||||
}
|
||||
}
|
||||
// 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:
|
||||
for bundle, oldStatus := range ocspStatus {
|
||||
_, 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.
|
||||
// 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)
|
||||
|
|
Loading…
Reference in a new issue