Commit 55601d3e authored by Matthew Holt's avatar Matthew Holt

letsencrypt: Fix OCSP stapling and restarts with new LE-capable hosts

Before, Caddy couldn't support graceful (zero-downtime) restarts when the reloaded Caddyfile had a host in it that was elligible for a LE certificate because the port was already in use. This commit makes it possible to do zero-downtime reloads and issue certificates for new hosts that need it. Supports only http-01 challenge at this time.

OCSP stapling is improved in that it updates before the expiration time when the validity window has shifted forward. See 30c94908. Before it only used to update when the status changed.

This commit also sets the user agent for Let's Encrypt requests with a string containing "Caddy".
parent 829a0f34
...@@ -190,7 +190,8 @@ func startServers(groupings bindingGroup) error { ...@@ -190,7 +190,8 @@ func startServers(groupings bindingGroup) error {
if err != nil { if err != nil {
return err return err
} }
s.HTTP2 = HTTP2 // TODO: This setting is temporary s.HTTP2 = HTTP2 // TODO: This setting is temporary
s.ReqCallback = letsencrypt.RequestCallback // ensures we can solve ACME challenges while running
var ln server.ListenerFile var ln server.ListenerFile
if IsRestart() { if IsRestart() {
......
...@@ -40,12 +40,12 @@ func TestSaveAndLoadRSAPrivateKey(t *testing.T) { ...@@ -40,12 +40,12 @@ func TestSaveAndLoadRSAPrivateKey(t *testing.T) {
} }
} }
// rsaPrivateKeyBytes returns the bytes of DER-encoded key.
func rsaPrivateKeyBytes(key *rsa.PrivateKey) []byte {
return x509.MarshalPKCS1PrivateKey(key)
}
// rsaPrivateKeysSame compares the bytes of a and b and returns true if they are the same. // rsaPrivateKeysSame compares the bytes of a and b and returns true if they are the same.
func rsaPrivateKeysSame(a, b *rsa.PrivateKey) bool { func rsaPrivateKeysSame(a, b *rsa.PrivateKey) bool {
return bytes.Equal(rsaPrivateKeyBytes(a), rsaPrivateKeyBytes(b)) return bytes.Equal(rsaPrivateKeyBytes(a), rsaPrivateKeyBytes(b))
} }
// rsaPrivateKeyBytes returns the bytes of DER-encoded key.
func rsaPrivateKeyBytes(key *rsa.PrivateKey) []byte {
return x509.MarshalPKCS1PrivateKey(key)
}
...@@ -2,30 +2,21 @@ package letsencrypt ...@@ -2,30 +2,21 @@ package letsencrypt
import ( import (
"crypto/tls" "crypto/tls"
"log"
"net" "net"
"net/http" "net/http"
"net/http/httputil" "net/http/httputil"
"net/url" "net/url"
"strings" "strings"
"github.com/mholt/caddy/middleware"
) )
const challengeBasePath = "/.well-known/acme-challenge" const challengeBasePath = "/.well-known/acme-challenge"
// Handler is a Caddy middleware that can proxy ACME challenge // RequestCallback proxies challenge requests to ACME client if the
// requests to the real ACME client endpoint. This is necessary // request path starts with challengeBasePath. It returns true if it
// to renew certificates while the server is running. // handled the request and no more needs to be done; it returns false
type Handler struct { // if this call was a no-op and the request still needs handling.
Next middleware.Handler func RequestCallback(w http.ResponseWriter, r *http.Request) bool {
//ChallengeActive int32 // (TODO) use sync/atomic to set/get this flag safely and efficiently
}
// 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) {
// Proxy challenge requests to ACME client
// TODO: Only do this if a challenge is active?
if strings.HasPrefix(r.URL.Path, challengeBasePath) { if strings.HasPrefix(r.URL.Path, challengeBasePath) {
scheme := "http" scheme := "http"
if r.TLS != nil { if r.TLS != nil {
...@@ -37,9 +28,11 @@ func (h *Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) (int, error) ...@@ -37,9 +28,11 @@ func (h *Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) (int, error)
hostname = r.URL.Host hostname = r.URL.Host
} }
upstream, err := url.Parse(scheme + "://" + hostname + ":" + alternatePort) upstream, err := url.Parse(scheme + "://" + hostname + ":" + AlternatePort)
if err != nil { if err != nil {
return http.StatusInternalServerError, err w.WriteHeader(http.StatusInternalServerError)
log.Printf("[ERROR] letsencrypt handler: %v", err)
return true
} }
proxy := httputil.NewSingleHostReverseProxy(upstream) proxy := httputil.NewSingleHostReverseProxy(upstream)
...@@ -48,8 +41,8 @@ func (h *Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) (int, error) ...@@ -48,8 +41,8 @@ func (h *Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) (int, error)
} }
proxy.ServeHTTP(w, r) proxy.ServeHTTP(w, r)
return 0, nil return true
} }
return h.Next.ServeHTTP(w, r) return false
} }
...@@ -7,11 +7,14 @@ import ( ...@@ -7,11 +7,14 @@ import (
"encoding/json" "encoding/json"
"errors" "errors"
"io/ioutil" "io/ioutil"
"net"
"net/http" "net/http"
"os" "os"
"strings" "strings"
"time" "time"
"golang.org/x/crypto/ocsp"
"github.com/mholt/caddy/caddy/setup" "github.com/mholt/caddy/caddy/setup"
"github.com/mholt/caddy/middleware" "github.com/mholt/caddy/middleware"
"github.com/mholt/caddy/middleware/redirect" "github.com/mholt/caddy/middleware/redirect"
...@@ -19,45 +22,20 @@ import ( ...@@ -19,45 +22,20 @@ import (
"github.com/xenolf/lego/acme" "github.com/xenolf/lego/acme"
) )
// Activate sets up TLS for each server config in configs func configureExisting(configs []server.Config) []server.Config {
// as needed. It only skips the config if the cert and key
// are already provided, if plaintext http is explicitly
// specified as the port, TLS is explicitly disabled, or
// the host looks like a loopback or wildcard address.
//
// This function may prompt the user to provide an email
// address if none is available through other means. It
// prefers the email address specified in the config, but
// if that is not available it will check the command line
// argument. If absent, it will use the most recent email
// address from last time. If there isn't one, the user
// will be prompted and shown SA link.
//
// Also note that calling this function activates asset
// management automatically, which keeps certificates
// renewed and OCSP stapling updated. This has the effect
// of causing restarts when assets are updated.
//
// Activate returns the updated list of configs, since
// some may have been appended, for example, to redirect
// plaintext HTTP requests to their HTTPS counterpart.
// This function only appends; it does not prepend or splice.
func Activate(configs []server.Config) ([]server.Config, error) {
// just in case previous caller forgot...
Deactivate()
// reset cached ocsp statuses from any previous activations
ocspStatus = make(map[*[]byte]int)
// Identify and configure any eligible hosts for which // Identify and configure any eligible hosts for which
// we already have certs and keys in storage from last time. // we already have certs and keys in storage from last time.
configLen := len(configs) // avoid infinite loop since this loop appends plaintext to the slice configLen := len(configs) // avoid infinite loop since this loop appends plaintext to the slice
for i := 0; i < configLen; i++ { for i := 0; i < configLen; i++ {
if existingCertAndKey(configs[i].Host) && configQualifies(configs, i) { if existingCertAndKey(configs[i].Host) && ConfigQualifies(configs, i) {
configs = autoConfigure(configs, i) configs = autoConfigure(configs, i)
} }
} }
return configs
}
// ObtainCertsAndConfigure obtains certificates for all qualifying configs.
func ObtainCertsAndConfigure(configs []server.Config, optPort string) ([]server.Config, error) {
// Group configs by email address; only configs that are eligible // Group configs by email address; only configs that are eligible
// for TLS management are included. We group by email so that we // for TLS management are included. We group by email so that we
// can request certificates in batches with the same client. // can request certificates in batches with the same client.
...@@ -73,67 +51,51 @@ func Activate(configs []server.Config) ([]server.Config, error) { ...@@ -73,67 +51,51 @@ func Activate(configs []server.Config) ([]server.Config, error) {
// config to use the certificates // config to use the certificates
for leEmail, cfgIndexes := range groupedConfigs { for leEmail, cfgIndexes := range groupedConfigs {
// make client to service this email address with CA server // make client to service this email address with CA server
client, err := newClient(leEmail) client, err := newClientPort(leEmail, optPort)
if err != nil { if err != nil {
return configs, errors.New("error creating client: " + err.Error()) return configs, errors.New("error creating client: " + err.Error())
} }
// little bit of housekeeping; gather the hostnames into a slice // let's get free, trusted SSL certificates!
hosts := make([]string, len(cfgIndexes)) for _, idx := range cfgIndexes {
for i, idx := range cfgIndexes { hostname := configs[idx].Host
hosts[i] = configs[idx].Host
} Obtain:
certificate, failures := client.ObtainCertificate([]string{hostname}, true)
// client is ready, so let's get free, trusted SSL certificates! if len(failures) == 0 {
Obtain: // Success - immediately save the certificate resource
certificates, failures := client.ObtainCertificates(hosts, true) err := saveCertResource(certificate)
if len(failures) > 0 { if err != nil {
// Build an error string to return, using all the failures in the list. return configs, errors.New("error saving assets for " + hostname + ": " + err.Error())
var errMsg string
// If an error is because of updated SA, only prompt user for agreement once
var promptedForAgreement bool
for domain, obtainErr := range failures {
// If the failure was simply because the terms have changed, re-prompt and re-try
if tosErr, ok := obtainErr.(acme.TOSError); ok {
if !Agreed && !promptedForAgreement {
Agreed = promptUserAgreement(tosErr.Detail, true) // TODO: Use latest URL
promptedForAgreement = true
}
if Agreed {
err := client.AgreeToTOS()
if err != nil {
return configs, errors.New("error agreeing to updated terms: " + err.Error())
}
goto Obtain
}
} }
} else {
// Error - either try to fix it or report them it to the user and abort
var errMsg string // we'll combine all the failures into a single error message
var promptedForAgreement bool // only prompt user for agreement at most once
for errDomain, obtainErr := range failures {
if obtainErr != nil {
if tosErr, ok := obtainErr.(acme.TOSError); ok {
if !Agreed && !promptedForAgreement {
Agreed = promptUserAgreement(tosErr.Detail, true) // TODO: Use latest URL
promptedForAgreement = true
}
if Agreed {
err := client.AgreeToTOS()
if err != nil {
return configs, errors.New("error agreeing to updated terms: " + err.Error())
}
goto Obtain
}
}
// If user did not agree or it was any other kind of error, just append to the list of errors // If user did not agree or it was any other kind of error, just append to the list of errors
errMsg += "[" + domain + "] failed to get certificate: " + obtainErr.Error() + "\n" errMsg += "[" + errDomain + "] failed to get certificate: " + obtainErr.Error() + "\n"
}
// Save the certs we did obtain, though, before leaving
if err := saveCertsAndKeys(certificates); err == nil {
if len(certificates) > 0 {
var certList []string
for _, cert := range certificates {
certList = append(certList, cert.Domain)
} }
errMsg += "Saved certificates for: " + strings.Join(certList, ", ") + "\n"
} }
} else {
errMsg += "Unable to save obtained certificates: " + err.Error() + "\n"
}
return configs, errors.New(errMsg) return configs, errors.New(errMsg)
} }
// ... that's it. save the certs, keys, and metadata files to disk
err = saveCertsAndKeys(certificates)
if err != nil {
return configs, errors.New("error saving assets: " + err.Error())
} }
// it all comes down to this: turning on TLS with all the new certs // it all comes down to this: turning on TLS with all the new certs
...@@ -142,7 +104,51 @@ func Activate(configs []server.Config) ([]server.Config, error) { ...@@ -142,7 +104,51 @@ func Activate(configs []server.Config) ([]server.Config, error) {
} }
} }
// renew all certificates that need renewal return configs, nil
}
// Activate sets up TLS for each server config in configs
// as needed. It only skips the config if the cert and key
// are already provided, if plaintext http is explicitly
// specified as the port, TLS is explicitly disabled, or
// the host looks like a loopback or wildcard address.
//
// This function may prompt the user to provide an email
// address if none is available through other means. It
// prefers the email address specified in the config, but
// if that is not available it will check the command line
// argument. If absent, it will use the most recent email
// address from last time. If there isn't one, the user
// will be prompted and shown SA link.
//
// Also note that calling this function activates asset
// management automatically, which keeps certificates
// renewed and OCSP stapling updated. This has the effect
// of causing restarts when assets are updated.
//
// Activate returns the updated list of configs, since
// some may have been appended, for example, to redirect
// plaintext HTTP requests to their HTTPS counterpart.
// This function only appends; it does not prepend or splice.
func Activate(configs []server.Config) ([]server.Config, error) {
var err error
// just in case previous caller forgot...
Deactivate()
// reset cached ocsp from any previous activations
ocspCache = make(map[*[]byte]*ocsp.Response)
// configure configs for which we have an existing certificate
configs = configureExisting(configs)
// obtain certificates for configs which need one, and make them use them
configs, err = ObtainCertsAndConfigure(configs, "")
if err != nil {
return configs, err
}
// renew all relevant certificates that need renewal; TODO: handle errors
renewCertificates(configs, false) renewCertificates(configs, false)
// keep certificates renewed and OCSP stapling updated // keep certificates renewed and OCSP stapling updated
...@@ -166,16 +172,17 @@ func Deactivate() (err error) { ...@@ -166,16 +172,17 @@ func Deactivate() (err error) {
return return
} }
// configQualifies returns true if the config at cfgIndex (within allConfigs) // ConfigQualifies returns true if the config at cfgIndex (within allConfigs)
// qualifes for automatic LE activation. It does NOT check to see if a cert // qualifes for automatic LE activation. It does NOT check to see if a cert
// and key already exist for the config. // and key already exist for the config.
func configQualifies(allConfigs []server.Config, cfgIndex int) bool { func ConfigQualifies(allConfigs []server.Config, cfgIndex int) bool {
cfg := allConfigs[cfgIndex] cfg := allConfigs[cfgIndex]
return cfg.TLS.Certificate == "" && // user could provide their own cert and key return cfg.TLS.Certificate == "" && // user could provide their own cert and key
cfg.TLS.Key == "" && cfg.TLS.Key == "" &&
// user can force-disable automatic HTTPS for this host // user can force-disable automatic HTTPS for this host
cfg.Port != "http" && cfg.Scheme != "http" &&
cfg.Port != "80" &&
cfg.TLS.LetsEncryptEmail != "off" && cfg.TLS.LetsEncryptEmail != "off" &&
// obviously we get can't certs for loopback or internal hosts // obviously we get can't certs for loopback or internal hosts
...@@ -193,13 +200,11 @@ func configQualifies(allConfigs []server.Config, cfgIndex int) bool { ...@@ -193,13 +200,11 @@ func configQualifies(allConfigs []server.Config, cfgIndex int) bool {
func HostQualifies(hostname string) bool { func HostQualifies(hostname string) bool {
return hostname != "localhost" && return hostname != "localhost" &&
strings.TrimSpace(hostname) != "" && strings.TrimSpace(hostname) != "" &&
hostname != "0.0.0.0" && net.ParseIP(hostname) == nil && // cannot be an IP address, see: https://community.letsencrypt.org/t/certificate-for-static-ip/84/2?u=mholt
hostname != "[::]" && // before parsing hostname != "[::]" && // before parsing
hostname != "::" && // after parsing hostname != "::" && // after parsing
hostname != "[::1]" && // before parsing hostname != "[::1]" && // before parsing
hostname != "::1" && // after parsing hostname != "::1" // after parsing
!strings.HasPrefix(hostname, "127.") // to use 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
} }
// groupConfigsByEmail groups configs by user email address. The returned map is // groupConfigsByEmail groups configs by user email address. The returned map is
...@@ -214,7 +219,7 @@ func groupConfigsByEmail(configs []server.Config) (map[string][]int, error) { ...@@ -214,7 +219,7 @@ func groupConfigsByEmail(configs []server.Config) (map[string][]int, error) {
// that we won't be obtaining certs for - this way we won't // that we won't be obtaining certs for - this way we won't
// bother the user for an email address unnecessarily and // bother the user for an email address unnecessarily and
// we don't obtain new certs for a host we already have certs for. // we don't obtain new certs for a host we already have certs for.
if existingCertAndKey(configs[i].Host) || !configQualifies(configs, i) { if existingCertAndKey(configs[i].Host) || !ConfigQualifies(configs, i) {
continue continue
} }
leEmail := getEmail(configs[i]) leEmail := getEmail(configs[i])
...@@ -258,10 +263,13 @@ func newClientPort(leEmail, port string) (*acme.Client, error) { ...@@ -258,10 +263,13 @@ func newClientPort(leEmail, port 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, port) client, err := acme.NewClient(CAUrl, &leUser, rsaKeySizeToUse)
if err != nil { if err != nil {
return nil, err return nil, err
} }
client.SetHTTPPort(port)
client.SetTLSPort(port)
client.ExcludeChallenges([]string{"tls-sni-01", "dns-01"}) // We can only guarantee http-01 at this time
// If not registered, the user must register an account with the CA // If not registered, the user must register an account with the CA
// and agree to terms // and agree to terms
...@@ -295,48 +303,37 @@ func newClientPort(leEmail, port string) (*acme.Client, error) { ...@@ -295,48 +303,37 @@ func newClientPort(leEmail, port string) (*acme.Client, error) {
return client, nil return client, nil
} }
// obtainCertificates obtains certificates from the CA server for // saveCertResource saves the certificate resource to disk. This
// the configurations in serverConfigs using client.
func obtainCertificates(client *acme.Client, serverConfigs []server.Config) ([]acme.CertificateResource, map[string]error) {
var hosts []string
for _, cfg := range serverConfigs {
hosts = append(hosts, cfg.Host)
}
return client.ObtainCertificates(hosts, true)
}
// saveCertificates saves each certificate resource to disk. This
// includes the certificate file itself, the private key, and the // includes the certificate file itself, the private key, and the
// metadata file. // metadata file.
func saveCertsAndKeys(certificates []acme.CertificateResource) error { func saveCertResource(cert acme.CertificateResource) error {
for _, cert := range certificates { err := os.MkdirAll(storage.Site(cert.Domain), 0700)
err := os.MkdirAll(storage.Site(cert.Domain), 0700) if err != nil {
if err != nil { return err
return err }
}
// Save cert // Save cert
err = ioutil.WriteFile(storage.SiteCertFile(cert.Domain), cert.Certificate, 0600) err = ioutil.WriteFile(storage.SiteCertFile(cert.Domain), cert.Certificate, 0600)
if err != nil { if err != nil {
return err return err
} }
// Save private key // Save private key
err = ioutil.WriteFile(storage.SiteKeyFile(cert.Domain), cert.PrivateKey, 0600) err = ioutil.WriteFile(storage.SiteKeyFile(cert.Domain), cert.PrivateKey, 0600)
if err != nil { if err != nil {
return err return err
} }
// Save cert metadata // Save cert metadata
jsonBytes, err := json.MarshalIndent(&cert, "", "\t") jsonBytes, err := json.MarshalIndent(&cert, "", "\t")
if err != nil { if err != nil {
return err return err
} }
err = ioutil.WriteFile(storage.SiteMetaFile(cert.Domain), jsonBytes, 0600) err = ioutil.WriteFile(storage.SiteMetaFile(cert.Domain), jsonBytes, 0600)
if err != nil { if err != nil {
return err return err
}
} }
return nil return nil
} }
...@@ -351,52 +348,28 @@ func autoConfigure(allConfigs []server.Config, cfgIndex int) []server.Config { ...@@ -351,52 +348,28 @@ func autoConfigure(allConfigs []server.Config, cfgIndex int) []server.Config {
bundleBytes, err := ioutil.ReadFile(storage.SiteCertFile(cfg.Host)) bundleBytes, err := ioutil.ReadFile(storage.SiteCertFile(cfg.Host))
// TODO: Handle these errors better // TODO: Handle these errors better
if err == nil { if err == nil {
ocsp, status, err := acme.GetOCSPForCert(bundleBytes) ocspBytes, ocspResp, err := acme.GetOCSPForCert(bundleBytes)
ocspStatus[&bundleBytes] = status ocspCache[&bundleBytes] = ocspResp
if err == nil && status == acme.OCSPGood { if err == nil && ocspResp.Status == ocsp.Good {
cfg.TLS.OCSPStaple = ocsp cfg.TLS.OCSPStaple = ocspBytes
} }
} }
cfg.TLS.Certificate = storage.SiteCertFile(cfg.Host) cfg.TLS.Certificate = storage.SiteCertFile(cfg.Host)
cfg.TLS.Key = storage.SiteKeyFile(cfg.Host) cfg.TLS.Key = storage.SiteKeyFile(cfg.Host)
cfg.TLS.Enabled = true cfg.TLS.Enabled = true
// Ensure all defaults are set for the TLS config
setup.SetDefaultTLSParams(cfg) setup.SetDefaultTLSParams(cfg)
if cfg.Port == "" { if cfg.Port == "" {
cfg.Port = "https" cfg.Port = "443"
} }
// Set up http->https redirect as long as there isn't already a http counterpart // Set up http->https redirect as long as there isn't already a http counterpart
// in the configs and this isn't, for some reason, already on port 80. // in the configs and this isn't, for some reason, already on port 80.
// Also, the port 80 variant of this config is necessary for proxying challenge requests. // Also, the port 80 variant of this config is necessary for proxying challenge requests.
if !otherHostHasScheme(allConfigs, cfgIndex, "http") && if !otherHostHasScheme(allConfigs, cfgIndex, "http") && cfg.Port != "80" && cfg.Scheme != "http" {
cfg.Port != "80" && cfg.Port != "http" { // (would not be http port with current program flow, but just in case)
allConfigs = append(allConfigs, redirPlaintextHost(*cfg)) allConfigs = append(allConfigs, redirPlaintextHost(*cfg))
} }
// To support renewals, we need handlers at ports 80 and 443,
// depending on the challenge type that is used to complete renewal.
for i, c := range allConfigs {
if c.Address() == cfg.Host+":80" ||
c.Address() == cfg.Host+":443" ||
c.Address() == cfg.Host+":http" ||
c.Address() == cfg.Host+":https" {
// Each virtualhost must have their own handlers, or the chaining gets messed up when middlewares are compiled!
handler := new(Handler)
mid := func(next middleware.Handler) middleware.Handler {
handler.Next = next
return handler
}
// TODO: Currently, acmeHandlers are not referenced, but we need to add a way to toggle
// their proxy functionality -- or maybe not. Gotta figure this out for sure.
acmeHandlers[c.Address()] = handler
allConfigs[i].Middleware["/"] = append(allConfigs[i].Middleware["/"], mid)
}
}
return allConfigs return allConfigs
} }
...@@ -406,21 +379,17 @@ func autoConfigure(allConfigs []server.Config, cfgIndex int) []server.Config { ...@@ -406,21 +379,17 @@ func autoConfigure(allConfigs []server.Config, cfgIndex int) []server.Config {
// "http" and "80". It does not tell you whether there is ANY config with scheme, // "http" and "80". It does not tell you whether there is ANY config with scheme,
// only if there's a different one with it. // only if there's a different one with it.
func otherHostHasScheme(allConfigs []server.Config, cfgIndex int, scheme string) bool { func otherHostHasScheme(allConfigs []server.Config, cfgIndex int, scheme string) bool {
if scheme == "80" { if scheme == "http" {
scheme = "http" scheme = "80"
} else if scheme == "443" { } else if scheme == "https" {
scheme = "https" scheme = "443"
} }
for i, otherCfg := range allConfigs { for i, otherCfg := range allConfigs {
if i == cfgIndex { if i == cfgIndex {
continue // has to be a config OTHER than the one we're comparing against continue // has to be a config OTHER than the one we're comparing against
} }
if otherCfg.Host == allConfigs[cfgIndex].Host { if otherCfg.Host == allConfigs[cfgIndex].Host && otherCfg.Port == scheme {
if (otherCfg.Port == scheme) || return true
(scheme == "https" && otherCfg.Port == "443") ||
(scheme == "http" && otherCfg.Port == "80") {
return true
}
} }
} }
return false return false
...@@ -432,7 +401,7 @@ func otherHostHasScheme(allConfigs []server.Config, cfgIndex int, scheme string) ...@@ -432,7 +401,7 @@ func otherHostHasScheme(allConfigs []server.Config, cfgIndex int, scheme string)
// to listen on the "http" port (port 80). // to listen on the "http" port (port 80).
func redirPlaintextHost(cfg server.Config) server.Config { func redirPlaintextHost(cfg server.Config) server.Config {
toURL := "https://" + cfg.Host toURL := "https://" + cfg.Host
if cfg.Port != "https" && cfg.Port != "http" { if cfg.Port != "443" && cfg.Port != "80" {
toURL += ":" + cfg.Port toURL += ":" + cfg.Port
} }
...@@ -449,7 +418,7 @@ func redirPlaintextHost(cfg server.Config) server.Config { ...@@ -449,7 +418,7 @@ func redirPlaintextHost(cfg server.Config) server.Config {
return server.Config{ return server.Config{
Host: cfg.Host, Host: cfg.Host,
Port: "http", Port: "80",
Middleware: map[string][]middleware.Middleware{ Middleware: map[string][]middleware.Middleware{
"/": []middleware.Middleware{redirMidware}, "/": []middleware.Middleware{redirMidware},
}, },
...@@ -504,17 +473,17 @@ var ( ...@@ -504,17 +473,17 @@ var (
// Some essential values related to the Let's Encrypt process // Some essential values related to the Let's Encrypt process
const ( const (
// alternatePort is the port on which the acme client will open a // AlternatePort is the port on which the acme client will open a
// listener and solve the CA's challenges. If this alternate port // listener and solve the CA's challenges. If this alternate port
// is used instead of the default port (80 or 443), then the // is used instead of the default port (80 or 443), then the
// default port for the challenge must be forwarded to this one. // default port for the challenge must be forwarded to this one.
alternatePort = "5033" AlternatePort = "5033"
// How often to check certificates for renewal. // RenewInterval is how often to check certificates for renewal.
renewInterval = 24 * time.Hour RenewInterval = 24 * time.Hour
// How often to update OCSP stapling. // OCSPInterval is how often to check if OCSP stapling needs updating.
ocspInterval = 1 * time.Hour OCSPInterval = 1 * time.Hour
) )
// KeySize represents the length of a key in bits. // KeySize represents the length of a key in bits.
...@@ -522,22 +491,22 @@ type KeySize int ...@@ -522,22 +491,22 @@ type KeySize int
// Key sizes are used to determine the strength of a key. // Key sizes are used to determine the strength of a key.
const ( const (
ECC_224 KeySize = 224 Ecc224 KeySize = 224
ECC_256 = 256 Ecc256 = 256
RSA_2048 = 2048 Rsa2048 = 2048
RSA_4096 = 4096 Rsa4096 = 4096
) )
// rsaKeySizeToUse is the size to use for new RSA keys. // rsaKeySizeToUse is the size to use for new RSA keys.
// This shouldn't need to change except for in tests; // This shouldn't need to change except for in tests;
// the size can be drastically reduced for speed. // the size can be drastically reduced for speed.
var rsaKeySizeToUse = RSA_2048 var rsaKeySizeToUse = Rsa2048
// stopChan is used to signal the maintenance goroutine // stopChan is used to signal the maintenance goroutine
// to terminate. // to terminate.
var stopChan chan struct{} var stopChan chan struct{}
// ocspStatus maps certificate bundle to OCSP status at start. // ocspCache maps certificate bundle to OCSP response.
// It is used during regular OCSP checks to see if the OCSP // It is used during regular OCSP checks to see if the OCSP
// status has changed. // response needs to be updated.
var ocspStatus = make(map[*[]byte]int) var ocspCache = make(map[*[]byte]*ocsp.Response)
...@@ -23,9 +23,11 @@ func TestHostQualifies(t *testing.T) { ...@@ -23,9 +23,11 @@ func TestHostQualifies(t *testing.T) {
{"", false}, {"", false},
{" ", false}, {" ", false},
{"0.0.0.0", false}, {"0.0.0.0", false},
{"192.168.1.3", true}, {"192.168.1.3", false},
{"10.0.2.1", true}, {"10.0.2.1", false},
{"169.112.53.4", false},
{"foobar.com", true}, {"foobar.com", true},
{"sub.foobar.com", true},
} { } {
if HostQualifies(test.host) && !test.expect { if HostQualifies(test.host) && !test.expect {
t.Errorf("Test %d: Expected '%s' to NOT qualify, but it did", i, test.host) t.Errorf("Test %d: Expected '%s' to NOT qualify, but it did", i, test.host)
...@@ -39,14 +41,14 @@ func TestHostQualifies(t *testing.T) { ...@@ -39,14 +41,14 @@ func TestHostQualifies(t *testing.T) {
func TestRedirPlaintextHost(t *testing.T) { func TestRedirPlaintextHost(t *testing.T) {
cfg := redirPlaintextHost(server.Config{ cfg := redirPlaintextHost(server.Config{
Host: "example.com", Host: "example.com",
Port: "http", Port: "80",
}) })
// Check host and port // Check host and port
if actual, expected := cfg.Host, "example.com"; actual != expected { if actual, expected := cfg.Host, "example.com"; actual != expected {
t.Errorf("Expected redir config to have host %s but got %s", expected, actual) t.Errorf("Expected redir config to have host %s but got %s", expected, actual)
} }
if actual, expected := cfg.Port, "http"; actual != expected { if actual, expected := cfg.Port, "80"; actual != expected {
t.Errorf("Expected redir config to have port '%s' but got '%s'", expected, actual) t.Errorf("Expected redir config to have port '%s' but got '%s'", expected, actual)
} }
......
...@@ -27,8 +27,8 @@ var OnChange func() error ...@@ -27,8 +27,8 @@ var OnChange func() error
// which you'll close when maintenance should stop, to allow this // which you'll close when maintenance should stop, to allow this
// goroutine to clean up after itself and unblock. // goroutine to clean up after itself and unblock.
func maintainAssets(configs []server.Config, stopChan chan struct{}) { func maintainAssets(configs []server.Config, stopChan chan struct{}) {
renewalTicker := time.NewTicker(renewInterval) renewalTicker := time.NewTicker(RenewInterval)
ocspTicker := time.NewTicker(ocspInterval) ocspTicker := time.NewTicker(OCSPInterval)
for { for {
select { select {
...@@ -47,15 +47,25 @@ func maintainAssets(configs []server.Config, stopChan chan struct{}) { ...@@ -47,15 +47,25 @@ func maintainAssets(configs []server.Config, stopChan chan struct{}) {
} }
} }
case <-ocspTicker.C: case <-ocspTicker.C:
for bundle, oldStatus := range ocspStatus { for bundle, oldResp := range ocspCache {
_, newStatus, err := acme.GetOCSPForCert(*bundle) // start checking OCSP staple about halfway through validity period for good measure
if err == nil && newStatus != oldStatus && OnChange != nil { refreshTime := oldResp.ThisUpdate.Add(oldResp.NextUpdate.Sub(oldResp.ThisUpdate) / 10)
log.Printf("[INFO] OCSP status changed from %v to %v", oldStatus, newStatus) if time.Now().After(refreshTime) {
err := OnChange() _, newResp, err := acme.GetOCSPForCert(*bundle)
if err != nil { if err != nil {
log.Printf("[ERROR] OnChange after OCSP update: %v", err) log.Printf("[ERROR] Checking OCSP for bundle: %v", err)
continue
}
if newResp.NextUpdate != oldResp.NextUpdate {
if OnChange != nil {
log.Printf("[INFO] Updating OCSP stapling to extend validity period to %v", newResp.NextUpdate)
err := OnChange()
if err != nil {
log.Printf("[ERROR] OnChange after OCSP trigger: %v", err)
}
break
}
} }
break
} }
} }
case <-stopChan: case <-stopChan:
...@@ -107,7 +117,7 @@ func renewCertificates(configs []server.Config, useCustomPort bool) (int, []erro ...@@ -107,7 +117,7 @@ func renewCertificates(configs []server.Config, useCustomPort bool) (int, []erro
log.Printf("[INFO] Certificate for %s has %d days remaining; attempting renewal", cfg.Host, daysLeft) log.Printf("[INFO] Certificate for %s has %d days remaining; attempting renewal", cfg.Host, daysLeft)
var client *acme.Client var client *acme.Client
if useCustomPort { if useCustomPort {
client, err = newClientPort("", alternatePort) // email not used for renewal client, err = newClientPort("", AlternatePort) // email not used for renewal
} else { } else {
client, err = newClient("") client, err = newClient("")
} }
...@@ -134,7 +144,7 @@ func renewCertificates(configs []server.Config, useCustomPort bool) (int, []erro ...@@ -134,7 +144,7 @@ func renewCertificates(configs []server.Config, useCustomPort bool) (int, []erro
// Renew certificate // Renew certificate
Renew: Renew:
newCertMeta, err := client.RenewCertificate(certMeta, true, true) newCertMeta, err := client.RenewCertificate(certMeta, true)
if err != nil { if err != nil {
if _, ok := err.(acme.TOSError); ok { if _, ok := err.(acme.TOSError); ok {
err := client.AgreeToTOS() err := client.AgreeToTOS()
...@@ -145,24 +155,20 @@ func renewCertificates(configs []server.Config, useCustomPort bool) (int, []erro ...@@ -145,24 +155,20 @@ func renewCertificates(configs []server.Config, useCustomPort bool) (int, []erro
} }
time.Sleep(10 * time.Second) time.Sleep(10 * time.Second)
newCertMeta, err = client.RenewCertificate(certMeta, true, true) newCertMeta, err = client.RenewCertificate(certMeta, true)
if err != nil { if err != nil {
errs = append(errs, err) errs = append(errs, err)
continue continue
} }
} }
saveCertsAndKeys([]acme.CertificateResource{newCertMeta}) saveCertResource(newCertMeta)
n++ n++
} else if daysLeft <= 30 { } else if daysLeft <= 21 {
// Warn on 30 days remaining. TODO: Just do this once... // Warn on 21 days remaining. TODO: Just do this once...
log.Printf("[WARNING] Certificate for %s has %d days remaining; will automatically renew when 14 days remain\n", cfg.Host, daysLeft) log.Printf("[WARNING] Certificate for %s has %d days remaining; will automatically renew when 14 days remain\n", cfg.Host, daysLeft)
} }
} }
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.
var acmeHandlers = make(map[string]*Handler)
...@@ -3,11 +3,17 @@ ...@@ -3,11 +3,17 @@
package caddy package caddy
import ( import (
"bytes"
"encoding/gob" "encoding/gob"
"errors"
"io/ioutil" "io/ioutil"
"log" "log"
"os" "os"
"os/exec" "os/exec"
"path"
"github.com/mholt/caddy/caddy/letsencrypt"
"github.com/mholt/caddy/server"
) )
func init() { func init() {
...@@ -33,6 +39,12 @@ func Restart(newCaddyfile Input) error { ...@@ -33,6 +39,12 @@ func Restart(newCaddyfile Input) error {
caddyfileMu.Unlock() caddyfileMu.Unlock()
} }
// Get certificates for any new hosts in the new Caddyfile without causing downtime
err := getCertsForNewCaddyfile(newCaddyfile)
if err != nil {
return errors.New("TLS preload: " + err.Error())
}
if len(os.Args) == 0 { // this should never happen, but... if len(os.Args) == 0 { // this should never happen, but...
os.Args = []string{""} os.Args = []string{""}
} }
...@@ -61,7 +73,7 @@ func Restart(newCaddyfile Input) error { ...@@ -61,7 +73,7 @@ func Restart(newCaddyfile Input) error {
// Pass along relevant file descriptors to child process; ordering // Pass along relevant file descriptors to child process; ordering
// is very important since we rely on these being in certain positions. // is very important since we rely on these being in certain positions.
extraFiles := []*os.File{sigwpipe} extraFiles := []*os.File{sigwpipe} // fd 3
// Add file descriptors of all the sockets // Add file descriptors of all the sockets
serversMu.Lock() serversMu.Lock()
...@@ -110,3 +122,45 @@ func Restart(newCaddyfile Input) error { ...@@ -110,3 +122,45 @@ func Restart(newCaddyfile Input) error {
// Looks like child is successful; we can exit gracefully. // Looks like child is successful; we can exit gracefully.
return Stop() return Stop()
} }
func getCertsForNewCaddyfile(newCaddyfile Input) error {
// parse the new caddyfile only up to (and including) TLS
// so we can know what we need to get certs for.
configs, _, _, err := loadConfigsUpToIncludingTLS(path.Base(newCaddyfile.Path()), bytes.NewReader(newCaddyfile.Body()))
if err != nil {
return errors.New("loading Caddyfile: " + err.Error())
}
// TODO: Yuck, this is hacky. port 443 not set until letsencrypt is activated, so we change it here.
for i := range configs {
if configs[i].Port == "" && letsencrypt.ConfigQualifies(configs, i) {
configs[i].Port = "443"
}
}
// only get certs for configs that bind to an address we're already listening on
groupings, err := arrangeBindings(configs)
if err != nil {
return errors.New("arranging bindings: " + err.Error())
}
var configsToSetup []server.Config
serversMu.Lock()
GroupLoop:
for _, group := range groupings {
for _, server := range servers {
if server.Addr == group.BindAddr.String() {
configsToSetup = append(configsToSetup, group.Configs...)
continue GroupLoop
}
}
}
serversMu.Unlock()
// obtain certs for eligible configs; letsencrypt pkg will filter out the rest.
configs, err = letsencrypt.ObtainCertsAndConfigure(configsToSetup, letsencrypt.AlternatePort)
if err != nil {
return errors.New("obtaining certs: " + err.Error())
}
return nil
}
...@@ -14,6 +14,7 @@ import ( ...@@ -14,6 +14,7 @@ import (
"github.com/mholt/caddy/caddy" "github.com/mholt/caddy/caddy"
"github.com/mholt/caddy/caddy/letsencrypt" "github.com/mholt/caddy/caddy/letsencrypt"
"github.com/xenolf/lego/acme"
) )
var ( var (
...@@ -53,6 +54,7 @@ func main() { ...@@ -53,6 +54,7 @@ func main() {
caddy.AppName = appName caddy.AppName = appName
caddy.AppVersion = appVersion caddy.AppVersion = appVersion
acme.UserAgent = appName + "/" + appVersion
// set up process log before anything bad happens // set up process log before anything bad happens
switch logfile { switch logfile {
......
...@@ -33,6 +33,7 @@ type Server struct { ...@@ -33,6 +33,7 @@ type Server struct {
httpWg sync.WaitGroup // used to wait on outstanding connections httpWg sync.WaitGroup // used to wait on outstanding connections
startChan chan struct{} // used to block until server is finished starting startChan chan struct{} // used to block until server is finished starting
connTimeout time.Duration // the maximum duration of a graceful shutdown connTimeout time.Duration // the maximum duration of a graceful shutdown
ReqCallback OptionalCallback // if non-nil, is executed at the beginning of every request
} }
// ListenerFile represents a listener. // ListenerFile represents a listener.
...@@ -41,6 +42,11 @@ type ListenerFile interface { ...@@ -41,6 +42,11 @@ type ListenerFile interface {
File() (*os.File, error) File() (*os.File, error)
} }
// OptionalCallback is a function that may or may not handle a request.
// It returns whether or not it handled the request. If it handled the
// request, it is presumed that no further request handling should occur.
type OptionalCallback func(http.ResponseWriter, *http.Request) bool
// New creates a new Server which will bind to addr and serve // New creates a new Server which will bind to addr and serve
// the sites/hosts configured in configs. Its listener will // the sites/hosts configured in configs. Its listener will
// gracefully close when the server is stopped which will take // gracefully close when the server is stopped which will take
...@@ -309,6 +315,13 @@ func (s *Server) ServeHTTP(w http.ResponseWriter, r *http.Request) { ...@@ -309,6 +315,13 @@ func (s *Server) ServeHTTP(w http.ResponseWriter, r *http.Request) {
} }
}() }()
w.Header().Set("Server", "Caddy")
// Execute the optional request callback if it exists
if s.ReqCallback != nil && s.ReqCallback(w, r) {
return
}
host, _, err := net.SplitHostPort(r.Host) host, _, err := net.SplitHostPort(r.Host)
if err != nil { if err != nil {
host = r.Host // oh well host = r.Host // oh well
...@@ -324,8 +337,6 @@ func (s *Server) ServeHTTP(w http.ResponseWriter, r *http.Request) { ...@@ -324,8 +337,6 @@ func (s *Server) ServeHTTP(w http.ResponseWriter, r *http.Request) {
} }
if vh, ok := s.vhosts[host]; ok { if vh, ok := s.vhosts[host]; ok {
w.Header().Set("Server", "Caddy")
status, _ := vh.stack.ServeHTTP(w, r) status, _ := vh.stack.ServeHTTP(w, r)
// Fallback error response in case error handling wasn't chained in // Fallback error response in case error handling wasn't chained in
......
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