Commit e99b3af0 authored by Matthew Holt's avatar Matthew Holt

letsencrypt: Numerous bug fixes

parent 88c646c8
...@@ -276,7 +276,7 @@ func Wait() { ...@@ -276,7 +276,7 @@ func Wait() {
// the Caddyfile. If loader does not return a Caddyfile, the // the Caddyfile. If loader does not return a Caddyfile, the
// default one will be returned. Thus, if there are no other // default one will be returned. Thus, if there are no other
// errors, this function always returns at least the default // errors, this function always returns at least the default
// Caddyfile. // Caddyfile (not the previously-used Caddyfile).
func LoadCaddyfile(loader func() (Input, error)) (cdyfile Input, err error) { func LoadCaddyfile(loader func() (Input, error)) (cdyfile Input, err error) {
// If we are a fork, finishing the restart is highest priority; // If we are a fork, finishing the restart is highest priority;
// piped input is required in this case. // piped input is required in this case.
......
...@@ -62,7 +62,7 @@ baz" ...@@ -62,7 +62,7 @@ baz"
{ // 8 { // 8
caddyfile: `http://host, https://host { caddyfile: `http://host, https://host {
}`, }`,
json: `[{"hosts":["host:http","host:https"],"body":{}}]`, // hosts in JSON are always host:port format (if port is specified) json: `[{"hosts":["host:http","host:https"],"body":{}}]`, // hosts in JSON are always host:port format (if port is specified), for consistency
}, },
} }
......
...@@ -89,10 +89,6 @@ func load(filename string, input io.Reader) ([]server.Config, error) { ...@@ -89,10 +89,6 @@ func load(filename string, input io.Reader) ([]server.Config, error) {
} }
} }
if config.Port == "" {
config.Port = Port
}
configs = append(configs, config) configs = append(configs, config)
} }
} }
...@@ -145,6 +141,11 @@ func arrangeBindings(allConfigs []server.Config) (Group, error) { ...@@ -145,6 +141,11 @@ func arrangeBindings(allConfigs []server.Config) (Group, error) {
// Group configs by bind address // Group configs by bind address
for _, conf := range allConfigs { for _, conf := range allConfigs {
// use default port if none is specified
if conf.Port == "" {
conf.Port = Port
}
bindAddr, warnErr, fatalErr := resolveAddr(conf) bindAddr, warnErr, fatalErr := resolveAddr(conf)
if fatalErr != nil { if fatalErr != nil {
return groupings, fatalErr return groupings, fatalErr
......
...@@ -39,32 +39,35 @@ import ( ...@@ -39,32 +39,35 @@ import (
// some may have been appended, for example, to redirect // some may have been appended, for example, to redirect
// plaintext HTTP requests to their HTTPS counterpart. // plaintext HTTP requests to their HTTPS counterpart.
func Activate(configs []server.Config) ([]server.Config, error) { func Activate(configs []server.Config) ([]server.Config, error) {
// just in case previous caller forgot...
Deactivate()
// TODO: Is multiple activation (before a deactivation) an error? // TODO: Is multiple activation (before a deactivation) an error?
// 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) && configs[i].TLS.LetsEncryptEmail != "off" { if existingCertAndKey(configs[i].Host) && configQualifies(configs[i], configs) {
configs = autoConfigure(&configs[i], configs) configs = autoConfigure(&configs[i], configs)
} }
} }
// Filter the configs by what we can maintain automatically // Group configs by email address; only configs that are eligible
filteredConfigs := filterConfigs(configs) // for TLS management are included. We group by email so that we
// can request certificates in batches with the same client.
// Renew any existing certificates that need renewal // Note: The return value is a map, and iteration over a map is
renewCertificates(filteredConfigs) // not ordered. I don't think it will be a problem, but if an
// ordering problem arises, look at this carefully.
// Group configs by LE email address; this will help us groupedConfigs, err := groupConfigsByEmail(configs)
// reduce round-trips when getting the certs.
groupedConfigs, err := groupConfigsByEmail(filteredConfigs)
if err != nil { if err != nil {
return configs, err return configs, err
} }
// Loop through each email address and obtain certs; this way, we can obtain more // obtain certificates for configs that need one, and reconfigure each
// than one certificate per email address, and still save them individually. // config to use the certificates
for leEmail, serverConfigs := range groupedConfigs { for leEmail, serverConfigs := 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 := newClient(leEmail)
...@@ -75,7 +78,7 @@ func Activate(configs []server.Config) ([]server.Config, error) { ...@@ -75,7 +78,7 @@ func Activate(configs []server.Config) ([]server.Config, error) {
// client is ready, so let's get free, trusted SSL certificates! yeah! // client is ready, so let's get free, trusted SSL certificates! yeah!
certificates, err := obtainCertificates(client, serverConfigs) certificates, err := obtainCertificates(client, serverConfigs)
if err != nil { if err != nil {
return configs, errors.New("error obtaining cert: " + err.Error()) return configs, errors.New("error getting certs: " + err.Error())
} }
// ... that's it. save the certs, keys, and metadata files to disk // ... that's it. save the certs, keys, and metadata files to disk
...@@ -84,15 +87,17 @@ func Activate(configs []server.Config) ([]server.Config, error) { ...@@ -84,15 +87,17 @@ func Activate(configs []server.Config) ([]server.Config, error) {
return configs, errors.New("error saving assets: " + err.Error()) return configs, errors.New("error saving assets: " + err.Error())
} }
// it all comes down to this: turning TLS on for all the configs // it all comes down to this: turning on TLS with all the new certs
for _, cfg := range serverConfigs { for i := 0; i < len(serverConfigs); i++ {
configs = autoConfigure(cfg, configs) configs = autoConfigure(serverConfigs[i], configs)
} }
} }
Deactivate() // in case previous caller wasn't clean about it // renew all certificates that need renewal
stopChan = make(chan struct{}) renewCertificates(configs)
go maintainAssets(filteredConfigs, stopChan)
// keep certificates renewed and OCSP stapling updated
go maintainAssets(configs, stopChan)
return configs, nil return configs, nil
} }
...@@ -108,55 +113,51 @@ func Deactivate() (err error) { ...@@ -108,55 +113,51 @@ func Deactivate() (err error) {
} }
}() }()
close(stopChan) close(stopChan)
stopChan = make(chan struct{})
return return
} }
// filterConfigs filters and returns configs that are eligible for automatic // configQualifies returns true if cfg qualifes for automatic LE activation,
// TLS by skipping configs that do not qualify for automatic maintenance // but it does require the list of all configs to be passed in as well.
// of assets. Configurations with a manual TLS configuration or that already // It does NOT check to see if a cert and key already exist for cfg.
// have an HTTPS counterpart host defined will be skipped. func configQualifies(cfg server.Config, allConfigs []server.Config) bool {
func filterConfigs(configs []server.Config) []server.Config { return cfg.TLS.Certificate == "" && // user could provide their own cert and key
var filtered []server.Config cfg.TLS.Key == "" &&
// configQualifies returns true if cfg qualifes for automatic LE activation // user can force-disable automatic HTTPS for this host
configQualifies := func(cfg server.Config) bool { cfg.Port != "http" &&
return cfg.TLS.Certificate == "" && // user could provide their own cert and key cfg.TLS.LetsEncryptEmail != "off" &&
cfg.TLS.Key == "" &&
// obviously we get can't certs for loopback or internal hosts
// user can force-disable automatic HTTPS for this host cfg.Host != "localhost" &&
cfg.Port != "http" && cfg.Host != "" &&
cfg.TLS.LetsEncryptEmail != "off" && cfg.Host != "0.0.0.0" &&
cfg.Host != "::1" &&
// obviously we get can't certs for loopback or internal hosts !strings.HasPrefix(cfg.Host, "127.") &&
cfg.Host != "localhost" && // TODO: Also exclude 10.* and 192.168.* addresses?
cfg.Host != "" &&
cfg.Host != "0.0.0.0" && // make sure an HTTPS version of this config doesn't exist in the list already
cfg.Host != "::1" && !hostHasOtherScheme(cfg.Host, "https", allConfigs)
!strings.HasPrefix(cfg.Host, "127.") &&
!strings.HasPrefix(cfg.Host, "10.") &&
// make sure an HTTPS version of this config doesn't exist in the list already
!hostHasOtherScheme(cfg.Host, "https", configs)
}
for _, cfg := range configs {
if configQualifies(cfg) {
filtered = append(filtered, cfg)
}
}
return filtered
} }
// groupConfigsByEmail groups configs by user email address. The returned map is // groupConfigsByEmail groups configs by user email address. The returned map is
// a map of email address to the configs that are serviced under that account. // a map of email address to the configs that are serviced under that account.
// If an email address is not available, the user will be prompted to provide one. // If an email address is not available for an eligible config, the user will be
// This function assumes that all configs passed in qualify for automatic management. // prompted to provide one. The returned map contains pointers to the original
// server config values.
func groupConfigsByEmail(configs []server.Config) (map[string][]*server.Config, error) { func groupConfigsByEmail(configs []server.Config) (map[string][]*server.Config, error) {
initMap := make(map[string][]*server.Config) initMap := make(map[string][]*server.Config)
for i := 0; i < len(configs); i++ { for i := 0; i < len(configs); i++ {
// filter out configs that we already have certs for and
// 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], configs) {
continue
}
leEmail := getEmail(configs[i]) leEmail := getEmail(configs[i])
if leEmail == "" { if leEmail == "" {
// TODO: This may not be an error; just a poor choice by the user
return nil, errors.New("must have email address to serve HTTPS without existing certificate and key") return nil, errors.New("must have email address to serve HTTPS without existing certificate and key")
} }
initMap[leEmail] = append(initMap[leEmail], &configs[i]) initMap[leEmail] = append(initMap[leEmail], &configs[i])
...@@ -280,7 +281,8 @@ func saveCertsAndKeys(certificates []acme.CertificateResource) error { ...@@ -280,7 +281,8 @@ func saveCertsAndKeys(certificates []acme.CertificateResource) error {
// autoConfigure enables TLS on cfg and appends, if necessary, a new config // autoConfigure enables TLS on cfg and appends, if necessary, a new config
// to allConfigs that redirects plaintext HTTP to its new HTTPS counterpart. // to allConfigs that redirects plaintext HTTP to its new HTTPS counterpart.
// It expects the certificate and key to already be in storage. It returns // It expects the certificate and key to already be in storage. It returns
// the new list of allConfigs. // the new list of allConfigs, since it may append a new config. This function
// assumes that cfg was already set up for HTTPS.
func autoConfigure(cfg *server.Config, allConfigs []server.Config) []server.Config { func autoConfigure(cfg *server.Config, allConfigs []server.Config) []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
...@@ -294,7 +296,9 @@ func autoConfigure(cfg *server.Config, allConfigs []server.Config) []server.Conf ...@@ -294,7 +296,9 @@ func autoConfigure(cfg *server.Config, allConfigs []server.Config) []server.Conf
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
cfg.Port = "https" if cfg.Port == "" {
cfg.Port = "https"
}
// Set up http->https redirect as long as there isn't already // Set up http->https redirect as long as there isn't already
// a http counterpart in the configs // a http counterpart in the configs
...@@ -308,11 +312,21 @@ func autoConfigure(cfg *server.Config, allConfigs []server.Config) []server.Conf ...@@ -308,11 +312,21 @@ func autoConfigure(cfg *server.Config, allConfigs []server.Config) []server.Conf
// hostHasOtherScheme tells you whether there is another config in the list // hostHasOtherScheme tells you whether there is another config in the list
// for the same host but with the port equal to scheme. For example, to see // for the same host but with the port equal to scheme. For example, to see
// if example.com has a https variant already, pass in example.com and // if example.com has a https variant already, pass in example.com and
// "https" along with the list of configs. // "https" along with the list of configs. This function considers "443"
// and "https" to be the same scheme, as well as "http" and "80".
func hostHasOtherScheme(host, scheme string, allConfigs []server.Config) bool { func hostHasOtherScheme(host, scheme string, allConfigs []server.Config) bool {
if scheme == "80" {
scheme = "http"
} else if scheme == "443" {
scheme = "https"
}
for _, otherCfg := range allConfigs { for _, otherCfg := range allConfigs {
if otherCfg.Host == host && otherCfg.Port == scheme { if otherCfg.Host == host {
return true if (otherCfg.Port == scheme) ||
(scheme == "https" && otherCfg.Port == "443") ||
(scheme == "http" && otherCfg.Port == "80") {
return true
}
} }
} }
return false return false
...@@ -323,12 +337,17 @@ func hostHasOtherScheme(host, scheme string, allConfigs []server.Config) bool { ...@@ -323,12 +337,17 @@ func hostHasOtherScheme(host, scheme string, allConfigs []server.Config) bool {
// be the HTTPS configuration. The returned configuration is set // be the HTTPS configuration. The returned configuration is set
// 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
if cfg.Port != "https" && cfg.Port != "http" {
toUrl += ":" + cfg.Port
}
redirMidware := func(next middleware.Handler) middleware.Handler { redirMidware := func(next middleware.Handler) middleware.Handler {
return redirect.Redirect{Next: next, Rules: []redirect.Rule{ return redirect.Redirect{Next: next, Rules: []redirect.Rule{
{ {
FromScheme: "http", FromScheme: "http",
FromPath: "/", FromPath: "/",
To: "https://" + cfg.Host + "{uri}", To: toUrl + "{uri}",
Code: http.StatusMovedPermanently, Code: http.StatusMovedPermanently,
}, },
}} }}
...@@ -391,13 +410,15 @@ var ( ...@@ -391,13 +410,15 @@ var (
// Some essential values related to the Let's Encrypt process // Some essential values related to the Let's Encrypt process
const ( const (
// The port to expose to the CA server for Simple HTTP Challenge // The port to expose to the CA server for Simple HTTP Challenge.
exposePort = "5001" // NOTE: Let's Encrypt requires port 443. If exposePort is not 443,
// then port 443 must be forwarded to exposePort.
exposePort = "443"
// How often to check certificates for renewal // How often to check certificates for renewal.
renewInterval = 24 * time.Hour renewInterval = 24 * time.Hour
// How often to update OCSP stapling // How often to update OCSP stapling.
ocspInterval = 1 * time.Hour ocspInterval = 1 * time.Hour
) )
......
...@@ -25,7 +25,7 @@ var OnChange func() error ...@@ -25,7 +25,7 @@ var OnChange func() error
// //
// You must pass in the server configs to maintain and the channel // You must pass in the server configs to maintain and the channel
// 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. // 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)
...@@ -66,7 +66,7 @@ func maintainAssets(configs []server.Config, stopChan chan struct{}) { ...@@ -66,7 +66,7 @@ func maintainAssets(configs []server.Config, stopChan chan struct{}) {
// renewCertificates loops through all configured site and // renewCertificates loops through all configured site and
// looks for certificates to renew. Nothing is mutated // looks for certificates to renew. Nothing is mutated
// through this function. The changes happen directly on disk. // through this function; all changes happen directly on disk.
// It returns the number of certificates renewed and any errors // It returns the number of certificates renewed and any errors
// that occurred. It only performs a renewal if necessary. // that occurred. It only performs a renewal if necessary.
func renewCertificates(configs []server.Config) (int, []error) { func renewCertificates(configs []server.Config) (int, []error) {
...@@ -75,7 +75,7 @@ func renewCertificates(configs []server.Config) (int, []error) { ...@@ -75,7 +75,7 @@ func renewCertificates(configs []server.Config) (int, []error) {
var n int var n int
for _, cfg := range configs { for _, cfg := range configs {
// Host must be TLS-enabled and have assets managed by LE // Host must be TLS-enabled and have existing assets managed by LE
if !cfg.TLS.Enabled || !existingCertAndKey(cfg.Host) { if !cfg.TLS.Enabled || !existingCertAndKey(cfg.Host) {
continue continue
} }
...@@ -100,7 +100,7 @@ func renewCertificates(configs []server.Config) (int, []error) { ...@@ -100,7 +100,7 @@ func renewCertificates(configs []server.Config) (int, []error) {
// Renew with a week or less remaining. // Renew with a week or less remaining.
if daysLeft <= 7 { if daysLeft <= 7 {
log.Printf("[INFO] There are %d days left on the certificate of %s. Trying to renew now.", daysLeft, cfg.Host) log.Printf("[INFO] There are %d days left on the certificate of %s. Trying to renew now.", daysLeft, cfg.Host)
client, err := newClient(getEmail(cfg)) client, err := newClient("") // email not used for renewal
if err != nil { if err != nil {
errs = append(errs, err) errs = append(errs, err)
continue continue
......
...@@ -41,7 +41,7 @@ func init() { ...@@ -41,7 +41,7 @@ func init() {
// TODO: Production endpoint is: https://acme-v01.api.letsencrypt.org // TODO: Production endpoint is: https://acme-v01.api.letsencrypt.org
flag.StringVar(&letsencrypt.CAUrl, "ca", "https://acme-staging.api.letsencrypt.org", "Certificate authority ACME server") flag.StringVar(&letsencrypt.CAUrl, "ca", "https://acme-staging.api.letsencrypt.org", "Certificate authority ACME server")
flag.BoolVar(&letsencrypt.Agreed, "agree", false, "Agree to Let's Encrypt Subscriber Agreement") flag.BoolVar(&letsencrypt.Agreed, "agree", false, "Agree to Let's Encrypt Subscriber Agreement")
flag.StringVar(&letsencrypt.DefaultEmail, "email", "", "Default email address to use for Let's Encrypt transactions") flag.StringVar(&letsencrypt.DefaultEmail, "email", "", "Default Let's Encrypt account email address")
flag.StringVar(&revoke, "revoke", "", "Hostname for which to revoke the certificate") flag.StringVar(&revoke, "revoke", "", "Hostname for which to revoke the certificate")
} }
......
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