Commit 9e4a2919 authored by Matt Holt's avatar Matt Holt Committed by GitHub

caddytls: Fix handling of IP-only TLS configs and empty-SNI handshakes (#2452)

* caddytls: Fix empty SNI handling (new -default-sni flag)

vendor: update certmagic, needed to support this

Hopefully fixes #2451, fixes #2438, and fixes #2414

* caddytls: Don't overwrite certmagic Manager (fixes #2407)

Supersedes #2447

* vendor: Update certmagic to fix nil pointer deref and TLS-ALPN cleanup

* Improve -default-sni flag help text
parent fa10b027
...@@ -46,6 +46,7 @@ func init() { ...@@ -46,6 +46,7 @@ func init() {
flag.BoolVar(&certmagic.Agreed, "agree", false, "Agree to the CA's Subscriber Agreement") flag.BoolVar(&certmagic.Agreed, "agree", false, "Agree to the CA's Subscriber Agreement")
flag.StringVar(&certmagic.CA, "ca", certmagic.CA, "URL to certificate authority's ACME server directory") flag.StringVar(&certmagic.CA, "ca", certmagic.CA, "URL to certificate authority's ACME server directory")
flag.StringVar(&certmagic.DefaultServerName, "default-sni", certmagic.DefaultServerName, "If a ClientHello ServerName is empty, use this ServerName to choose a TLS certificate")
flag.BoolVar(&certmagic.DisableHTTPChallenge, "disable-http-challenge", certmagic.DisableHTTPChallenge, "Disable the ACME HTTP challenge") flag.BoolVar(&certmagic.DisableHTTPChallenge, "disable-http-challenge", certmagic.DisableHTTPChallenge, "Disable the ACME HTTP challenge")
flag.BoolVar(&certmagic.DisableTLSALPNChallenge, "disable-tls-alpn-challenge", certmagic.DisableTLSALPNChallenge, "Disable the ACME TLS-ALPN challenge") flag.BoolVar(&certmagic.DisableTLSALPNChallenge, "disable-tls-alpn-challenge", certmagic.DisableTLSALPNChallenge, "Disable the ACME TLS-ALPN challenge")
flag.StringVar(&disabledMetrics, "disabled-metrics", "", "Comma-separated list of telemetry metrics to disable") flag.StringVar(&disabledMetrics, "disabled-metrics", "", "Comma-separated list of telemetry metrics to disable")
...@@ -108,7 +109,7 @@ func Run() { ...@@ -108,7 +109,7 @@ func Run() {
} }
} }
//Load all additional envs as soon as possible // load all additional envs as soon as possible
if err := LoadEnvFromFile(envFile); err != nil { if err := LoadEnvFromFile(envFile); err != nil {
mustLogFatalf("%v", err) mustLogFatalf("%v", err)
} }
......
...@@ -93,7 +93,7 @@ type Config struct { ...@@ -93,7 +93,7 @@ type Config struct {
} }
// NewConfig returns a new Config with a pointer to the instance's // NewConfig returns a new Config with a pointer to the instance's
// certificate cache. You will usually need to set Other fields on // certificate cache. You will usually need to set other fields on
// the returned Config for successful practical use. // the returned Config for successful practical use.
func NewConfig(inst *caddy.Instance) *Config { func NewConfig(inst *caddy.Instance) *Config {
inst.StorageMu.RLock() inst.StorageMu.RLock()
...@@ -257,10 +257,14 @@ func MakeTLSConfig(configs []*Config) (*tls.Config, error) { ...@@ -257,10 +257,14 @@ func MakeTLSConfig(configs []*Config) (*tls.Config, error) {
// configs with the same hostname pattern; should // configs with the same hostname pattern; should
// be OK since we already asserted they are roughly // be OK since we already asserted they are roughly
// the same); during TLS handshakes, configs are // the same); during TLS handshakes, configs are
// loaded based on the hostname pattern, according // loaded based on the hostname pattern according
// to client's SNI // to client's ServerName (SNI) value
if cfg.Hostname == "0.0.0.0" || cfg.Hostname == "::" {
configMap[""] = cfg
} else {
configMap[cfg.Hostname] = cfg configMap[cfg.Hostname] = cfg
} }
}
// Is TLS disabled? By now, we know that all // Is TLS disabled? By now, we know that all
// configs agree whether it is or not, so we // configs agree whether it is or not, so we
......
...@@ -17,6 +17,8 @@ package caddytls ...@@ -17,6 +17,8 @@ package caddytls
import ( import (
"crypto/tls" "crypto/tls"
"fmt" "fmt"
"log"
"net"
"strings" "strings"
"github.com/mholt/caddy/telemetry" "github.com/mholt/caddy/telemetry"
...@@ -38,14 +40,31 @@ type configGroup map[string]*Config ...@@ -38,14 +40,31 @@ type configGroup map[string]*Config
// This function follows nearly the same logic to lookup // This function follows nearly the same logic to lookup
// a hostname as the getCertificate function uses. // a hostname as the getCertificate function uses.
func (cg configGroup) getConfig(hello *tls.ClientHelloInfo) *Config { func (cg configGroup) getConfig(hello *tls.ClientHelloInfo) *Config {
name := certmagic.CertNameFromClientHello(hello) name := certmagic.NormalizedName(hello.ServerName)
if name == "" {
name = certmagic.NormalizedName(certmagic.DefaultServerName)
}
// if SNI is empty, prefer matching IP address (it is
// more specific than a "catch-all" configuration)
if name == "" && hello.Conn != nil {
addr := hello.Conn.LocalAddr().String()
ip, _, err := net.SplitHostPort(addr)
if err == nil {
addr = ip
}
if config, ok := cg[addr]; ok {
return config
}
}
// exact match? great, let's use it // otherwise, try an exact match
if config, ok := cg[name]; ok { if config, ok := cg[name]; ok {
return config return config
} }
// try replacing labels in the name with wildcards until we get a match // then try replacing labels in the name with
// wildcards until we get a match
labels := strings.Split(name, ".") labels := strings.Split(name, ".")
for i := range labels { for i := range labels {
labels[i] = "*" labels[i] = "*"
...@@ -55,14 +74,25 @@ func (cg configGroup) getConfig(hello *tls.ClientHelloInfo) *Config { ...@@ -55,14 +74,25 @@ func (cg configGroup) getConfig(hello *tls.ClientHelloInfo) *Config {
} }
} }
// try a config that serves all names (the above // try a config that matches all names - this
// loop doesn't try empty string; for hosts defined // is needed to match configs defined without
// with only a port, for instance, like ":443") - // a specific host, like ":443", when SNI is
// also known as the default config // a non-empty value
if config, ok := cg[""]; ok { if config, ok := cg[""]; ok {
return config return config
} }
// failover with a random config: this is necessary
// because we might be needing to solve a TLS-ALPN
// ACME challenge for a name that we don't have a
// TLS configuration for; any config will do for
// this purpose
for _, config := range cg {
return config
}
log.Printf("[ERROR] No TLS configuration available for ClientHello with ServerName: %s", hello.ServerName)
return nil return nil
} }
......
...@@ -54,18 +54,6 @@ func setupTLS(c *caddy.Controller) error { ...@@ -54,18 +54,6 @@ func setupTLS(c *caddy.Controller) error {
config.Enabled = true config.Enabled = true
// a single certificate cache is used by the whole caddy.Instance; get a pointer to it
certCache, ok := c.Get(CertCacheInstStorageKey).(*certmagic.Cache)
if !ok || certCache == nil {
certCache = certmagic.NewCache(certmagic.DefaultStorage)
c.OnShutdown(func() error {
certCache.Stop()
return nil
})
c.Set(CertCacheInstStorageKey, certCache)
}
config.Manager = certmagic.NewWithCache(certCache, certmagic.Config{})
// we use certmagic events to collect metrics for telemetry // we use certmagic events to collect metrics for telemetry
config.Manager.OnEvent = func(event string, data interface{}) { config.Manager.OnEvent = func(event string, data interface{}) {
switch event { switch event {
......
...@@ -47,11 +47,18 @@ func TestMain(m *testing.M) { ...@@ -47,11 +47,18 @@ func TestMain(m *testing.M) {
} }
func TestSetupParseBasic(t *testing.T) { func TestSetupParseBasic(t *testing.T) {
cfg := &Config{Manager: &certmagic.Config{}} tmpdir, err := ioutil.TempDir("", "caddytls_setup_test_")
if err != nil {
t.Fatal(err)
}
defer os.RemoveAll(tmpdir)
certCache := certmagic.NewCache(&certmagic.FileStorage{Path: tmpdir})
cfg := &Config{Manager: certmagic.NewWithCache(certCache, certmagic.Config{})}
RegisterConfigGetter("", func(c *caddy.Controller) *Config { return cfg }) RegisterConfigGetter("", func(c *caddy.Controller) *Config { return cfg })
c := caddy.NewTestController("", `tls `+certFile+` `+keyFile+``) c := caddy.NewTestController("", `tls `+certFile+` `+keyFile+``)
err := setupTLS(c) err = setupTLS(c)
if err != nil { if err != nil {
t.Errorf("Expected no errors, got: %v", err) t.Errorf("Expected no errors, got: %v", err)
} }
...@@ -126,11 +133,18 @@ func TestSetupParseWithOptionalParams(t *testing.T) { ...@@ -126,11 +133,18 @@ func TestSetupParseWithOptionalParams(t *testing.T) {
alpn http/1.1 alpn http/1.1
}` }`
cfg := &Config{Manager: &certmagic.Config{}} tmpdir, err := ioutil.TempDir("", "caddytls_setup_test_")
if err != nil {
t.Fatal(err)
}
defer os.RemoveAll(tmpdir)
certCache := certmagic.NewCache(&certmagic.FileStorage{Path: tmpdir})
cfg := &Config{Manager: certmagic.NewWithCache(certCache, certmagic.Config{})}
RegisterConfigGetter("", func(c *caddy.Controller) *Config { return cfg }) RegisterConfigGetter("", func(c *caddy.Controller) *Config { return cfg })
c := caddy.NewTestController("", params) c := caddy.NewTestController("", params)
err := setupTLS(c) err = setupTLS(c)
if err != nil { if err != nil {
t.Errorf("Expected no errors, got: %v", err) t.Errorf("Expected no errors, got: %v", err)
} }
......
...@@ -315,7 +315,7 @@ func (cfg *Config) cacheCertificate(cert Certificate) Certificate { ...@@ -315,7 +315,7 @@ func (cfg *Config) cacheCertificate(cert Certificate) Certificate {
// (yes, if certs overlap in the names they serve, one will // (yes, if certs overlap in the names they serve, one will
// overwrite another here, but that's just how it goes) // overwrite another here, but that's just how it goes)
for _, name := range cert.Names { for _, name := range cert.Names {
cfg.certificates[name] = cert.Hash cfg.certificates[NormalizedName(name)] = cert.Hash
} }
// store the certificate // store the certificate
......
...@@ -484,6 +484,12 @@ var ( ...@@ -484,6 +484,12 @@ var (
// the risk of rate limiting. // the risk of rate limiting.
CertObtainTimeout time.Duration CertObtainTimeout time.Duration
// Set the default server name for clients
// not indicating a server name using SNI.
// In most cases this will be the primary
// domain that is being served.
DefaultServerName string
// The state needed to operate on-demand TLS // The state needed to operate on-demand TLS
OnDemand *OnDemandConfig OnDemand *OnDemandConfig
......
...@@ -215,7 +215,7 @@ func (cfg *Config) newACMEClient(interactive bool) (*acmeClient, error) { ...@@ -215,7 +215,7 @@ func (cfg *Config) newACMEClient(interactive bool) (*acmeClient, error) {
// lockKey returns a key for a lock that is specific to the operation // lockKey returns a key for a lock that is specific to the operation
// named op being performed related to domainName and this config's CA. // named op being performed related to domainName and this config's CA.
func (cfg *Config) lockKey(op, domainName string) string { func (cfg *Config) lockKey(op, domainName string) string {
return fmt.Sprintf("%s:%s:%s", op, domainName, cfg.CA) return fmt.Sprintf("%s_%s_%s", op, domainName, cfg.CA)
} }
// Obtain obtains a single certificate for name. It stores the certificate // Obtain obtains a single certificate for name. It stores the certificate
......
...@@ -101,6 +101,11 @@ type Config struct { ...@@ -101,6 +101,11 @@ type Config struct {
// the risk of rate limiting. // the risk of rate limiting.
CertObtainTimeout time.Duration CertObtainTimeout time.Duration
// DefaultServerName specifies a server name
// to use when choosing a certificate if the
// ClientHello's ServerName field is empty
DefaultServerName string
// The state needed to operate on-demand TLS // The state needed to operate on-demand TLS
OnDemand *OnDemandConfig OnDemand *OnDemandConfig
...@@ -207,6 +212,9 @@ func NewWithCache(certCache *Cache, cfg Config) *Config { ...@@ -207,6 +212,9 @@ func NewWithCache(certCache *Cache, cfg Config) *Config {
if cfg.CertObtainTimeout == 0 { if cfg.CertObtainTimeout == 0 {
cfg.CertObtainTimeout = CertObtainTimeout cfg.CertObtainTimeout = CertObtainTimeout
} }
if cfg.DefaultServerName == "" {
cfg.DefaultServerName = DefaultServerName
}
if cfg.OnDemand == nil { if cfg.OnDemand == nil {
cfg.OnDemand = OnDemand cfg.OnDemand = OnDemand
} }
......
...@@ -35,6 +35,8 @@ func (certCache *Cache) maintainAssets() { ...@@ -35,6 +35,8 @@ func (certCache *Cache) maintainAssets() {
renewalTicker := time.NewTicker(certCache.RenewInterval) renewalTicker := time.NewTicker(certCache.RenewInterval)
ocspTicker := time.NewTicker(certCache.OCSPInterval) ocspTicker := time.NewTicker(certCache.OCSPInterval)
log.Printf("[INFO][%s] Started certificate maintenance routine", certCache.storage)
for { for {
select { select {
case <-renewalTicker.C: case <-renewalTicker.C:
......
...@@ -50,7 +50,7 @@ func (s tlsALPNSolver) Present(domain, token, keyAuth string) error { ...@@ -50,7 +50,7 @@ func (s tlsALPNSolver) Present(domain, token, keyAuth string) error {
// CleanUp removes the challenge certificate from the cache. // CleanUp removes the challenge certificate from the cache.
func (s tlsALPNSolver) CleanUp(domain, token, keyAuth string) error { func (s tlsALPNSolver) CleanUp(domain, token, keyAuth string) error {
s.certCache.mu.Lock() s.certCache.mu.Lock()
delete(s.certCache.cache, domain) delete(s.certCache.cache, tlsALPNCertKeyName(domain))
s.certCache.mu.Unlock() s.certCache.mu.Unlock()
return nil return nil
} }
......
...@@ -138,7 +138,7 @@ ...@@ -138,7 +138,7 @@
"importpath": "github.com/mholt/certmagic", "importpath": "github.com/mholt/certmagic",
"repository": "https://github.com/mholt/certmagic", "repository": "https://github.com/mholt/certmagic",
"vcs": "git", "vcs": "git",
"revision": "c1d472b46046ee329c099086d689ada0c44d56b0", "revision": "a7f18a937c080b88693cd4e14d48e42cc067b268",
"branch": "master", "branch": "master",
"notests": true "notests": true
}, },
......
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