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() {
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.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.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")
......@@ -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 {
mustLogFatalf("%v", err)
}
......
......@@ -93,7 +93,7 @@ type Config struct {
}
// 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.
func NewConfig(inst *caddy.Instance) *Config {
inst.StorageMu.RLock()
......@@ -257,10 +257,14 @@ func MakeTLSConfig(configs []*Config) (*tls.Config, error) {
// configs with the same hostname pattern; should
// be OK since we already asserted they are roughly
// the same); during TLS handshakes, configs are
// loaded based on the hostname pattern, according
// to client's SNI
// loaded based on the hostname pattern according
// to client's ServerName (SNI) value
if cfg.Hostname == "0.0.0.0" || cfg.Hostname == "::" {
configMap[""] = cfg
} else {
configMap[cfg.Hostname] = cfg
}
}
// Is TLS disabled? By now, we know that all
// configs agree whether it is or not, so we
......
......@@ -17,6 +17,8 @@ package caddytls
import (
"crypto/tls"
"fmt"
"log"
"net"
"strings"
"github.com/mholt/caddy/telemetry"
......@@ -38,14 +40,31 @@ type configGroup map[string]*Config
// This function follows nearly the same logic to lookup
// a hostname as the getCertificate function uses.
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 {
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, ".")
for i := range labels {
labels[i] = "*"
......@@ -55,14 +74,25 @@ func (cg configGroup) getConfig(hello *tls.ClientHelloInfo) *Config {
}
}
// try a config that serves all names (the above
// loop doesn't try empty string; for hosts defined
// with only a port, for instance, like ":443") -
// also known as the default config
// try a config that matches all names - this
// is needed to match configs defined without
// a specific host, like ":443", when SNI is
// a non-empty value
if config, ok := cg[""]; ok {
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
}
......
......@@ -54,18 +54,6 @@ func setupTLS(c *caddy.Controller) error {
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
config.Manager.OnEvent = func(event string, data interface{}) {
switch event {
......
......@@ -47,11 +47,18 @@ func TestMain(m *testing.M) {
}
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 })
c := caddy.NewTestController("", `tls `+certFile+` `+keyFile+``)
err := setupTLS(c)
err = setupTLS(c)
if err != nil {
t.Errorf("Expected no errors, got: %v", err)
}
......@@ -126,11 +133,18 @@ func TestSetupParseWithOptionalParams(t *testing.T) {
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 })
c := caddy.NewTestController("", params)
err := setupTLS(c)
err = setupTLS(c)
if err != nil {
t.Errorf("Expected no errors, got: %v", err)
}
......
......@@ -315,7 +315,7 @@ func (cfg *Config) cacheCertificate(cert Certificate) Certificate {
// (yes, if certs overlap in the names they serve, one will
// overwrite another here, but that's just how it goes)
for _, name := range cert.Names {
cfg.certificates[name] = cert.Hash
cfg.certificates[NormalizedName(name)] = cert.Hash
}
// store the certificate
......
......@@ -484,6 +484,12 @@ var (
// the risk of rate limiting.
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
OnDemand *OnDemandConfig
......
......@@ -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
// named op being performed related to domainName and this config's CA.
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
......
......@@ -101,6 +101,11 @@ type Config struct {
// the risk of rate limiting.
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
OnDemand *OnDemandConfig
......@@ -207,6 +212,9 @@ func NewWithCache(certCache *Cache, cfg Config) *Config {
if cfg.CertObtainTimeout == 0 {
cfg.CertObtainTimeout = CertObtainTimeout
}
if cfg.DefaultServerName == "" {
cfg.DefaultServerName = DefaultServerName
}
if cfg.OnDemand == nil {
cfg.OnDemand = OnDemand
}
......
......@@ -67,13 +67,8 @@ func (cfg *Config) GetCertificate(clientHello *tls.ClientHelloInfo) (*tls.Certif
}
}
wrapped := wrappedClientHelloInfo{
ClientHelloInfo: clientHello,
serverNameOrIP: CertNameFromClientHello(clientHello),
}
// get the certificate and serve it up
cert, err := cfg.getCertDuringHandshake(wrapped, true, true)
cert, err := cfg.getCertDuringHandshake(clientHello, true, true)
if err == nil && cfg.OnEvent != nil {
cfg.OnEvent("tls_handshake_completed", clientHello)
}
......@@ -96,21 +91,49 @@ func (cfg *Config) GetCertificate(clientHello *tls.ClientHelloInfo) (*tls.Certif
// which is by the Go Authors.
//
// This function is safe for concurrent use.
func (cfg *Config) getCertificate(name string) (cert Certificate, matched, defaulted bool) {
var certKey string
var ok bool
func (cfg *Config) getCertificate(hello *tls.ClientHelloInfo) (cert Certificate, matched, defaulted bool) {
name := NormalizedName(hello.ServerName)
cfg.certCache.mu.RLock()
defer cfg.certCache.mu.RUnlock()
// exact match? great, let's use it
var certKey string
var ok bool
if name == "" {
// if SNI is empty, prefer matching IP address
if hello.Conn != nil {
addr := hello.Conn.LocalAddr().String()
ip, _, err := net.SplitHostPort(addr)
if err == nil {
addr = ip
}
if certKey, ok = cfg.certificates[addr]; ok {
cert = cfg.certCache.cache[certKey]
matched = true
return
}
}
// fall back to a "default" certificate, if specified
if cfg.DefaultServerName != "" {
normDefault := NormalizedName(cfg.DefaultServerName)
if certKey, ok := cfg.certificates[normDefault]; ok {
cert = cfg.certCache.cache[certKey]
defaulted = true
return
}
}
} else {
// if SNI is specified, try an exact match first
if certKey, ok = cfg.certificates[name]; ok {
cert = cfg.certCache.cache[certKey]
matched = true
return
}
// try replacing labels in the name with wildcards until we get a match
// try replacing labels in the name with
// wildcards until we get a match
labels := strings.Split(name, ".")
for i := range labels {
labels[i] = "*"
......@@ -135,18 +158,15 @@ func (cfg *Config) getCertificate(name string) (cert Certificate, matched, defau
matched = true
return
}
// if nothing matches, use a "default" certificate (See issues
// mholt/caddy#2035 and mholt/caddy#1303; any change to this
// behavior must account for hosts defined like ":443" or
// "0.0.0.0:443" where the hostname is empty or a catch-all
// IP or something.)
if certKey, ok := cfg.certificates[""]; ok {
cert = cfg.certCache.cache[certKey]
defaulted = true
return
}
// otherwise, we're bingo on ammo; see issues
// mholt/caddy#2035 and mholt/caddy#1303 (any
// change to certificate matching behavior must
// account for hosts defined where the hostname
// is empty or a catch-all, like ":443" or
// "0.0.0.0:443")
return
}
......@@ -161,9 +181,11 @@ func (cfg *Config) getCertificate(name string) (cert Certificate, matched, defau
// certificate is available.
//
// This function is safe for concurrent use.
func (cfg *Config) getCertDuringHandshake(hello wrappedClientHelloInfo, loadIfNecessary, obtainIfNecessary bool) (Certificate, error) {
func (cfg *Config) getCertDuringHandshake(hello *tls.ClientHelloInfo, loadIfNecessary, obtainIfNecessary bool) (Certificate, error) {
name := NormalizedName(hello.ServerName)
// First check our in-memory cache to see if we've already loaded it
cert, matched, defaulted := cfg.getCertificate(hello.serverNameOrIP)
cert, matched, defaulted := cfg.getCertificate(hello)
if matched {
return cert, nil
}
......@@ -172,11 +194,11 @@ func (cfg *Config) getCertDuringHandshake(hello wrappedClientHelloInfo, loadIfNe
// obtain a needed certificate
if cfg.OnDemand != nil && loadIfNecessary {
// Then check to see if we have one on disk
loadedCert, err := cfg.CacheManagedCertificate(hello.serverNameOrIP)
loadedCert, err := cfg.CacheManagedCertificate(name)
if err == nil {
loadedCert, err = cfg.handshakeMaintenance(hello, loadedCert)
if err != nil {
log.Printf("[ERROR] Maintaining newly-loaded certificate for %s: %v", hello.serverNameOrIP, err)
log.Printf("[ERROR] Maintaining newly-loaded certificate for %s: %v", name, err)
}
return loadedCert, nil
}
......@@ -184,14 +206,14 @@ func (cfg *Config) getCertDuringHandshake(hello wrappedClientHelloInfo, loadIfNe
// By this point, we need to ask the CA for a certificate
// Make sure the certificate should be obtained based on config
err := cfg.checkIfCertShouldBeObtained(hello.serverNameOrIP)
err := cfg.checkIfCertShouldBeObtained(name)
if err != nil {
return Certificate{}, err
}
// Name has to qualify for a certificate
if !HostQualifies(hello.serverNameOrIP) {
return cert, fmt.Errorf("hostname '%s' does not qualify for certificate", hello.serverNameOrIP)
if !HostQualifies(name) {
return cert, fmt.Errorf("hostname '%s' does not qualify for certificate", name)
}
// Obtain certificate from the CA
......@@ -204,7 +226,7 @@ func (cfg *Config) getCertDuringHandshake(hello wrappedClientHelloInfo, loadIfNe
return cert, nil
}
return Certificate{}, fmt.Errorf("no certificate available for %s", hello.serverNameOrIP)
return Certificate{}, fmt.Errorf("no certificate available for '%s'", name)
}
// checkIfCertShouldBeObtained checks to see if an on-demand tls certificate
......@@ -222,10 +244,12 @@ func (cfg *Config) checkIfCertShouldBeObtained(name string) error {
// hello, it will wait and use what the other goroutine obtained.
//
// This function is safe for use by multiple concurrent goroutines.
func (cfg *Config) obtainOnDemandCertificate(hello wrappedClientHelloInfo) (Certificate, error) {
func (cfg *Config) obtainOnDemandCertificate(hello *tls.ClientHelloInfo) (Certificate, error) {
name := NormalizedName(hello.ServerName)
// We must protect this process from happening concurrently, so synchronize.
obtainCertWaitChansMu.Lock()
wait, ok := obtainCertWaitChans[hello.serverNameOrIP]
wait, ok := obtainCertWaitChans[name]
if ok {
// lucky us -- another goroutine is already obtaining the certificate.
// wait for it to finish obtaining the cert and then we'll use it.
......@@ -237,32 +261,32 @@ func (cfg *Config) obtainOnDemandCertificate(hello wrappedClientHelloInfo) (Cert
// looks like it's up to us to do all the work and obtain the cert.
// make a chan others can wait on if needed
wait = make(chan struct{})
obtainCertWaitChans[hello.serverNameOrIP] = wait
obtainCertWaitChans[name] = wait
obtainCertWaitChansMu.Unlock()
// obtain the certificate
log.Printf("[INFO] Obtaining new certificate for %s", hello.serverNameOrIP)
err := cfg.ObtainCert(hello.serverNameOrIP, false)
log.Printf("[INFO] Obtaining new certificate for %s", name)
err := cfg.ObtainCert(name, false)
// immediately unblock anyone waiting for it; doing this in
// a defer would risk deadlock because of the recursive call
// to getCertDuringHandshake below when we return!
obtainCertWaitChansMu.Lock()
close(wait)
delete(obtainCertWaitChans, hello.serverNameOrIP)
delete(obtainCertWaitChans, name)
obtainCertWaitChansMu.Unlock()
if err != nil {
// Failed to solve challenge, so don't allow another on-demand
// issue for this name to be attempted for a little while.
failedIssuanceMu.Lock()
failedIssuance[hello.serverNameOrIP] = time.Now()
failedIssuance[name] = time.Now()
go func(name string) {
time.Sleep(5 * time.Minute)
failedIssuanceMu.Lock()
delete(failedIssuance, name)
failedIssuanceMu.Unlock()
}(hello.serverNameOrIP)
}(name)
failedIssuanceMu.Unlock()
return Certificate{}, err
}
......@@ -280,7 +304,7 @@ func (cfg *Config) obtainOnDemandCertificate(hello wrappedClientHelloInfo) (Cert
// handshakeMaintenance performs a check on cert for expiration and OCSP validity.
//
// This function is safe for use by multiple concurrent goroutines.
func (cfg *Config) handshakeMaintenance(hello wrappedClientHelloInfo, cert Certificate) (Certificate, error) {
func (cfg *Config) handshakeMaintenance(hello *tls.ClientHelloInfo, cert Certificate) (Certificate, error) {
// Check cert expiration
timeLeft := cert.NotAfter.Sub(time.Now().UTC())
if timeLeft < cfg.RenewDurationBefore {
......@@ -296,7 +320,7 @@ func (cfg *Config) handshakeMaintenance(hello wrappedClientHelloInfo, cert Certi
if err != nil {
// An error with OCSP stapling is not the end of the world, and in fact, is
// quite common considering not all certs have issuer URLs that support it.
log.Printf("[ERROR] Getting OCSP for %s: %v", hello.serverNameOrIP, err)
log.Printf("[ERROR] Getting OCSP for %s: %v", hello.ServerName, err)
}
cfg.certCache.mu.Lock()
cfg.certCache.cache[cert.Hash] = cert
......@@ -313,10 +337,11 @@ func (cfg *Config) handshakeMaintenance(hello wrappedClientHelloInfo, cert Certi
// ClientHello.
//
// This function is safe for use by multiple concurrent goroutines.
func (cfg *Config) renewDynamicCertificate(hello wrappedClientHelloInfo, currentCert Certificate) (Certificate, error) {
func (cfg *Config) renewDynamicCertificate(hello *tls.ClientHelloInfo, currentCert Certificate) (Certificate, error) {
name := NormalizedName(hello.ServerName)
obtainCertWaitChansMu.Lock()
wait, ok := obtainCertWaitChans[hello.serverNameOrIP]
wait, ok := obtainCertWaitChans[name]
if ok {
// lucky us -- another goroutine is already renewing the certificate.
// wait for it to finish, then we'll use the new one.
......@@ -327,24 +352,24 @@ func (cfg *Config) renewDynamicCertificate(hello wrappedClientHelloInfo, current
// looks like it's up to us to do all the work and renew the cert
wait = make(chan struct{})
obtainCertWaitChans[hello.serverNameOrIP] = wait
obtainCertWaitChans[name] = wait
obtainCertWaitChansMu.Unlock()
// renew and reload the certificate
log.Printf("[INFO] Renewing certificate for %s", hello.serverNameOrIP)
err := cfg.RenewCert(hello.serverNameOrIP, false)
log.Printf("[INFO] Renewing certificate for %s", name)
err := cfg.RenewCert(name, false)
if err == nil {
// even though the recursive nature of the dynamic cert loading
// would just call this function anyway, we do it here to
// make the replacement as atomic as possible.
newCert, err := currentCert.configs[0].CacheManagedCertificate(hello.serverNameOrIP)
newCert, err := currentCert.configs[0].CacheManagedCertificate(name)
if err != nil {
log.Printf("[ERROR] loading renewed certificate for %s: %v", hello.serverNameOrIP, err)
log.Printf("[ERROR] loading renewed certificate for %s: %v", name, err)
} else {
// replace the old certificate with the new one
err = cfg.certCache.replaceCertificate(currentCert, newCert)
if err != nil {
log.Printf("[ERROR] Replacing certificate for %s: %v", hello.serverNameOrIP, err)
log.Printf("[ERROR] Replacing certificate for %s: %v", name, err)
}
}
}
......@@ -354,7 +379,7 @@ func (cfg *Config) renewDynamicCertificate(hello wrappedClientHelloInfo, current
// to getCertDuringHandshake below when we return!
obtainCertWaitChansMu.Lock()
close(wait)
delete(obtainCertWaitChans, hello.serverNameOrIP)
delete(obtainCertWaitChans, name)
obtainCertWaitChansMu.Unlock()
if err != nil {
......@@ -370,7 +395,7 @@ func (cfg *Config) renewDynamicCertificate(hello wrappedClientHelloInfo, current
// is present, it makes a certificate to solve this challenge and returns it.
// A boolean true is returned if a valid certificate is returned.
func (cfg *Config) tryDistributedChallengeSolver(clientHello *tls.ClientHelloInfo) (Certificate, bool, error) {
tokenKey := distributedSolver{}.challengeTokensKey(clientHello.ServerName)
tokenKey := distributedSolver{config: cfg}.challengeTokensKey(clientHello.ServerName)
chalInfoBytes, err := cfg.certCache.storage.Load(tokenKey)
if err != nil {
if _, ok := err.(ErrNotExist); ok {
......@@ -396,36 +421,10 @@ func (cfg *Config) tryDistributedChallengeSolver(clientHello *tls.ClientHelloInf
return Certificate{Certificate: *cert}, true, nil
}
// CertNameFromClientHello returns a normalized name for which to
// look up a certificate given this ClientHelloInfo. If the client
// did not send a ServerName value, the connection's local IP is
// assumed.
func CertNameFromClientHello(hello *tls.ClientHelloInfo) string {
// Not going to trim trailing dots here since RFC 3546 says,
// "The hostname is represented ... without a trailing dot."
// Just normalize to lowercase and remove any leading or
// trailing whitespace n case the hello was sloppily made
name := strings.ToLower(strings.TrimSpace(hello.ServerName))
// if SNI is not set, assume IP of listener
if name == "" && hello.Conn != nil {
addr := hello.Conn.LocalAddr().String()
ip, _, err := net.SplitHostPort(addr)
if err == nil {
name = ip
}
}
return name
}
// wrappedClientHelloInfo is a type that allows us to
// attach a name with which to look for a certificate
// to a given ClientHelloInfo, since not all clients
// use SNI and some self-signed certificates use IP.
type wrappedClientHelloInfo struct {
*tls.ClientHelloInfo
serverNameOrIP string
// NormalizedName returns a cleaned form of serverName that is
// used for consistency when referring to a SNI value.
func NormalizedName(serverName string) string {
return strings.ToLower(strings.TrimSpace(serverName))
}
// obtainCertWaitChans is used to coordinate obtaining certs for each hostname.
......
......@@ -35,6 +35,8 @@ func (certCache *Cache) maintainAssets() {
renewalTicker := time.NewTicker(certCache.RenewInterval)
ocspTicker := time.NewTicker(certCache.OCSPInterval)
log.Printf("[INFO][%s] Started certificate maintenance routine", certCache.storage)
for {
select {
case <-renewalTicker.C:
......
......@@ -50,7 +50,7 @@ func (s tlsALPNSolver) Present(domain, token, keyAuth string) error {
// CleanUp removes the challenge certificate from the cache.
func (s tlsALPNSolver) CleanUp(domain, token, keyAuth string) error {
s.certCache.mu.Lock()
delete(s.certCache.cache, domain)
delete(s.certCache.cache, tlsALPNCertKeyName(domain))
s.certCache.mu.Unlock()
return nil
}
......
......@@ -138,7 +138,7 @@
"importpath": "github.com/mholt/certmagic",
"repository": "https://github.com/mholt/certmagic",
"vcs": "git",
"revision": "c1d472b46046ee329c099086d689ada0c44d56b0",
"revision": "a7f18a937c080b88693cd4e14d48e42cc067b268",
"branch": "master",
"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