mirror of
https://github.com/caddyserver/caddy.git
synced 2025-01-07 11:28:48 +03:00
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 30c949085c
. 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".
This commit is contained in:
parent
829a0f34d0
commit
55601d3ec2
9 changed files with 284 additions and 246 deletions
|
@ -190,7 +190,8 @@ func startServers(groupings bindingGroup) error {
|
|||
if err != nil {
|
||||
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
|
||||
if IsRestart() {
|
||||
|
|
|
@ -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.
|
||||
func rsaPrivateKeysSame(a, b *rsa.PrivateKey) bool {
|
||||
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
|
|||
|
||||
import (
|
||||
"crypto/tls"
|
||||
"log"
|
||||
"net"
|
||||
"net/http"
|
||||
"net/http/httputil"
|
||||
"net/url"
|
||||
"strings"
|
||||
|
||||
"github.com/mholt/caddy/middleware"
|
||||
)
|
||||
|
||||
const challengeBasePath = "/.well-known/acme-challenge"
|
||||
|
||||
// Handler is a Caddy middleware that can proxy ACME challenge
|
||||
// requests to the real ACME client endpoint. This is necessary
|
||||
// to renew certificates while the server is running.
|
||||
type Handler struct {
|
||||
Next middleware.Handler
|
||||
//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?
|
||||
// RequestCallback proxies challenge requests to ACME client if the
|
||||
// request path starts with challengeBasePath. It returns true if it
|
||||
// handled the request and no more needs to be done; it returns false
|
||||
// if this call was a no-op and the request still needs handling.
|
||||
func RequestCallback(w http.ResponseWriter, r *http.Request) bool {
|
||||
if strings.HasPrefix(r.URL.Path, challengeBasePath) {
|
||||
scheme := "http"
|
||||
if r.TLS != nil {
|
||||
|
@ -37,9 +28,11 @@ func (h *Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) (int, error)
|
|||
hostname = r.URL.Host
|
||||
}
|
||||
|
||||
upstream, err := url.Parse(scheme + "://" + hostname + ":" + alternatePort)
|
||||
upstream, err := url.Parse(scheme + "://" + hostname + ":" + AlternatePort)
|
||||
if err != nil {
|
||||
return http.StatusInternalServerError, err
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
log.Printf("[ERROR] letsencrypt handler: %v", err)
|
||||
return true
|
||||
}
|
||||
|
||||
proxy := httputil.NewSingleHostReverseProxy(upstream)
|
||||
|
@ -48,8 +41,8 @@ func (h *Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) (int, error)
|
|||
}
|
||||
proxy.ServeHTTP(w, r)
|
||||
|
||||
return 0, nil
|
||||
return true
|
||||
}
|
||||
|
||||
return h.Next.ServeHTTP(w, r)
|
||||
return false
|
||||
}
|
||||
|
|
|
@ -7,11 +7,14 @@ import (
|
|||
"encoding/json"
|
||||
"errors"
|
||||
"io/ioutil"
|
||||
"net"
|
||||
"net/http"
|
||||
"os"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"golang.org/x/crypto/ocsp"
|
||||
|
||||
"github.com/mholt/caddy/caddy/setup"
|
||||
"github.com/mholt/caddy/middleware"
|
||||
"github.com/mholt/caddy/middleware/redirect"
|
||||
|
@ -19,6 +22,91 @@ import (
|
|||
"github.com/xenolf/lego/acme"
|
||||
)
|
||||
|
||||
func configureExisting(configs []server.Config) []server.Config {
|
||||
// Identify and configure any eligible hosts for which
|
||||
// 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
|
||||
for i := 0; i < configLen; i++ {
|
||||
if existingCertAndKey(configs[i].Host) && ConfigQualifies(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
|
||||
// for TLS management are included. We group by email so that we
|
||||
// can request certificates in batches with the same client.
|
||||
// Note: The return value is a map, and iteration over a map is
|
||||
// not ordered. I don't think it will be a problem, but if an
|
||||
// ordering problem arises, look at this carefully.
|
||||
groupedConfigs, err := groupConfigsByEmail(configs)
|
||||
if err != nil {
|
||||
return configs, err
|
||||
}
|
||||
|
||||
// obtain certificates for configs that need one, and reconfigure each
|
||||
// config to use the certificates
|
||||
for leEmail, cfgIndexes := range groupedConfigs {
|
||||
// make client to service this email address with CA server
|
||||
client, err := newClientPort(leEmail, optPort)
|
||||
if err != nil {
|
||||
return configs, errors.New("error creating client: " + err.Error())
|
||||
}
|
||||
|
||||
// let's get free, trusted SSL certificates!
|
||||
for _, idx := range cfgIndexes {
|
||||
hostname := configs[idx].Host
|
||||
|
||||
Obtain:
|
||||
certificate, failures := client.ObtainCertificate([]string{hostname}, true)
|
||||
if len(failures) == 0 {
|
||||
// Success - immediately save the certificate resource
|
||||
err := saveCertResource(certificate)
|
||||
if err != nil {
|
||||
return configs, errors.New("error saving assets for " + hostname + ": " + err.Error())
|
||||
}
|
||||
} 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
|
||||
errMsg += "[" + errDomain + "] failed to get certificate: " + obtainErr.Error() + "\n"
|
||||
}
|
||||
}
|
||||
|
||||
return configs, errors.New(errMsg)
|
||||
}
|
||||
}
|
||||
|
||||
// it all comes down to this: turning on TLS with all the new certs
|
||||
for _, idx := range cfgIndexes {
|
||||
configs = autoConfigure(configs, idx)
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
|
@ -43,106 +131,24 @@ import (
|
|||
// 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 statuses from any previous activations
|
||||
ocspStatus = make(map[*[]byte]int)
|
||||
// reset cached ocsp from any previous activations
|
||||
ocspCache = make(map[*[]byte]*ocsp.Response)
|
||||
|
||||
// Identify and configure any eligible hosts for which
|
||||
// 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
|
||||
for i := 0; i < configLen; i++ {
|
||||
if existingCertAndKey(configs[i].Host) && configQualifies(configs, i) {
|
||||
configs = autoConfigure(configs, i)
|
||||
}
|
||||
}
|
||||
// configure configs for which we have an existing certificate
|
||||
configs = configureExisting(configs)
|
||||
|
||||
// Group configs by email address; only configs that are eligible
|
||||
// for TLS management are included. We group by email so that we
|
||||
// can request certificates in batches with the same client.
|
||||
// Note: The return value is a map, and iteration over a map is
|
||||
// not ordered. I don't think it will be a problem, but if an
|
||||
// ordering problem arises, look at this carefully.
|
||||
groupedConfigs, err := groupConfigsByEmail(configs)
|
||||
// obtain certificates for configs which need one, and make them use them
|
||||
configs, err = ObtainCertsAndConfigure(configs, "")
|
||||
if err != nil {
|
||||
return configs, err
|
||||
}
|
||||
|
||||
// obtain certificates for configs that need one, and reconfigure each
|
||||
// config to use the certificates
|
||||
for leEmail, cfgIndexes := range groupedConfigs {
|
||||
// make client to service this email address with CA server
|
||||
client, err := newClient(leEmail)
|
||||
if err != nil {
|
||||
return configs, errors.New("error creating client: " + err.Error())
|
||||
}
|
||||
|
||||
// little bit of housekeeping; gather the hostnames into a slice
|
||||
hosts := make([]string, len(cfgIndexes))
|
||||
for i, idx := range cfgIndexes {
|
||||
hosts[i] = configs[idx].Host
|
||||
}
|
||||
|
||||
// client is ready, so let's get free, trusted SSL certificates!
|
||||
Obtain:
|
||||
certificates, failures := client.ObtainCertificates(hosts, true)
|
||||
if len(failures) > 0 {
|
||||
// Build an error string to return, using all the failures in the list.
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
// 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"
|
||||
}
|
||||
|
||||
// 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)
|
||||
}
|
||||
|
||||
// ... 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
|
||||
for _, idx := range cfgIndexes {
|
||||
configs = autoConfigure(configs, idx)
|
||||
}
|
||||
}
|
||||
|
||||
// renew all certificates that need renewal
|
||||
// renew all relevant certificates that need renewal; TODO: handle errors
|
||||
renewCertificates(configs, false)
|
||||
|
||||
// keep certificates renewed and OCSP stapling updated
|
||||
|
@ -166,16 +172,17 @@ func Deactivate() (err error) {
|
|||
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
|
||||
// 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]
|
||||
return cfg.TLS.Certificate == "" && // user could provide their own cert and key
|
||||
cfg.TLS.Key == "" &&
|
||||
|
||||
// user can force-disable automatic HTTPS for this host
|
||||
cfg.Port != "http" &&
|
||||
cfg.Scheme != "http" &&
|
||||
cfg.Port != "80" &&
|
||||
cfg.TLS.LetsEncryptEmail != "off" &&
|
||||
|
||||
// obviously we get can't certs for loopback or internal hosts
|
||||
|
@ -193,13 +200,11 @@ func configQualifies(allConfigs []server.Config, cfgIndex int) bool {
|
|||
func HostQualifies(hostname string) bool {
|
||||
return hostname != "localhost" &&
|
||||
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 != "::" && // after parsing
|
||||
hostname != "[::1]" && // before 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
|
||||
hostname != "::1" // after parsing
|
||||
}
|
||||
|
||||
// groupConfigsByEmail groups configs by user email address. The returned map is
|
||||
|
@ -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
|
||||
// bother the user for an email address unnecessarily and
|
||||
// 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
|
||||
}
|
||||
leEmail := getEmail(configs[i])
|
||||
|
@ -258,10 +263,13 @@ func newClientPort(leEmail, port string) (*acme.Client, error) {
|
|||
}
|
||||
|
||||
// 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 {
|
||||
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
|
||||
// and agree to terms
|
||||
|
@ -295,48 +303,37 @@ func newClientPort(leEmail, port string) (*acme.Client, error) {
|
|||
return client, nil
|
||||
}
|
||||
|
||||
// obtainCertificates obtains certificates from the CA server for
|
||||
// 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
|
||||
// saveCertResource saves the certificate resource to disk. This
|
||||
// includes the certificate file itself, the private key, and the
|
||||
// metadata file.
|
||||
func saveCertsAndKeys(certificates []acme.CertificateResource) error {
|
||||
for _, cert := range certificates {
|
||||
err := os.MkdirAll(storage.Site(cert.Domain), 0700)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Save cert
|
||||
err = ioutil.WriteFile(storage.SiteCertFile(cert.Domain), cert.Certificate, 0600)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Save private key
|
||||
err = ioutil.WriteFile(storage.SiteKeyFile(cert.Domain), cert.PrivateKey, 0600)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Save cert metadata
|
||||
jsonBytes, err := json.MarshalIndent(&cert, "", "\t")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
err = ioutil.WriteFile(storage.SiteMetaFile(cert.Domain), jsonBytes, 0600)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
func saveCertResource(cert acme.CertificateResource) error {
|
||||
err := os.MkdirAll(storage.Site(cert.Domain), 0700)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Save cert
|
||||
err = ioutil.WriteFile(storage.SiteCertFile(cert.Domain), cert.Certificate, 0600)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Save private key
|
||||
err = ioutil.WriteFile(storage.SiteKeyFile(cert.Domain), cert.PrivateKey, 0600)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Save cert metadata
|
||||
jsonBytes, err := json.MarshalIndent(&cert, "", "\t")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
err = ioutil.WriteFile(storage.SiteMetaFile(cert.Domain), jsonBytes, 0600)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
|
@ -351,52 +348,28 @@ func autoConfigure(allConfigs []server.Config, cfgIndex int) []server.Config {
|
|||
bundleBytes, err := ioutil.ReadFile(storage.SiteCertFile(cfg.Host))
|
||||
// TODO: Handle these errors better
|
||||
if err == nil {
|
||||
ocsp, status, err := acme.GetOCSPForCert(bundleBytes)
|
||||
ocspStatus[&bundleBytes] = status
|
||||
if err == nil && status == acme.OCSPGood {
|
||||
cfg.TLS.OCSPStaple = ocsp
|
||||
ocspBytes, ocspResp, err := acme.GetOCSPForCert(bundleBytes)
|
||||
ocspCache[&bundleBytes] = ocspResp
|
||||
if err == nil && ocspResp.Status == ocsp.Good {
|
||||
cfg.TLS.OCSPStaple = ocspBytes
|
||||
}
|
||||
}
|
||||
cfg.TLS.Certificate = storage.SiteCertFile(cfg.Host)
|
||||
cfg.TLS.Key = storage.SiteKeyFile(cfg.Host)
|
||||
cfg.TLS.Enabled = true
|
||||
// Ensure all defaults are set for the TLS config
|
||||
setup.SetDefaultTLSParams(cfg)
|
||||
|
||||
if cfg.Port == "" {
|
||||
cfg.Port = "https"
|
||||
cfg.Port = "443"
|
||||
}
|
||||
|
||||
// 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.
|
||||
// Also, the port 80 variant of this config is necessary for proxying challenge requests.
|
||||
if !otherHostHasScheme(allConfigs, cfgIndex, "http") &&
|
||||
cfg.Port != "80" && cfg.Port != "http" { // (would not be http port with current program flow, but just in case)
|
||||
if !otherHostHasScheme(allConfigs, cfgIndex, "http") && cfg.Port != "80" && cfg.Scheme != "http" {
|
||||
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
|
||||
}
|
||||
|
||||
|
@ -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,
|
||||
// only if there's a different one with it.
|
||||
func otherHostHasScheme(allConfigs []server.Config, cfgIndex int, scheme string) bool {
|
||||
if scheme == "80" {
|
||||
scheme = "http"
|
||||
} else if scheme == "443" {
|
||||
scheme = "https"
|
||||
if scheme == "http" {
|
||||
scheme = "80"
|
||||
} else if scheme == "https" {
|
||||
scheme = "443"
|
||||
}
|
||||
for i, otherCfg := range allConfigs {
|
||||
if i == cfgIndex {
|
||||
continue // has to be a config OTHER than the one we're comparing against
|
||||
}
|
||||
if otherCfg.Host == allConfigs[cfgIndex].Host {
|
||||
if (otherCfg.Port == scheme) ||
|
||||
(scheme == "https" && otherCfg.Port == "443") ||
|
||||
(scheme == "http" && otherCfg.Port == "80") {
|
||||
return true
|
||||
}
|
||||
if otherCfg.Host == allConfigs[cfgIndex].Host && otherCfg.Port == scheme {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
|
@ -432,7 +401,7 @@ func otherHostHasScheme(allConfigs []server.Config, cfgIndex int, scheme string)
|
|||
// to listen on the "http" port (port 80).
|
||||
func redirPlaintextHost(cfg server.Config) server.Config {
|
||||
toURL := "https://" + cfg.Host
|
||||
if cfg.Port != "https" && cfg.Port != "http" {
|
||||
if cfg.Port != "443" && cfg.Port != "80" {
|
||||
toURL += ":" + cfg.Port
|
||||
}
|
||||
|
||||
|
@ -449,7 +418,7 @@ func redirPlaintextHost(cfg server.Config) server.Config {
|
|||
|
||||
return server.Config{
|
||||
Host: cfg.Host,
|
||||
Port: "http",
|
||||
Port: "80",
|
||||
Middleware: map[string][]middleware.Middleware{
|
||||
"/": []middleware.Middleware{redirMidware},
|
||||
},
|
||||
|
@ -504,17 +473,17 @@ var (
|
|||
|
||||
// Some essential values related to the Let's Encrypt process
|
||||
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
|
||||
// is used instead of the default port (80 or 443), then the
|
||||
// default port for the challenge must be forwarded to this one.
|
||||
alternatePort = "5033"
|
||||
AlternatePort = "5033"
|
||||
|
||||
// How often to check certificates for renewal.
|
||||
renewInterval = 24 * time.Hour
|
||||
// RenewInterval is how often to check certificates for renewal.
|
||||
RenewInterval = 24 * time.Hour
|
||||
|
||||
// How often to update OCSP stapling.
|
||||
ocspInterval = 1 * time.Hour
|
||||
// OCSPInterval is how often to check if OCSP stapling needs updating.
|
||||
OCSPInterval = 1 * time.Hour
|
||||
)
|
||||
|
||||
// KeySize represents the length of a key in bits.
|
||||
|
@ -522,22 +491,22 @@ type KeySize int
|
|||
|
||||
// Key sizes are used to determine the strength of a key.
|
||||
const (
|
||||
ECC_224 KeySize = 224
|
||||
ECC_256 = 256
|
||||
RSA_2048 = 2048
|
||||
RSA_4096 = 4096
|
||||
Ecc224 KeySize = 224
|
||||
Ecc256 = 256
|
||||
Rsa2048 = 2048
|
||||
Rsa4096 = 4096
|
||||
)
|
||||
|
||||
// rsaKeySizeToUse is the size to use for new RSA keys.
|
||||
// This shouldn't need to change except for in tests;
|
||||
// the size can be drastically reduced for speed.
|
||||
var rsaKeySizeToUse = RSA_2048
|
||||
var rsaKeySizeToUse = Rsa2048
|
||||
|
||||
// stopChan is used to signal the maintenance goroutine
|
||||
// to terminate.
|
||||
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
|
||||
// status has changed.
|
||||
var ocspStatus = make(map[*[]byte]int)
|
||||
// response needs to be updated.
|
||||
var ocspCache = make(map[*[]byte]*ocsp.Response)
|
||||
|
|
|
@ -23,9 +23,11 @@ func TestHostQualifies(t *testing.T) {
|
|||
{"", false},
|
||||
{" ", false},
|
||||
{"0.0.0.0", false},
|
||||
{"192.168.1.3", true},
|
||||
{"10.0.2.1", true},
|
||||
{"192.168.1.3", false},
|
||||
{"10.0.2.1", false},
|
||||
{"169.112.53.4", false},
|
||||
{"foobar.com", true},
|
||||
{"sub.foobar.com", true},
|
||||
} {
|
||||
if HostQualifies(test.host) && !test.expect {
|
||||
t.Errorf("Test %d: Expected '%s' to NOT qualify, but it did", i, test.host)
|
||||
|
@ -39,14 +41,14 @@ func TestHostQualifies(t *testing.T) {
|
|||
func TestRedirPlaintextHost(t *testing.T) {
|
||||
cfg := redirPlaintextHost(server.Config{
|
||||
Host: "example.com",
|
||||
Port: "http",
|
||||
Port: "80",
|
||||
})
|
||||
|
||||
// Check host and port
|
||||
if actual, expected := cfg.Host, "example.com"; actual != expected {
|
||||
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)
|
||||
}
|
||||
|
||||
|
|
|
@ -27,8 +27,8 @@ var OnChange func() error
|
|||
// which you'll close when maintenance should stop, to allow this
|
||||
// goroutine to clean up after itself and unblock.
|
||||
func maintainAssets(configs []server.Config, stopChan chan struct{}) {
|
||||
renewalTicker := time.NewTicker(renewInterval)
|
||||
ocspTicker := time.NewTicker(ocspInterval)
|
||||
renewalTicker := time.NewTicker(RenewInterval)
|
||||
ocspTicker := time.NewTicker(OCSPInterval)
|
||||
|
||||
for {
|
||||
select {
|
||||
|
@ -47,15 +47,25 @@ func maintainAssets(configs []server.Config, stopChan chan struct{}) {
|
|||
}
|
||||
}
|
||||
case <-ocspTicker.C:
|
||||
for bundle, oldStatus := range ocspStatus {
|
||||
_, newStatus, err := acme.GetOCSPForCert(*bundle)
|
||||
if err == nil && newStatus != oldStatus && OnChange != nil {
|
||||
log.Printf("[INFO] OCSP status changed from %v to %v", oldStatus, newStatus)
|
||||
err := OnChange()
|
||||
for bundle, oldResp := range ocspCache {
|
||||
// start checking OCSP staple about halfway through validity period for good measure
|
||||
refreshTime := oldResp.ThisUpdate.Add(oldResp.NextUpdate.Sub(oldResp.ThisUpdate) / 10)
|
||||
if time.Now().After(refreshTime) {
|
||||
_, newResp, err := acme.GetOCSPForCert(*bundle)
|
||||
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:
|
||||
|
@ -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)
|
||||
var client *acme.Client
|
||||
if useCustomPort {
|
||||
client, err = newClientPort("", alternatePort) // email not used for renewal
|
||||
client, err = newClientPort("", AlternatePort) // email not used for renewal
|
||||
} else {
|
||||
client, err = newClient("")
|
||||
}
|
||||
|
@ -134,7 +144,7 @@ func renewCertificates(configs []server.Config, useCustomPort bool) (int, []erro
|
|||
|
||||
// Renew certificate
|
||||
Renew:
|
||||
newCertMeta, err := client.RenewCertificate(certMeta, true, true)
|
||||
newCertMeta, err := client.RenewCertificate(certMeta, true)
|
||||
if err != nil {
|
||||
if _, ok := err.(acme.TOSError); ok {
|
||||
err := client.AgreeToTOS()
|
||||
|
@ -145,24 +155,20 @@ func renewCertificates(configs []server.Config, useCustomPort bool) (int, []erro
|
|||
}
|
||||
|
||||
time.Sleep(10 * time.Second)
|
||||
newCertMeta, err = client.RenewCertificate(certMeta, true, true)
|
||||
newCertMeta, err = client.RenewCertificate(certMeta, true)
|
||||
if err != nil {
|
||||
errs = append(errs, err)
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
saveCertsAndKeys([]acme.CertificateResource{newCertMeta})
|
||||
saveCertResource(newCertMeta)
|
||||
n++
|
||||
} else if daysLeft <= 30 {
|
||||
// Warn on 30 days remaining. TODO: Just do this once...
|
||||
} else if daysLeft <= 21 {
|
||||
// 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)
|
||||
}
|
||||
}
|
||||
|
||||
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 @@
|
|||
package caddy
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/gob"
|
||||
"errors"
|
||||
"io/ioutil"
|
||||
"log"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path"
|
||||
|
||||
"github.com/mholt/caddy/caddy/letsencrypt"
|
||||
"github.com/mholt/caddy/server"
|
||||
)
|
||||
|
||||
func init() {
|
||||
|
@ -33,6 +39,12 @@ func Restart(newCaddyfile Input) error {
|
|||
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...
|
||||
os.Args = []string{""}
|
||||
}
|
||||
|
@ -61,7 +73,7 @@ func Restart(newCaddyfile Input) error {
|
|||
|
||||
// Pass along relevant file descriptors to child process; ordering
|
||||
// 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
|
||||
serversMu.Lock()
|
||||
|
@ -110,3 +122,45 @@ func Restart(newCaddyfile Input) error {
|
|||
// Looks like child is successful; we can exit gracefully.
|
||||
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
|
||||
}
|
||||
|
|
2
main.go
2
main.go
|
@ -14,6 +14,7 @@ import (
|
|||
|
||||
"github.com/mholt/caddy/caddy"
|
||||
"github.com/mholt/caddy/caddy/letsencrypt"
|
||||
"github.com/xenolf/lego/acme"
|
||||
)
|
||||
|
||||
var (
|
||||
|
@ -53,6 +54,7 @@ func main() {
|
|||
|
||||
caddy.AppName = appName
|
||||
caddy.AppVersion = appVersion
|
||||
acme.UserAgent = appName + "/" + appVersion
|
||||
|
||||
// set up process log before anything bad happens
|
||||
switch logfile {
|
||||
|
|
|
@ -33,6 +33,7 @@ type Server struct {
|
|||
httpWg sync.WaitGroup // used to wait on outstanding connections
|
||||
startChan chan struct{} // used to block until server is finished starting
|
||||
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.
|
||||
|
@ -41,6 +42,11 @@ type ListenerFile interface {
|
|||
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
|
||||
// the sites/hosts configured in configs. Its listener will
|
||||
// gracefully close when the server is stopped which will take
|
||||
|
@ -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)
|
||||
if err != nil {
|
||||
host = r.Host // oh well
|
||||
|
@ -324,8 +337,6 @@ func (s *Server) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
|||
}
|
||||
|
||||
if vh, ok := s.vhosts[host]; ok {
|
||||
w.Header().Set("Server", "Caddy")
|
||||
|
||||
status, _ := vh.stack.ServeHTTP(w, r)
|
||||
|
||||
// Fallback error response in case error handling wasn't chained in
|
||||
|
|
Loading…
Reference in a new issue