mirror of
https://github.com/caddyserver/caddy.git
synced 2025-02-03 23:09:57 -05:00
letsencrypt: Numerous bug fixes
This commit is contained in:
parent
88c646c86c
commit
e99b3af0a5
6 changed files with 96 additions and 74 deletions
|
@ -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"
|
||||||
{ // 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) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
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) {
|
||||||
|
|
||||||
// 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 (
|
||||||
// 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) {
|
||||||
// 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) {
|
||||||
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) {
|
||||||
}
|
}
|
||||||
}()
|
}()
|
||||||
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 == "" &&
|
|
||||||
|
|
||||||
// user can force-disable automatic HTTPS for this host
|
// obviously we get can't certs for loopback or internal hosts
|
||||||
cfg.Port != "http" &&
|
cfg.Host != "localhost" &&
|
||||||
cfg.TLS.LetsEncryptEmail != "off" &&
|
cfg.Host != "" &&
|
||||||
|
cfg.Host != "0.0.0.0" &&
|
||||||
|
cfg.Host != "::1" &&
|
||||||
|
!strings.HasPrefix(cfg.Host, "127.") &&
|
||||||
|
// TODO: Also exclude 10.* and 192.168.* addresses?
|
||||||
|
|
||||||
// obviously we get can't certs for loopback or internal hosts
|
// make sure an HTTPS version of this config doesn't exist in the list already
|
||||||
cfg.Host != "localhost" &&
|
!hostHasOtherScheme(cfg.Host, "https", allConfigs)
|
||||||
cfg.Host != "" &&
|
|
||||||
cfg.Host != "0.0.0.0" &&
|
|
||||||
cfg.Host != "::1" &&
|
|
||||||
!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 {
|
||||||
// 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
|
||||||
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
|
||||||
// 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 {
|
||||||
// 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 (
|
||||||
|
|
||||||
// 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
|
||||||
//
|
//
|
||||||
// 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{}) {
|
||||||
|
|
||||||
// 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) {
|
||||||
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) {
|
||||||
// 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
|
||||||
|
|
2
main.go
2
main.go
|
@ -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")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
Loading…
Add table
Reference in a new issue