mirror of
https://github.com/caddyserver/caddy.git
synced 2025-03-04 02:02:53 -05:00
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
This commit is contained in:
parent
fa10b0275f
commit
9e4a29191c
13 changed files with 187 additions and 135 deletions
|
@ -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() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
//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 {
|
||||||
}
|
}
|
||||||
|
|
||||||
// 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) {
|
||||||
// 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
|
||||||
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
|
||||||
// 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)
|
||||||
|
}
|
||||||
|
|
||||||
// exact match? great, let's use it
|
// 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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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 {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 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 {
|
||||||
|
|
||||||
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) {
|
||||||
}
|
}
|
||||||
|
|
||||||
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) {
|
||||||
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)
|
||||||
}
|
}
|
||||||
|
|
2
vendor/github.com/mholt/certmagic/certificates.go
generated
vendored
2
vendor/github.com/mholt/certmagic/certificates.go
generated
vendored
|
@ -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
|
||||||
|
|
6
vendor/github.com/mholt/certmagic/certmagic.go
generated
vendored
6
vendor/github.com/mholt/certmagic/certmagic.go
generated
vendored
|
@ -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
|
||||||
|
|
||||||
|
|
2
vendor/github.com/mholt/certmagic/client.go
generated
vendored
2
vendor/github.com/mholt/certmagic/client.go
generated
vendored
|
@ -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
|
||||||
|
|
8
vendor/github.com/mholt/certmagic/config.go
generated
vendored
8
vendor/github.com/mholt/certmagic/config.go
generated
vendored
|
@ -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 {
|
||||||
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
|
||||||
}
|
}
|
||||||
|
|
157
vendor/github.com/mholt/certmagic/handshake.go
generated
vendored
157
vendor/github.com/mholt/certmagic/handshake.go
generated
vendored
|
@ -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
|
// 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 {
|
if err == nil && cfg.OnEvent != nil {
|
||||||
cfg.OnEvent("tls_handshake_completed", clientHello)
|
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.
|
// which is by the Go Authors.
|
||||||
//
|
//
|
||||||
// This function is safe for concurrent use.
|
// This function is safe for concurrent use.
|
||||||
func (cfg *Config) getCertificate(name string) (cert Certificate, matched, defaulted bool) {
|
func (cfg *Config) getCertificate(hello *tls.ClientHelloInfo) (cert Certificate, matched, defaulted bool) {
|
||||||
var certKey string
|
name := NormalizedName(hello.ServerName)
|
||||||
var ok bool
|
|
||||||
|
|
||||||
cfg.certCache.mu.RLock()
|
cfg.certCache.mu.RLock()
|
||||||
defer cfg.certCache.mu.RUnlock()
|
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 {
|
if certKey, ok = cfg.certificates[name]; ok {
|
||||||
cert = cfg.certCache.cache[certKey]
|
cert = cfg.certCache.cache[certKey]
|
||||||
matched = true
|
matched = true
|
||||||
return
|
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, ".")
|
labels := strings.Split(name, ".")
|
||||||
for i := range labels {
|
for i := range labels {
|
||||||
labels[i] = "*"
|
labels[i] = "*"
|
||||||
|
@ -135,18 +158,15 @@ func (cfg *Config) getCertificate(name string) (cert Certificate, matched, defau
|
||||||
matched = true
|
matched = true
|
||||||
return
|
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
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -161,9 +181,11 @@ func (cfg *Config) getCertificate(name string) (cert Certificate, matched, defau
|
||||||
// certificate is available.
|
// certificate is available.
|
||||||
//
|
//
|
||||||
// This function is safe for concurrent use.
|
// 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
|
// 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 {
|
if matched {
|
||||||
return cert, nil
|
return cert, nil
|
||||||
}
|
}
|
||||||
|
@ -172,11 +194,11 @@ func (cfg *Config) getCertDuringHandshake(hello wrappedClientHelloInfo, loadIfNe
|
||||||
// obtain a needed certificate
|
// obtain a needed certificate
|
||||||
if cfg.OnDemand != nil && loadIfNecessary {
|
if cfg.OnDemand != nil && loadIfNecessary {
|
||||||
// Then check to see if we have one on disk
|
// Then check to see if we have one on disk
|
||||||
loadedCert, err := cfg.CacheManagedCertificate(hello.serverNameOrIP)
|
loadedCert, err := cfg.CacheManagedCertificate(name)
|
||||||
if err == nil {
|
if err == nil {
|
||||||
loadedCert, err = cfg.handshakeMaintenance(hello, loadedCert)
|
loadedCert, err = cfg.handshakeMaintenance(hello, loadedCert)
|
||||||
if err != nil {
|
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
|
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
|
// By this point, we need to ask the CA for a certificate
|
||||||
|
|
||||||
// Make sure the certificate should be obtained based on config
|
// Make sure the certificate should be obtained based on config
|
||||||
err := cfg.checkIfCertShouldBeObtained(hello.serverNameOrIP)
|
err := cfg.checkIfCertShouldBeObtained(name)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return Certificate{}, err
|
return Certificate{}, err
|
||||||
}
|
}
|
||||||
|
|
||||||
// Name has to qualify for a certificate
|
// Name has to qualify for a certificate
|
||||||
if !HostQualifies(hello.serverNameOrIP) {
|
if !HostQualifies(name) {
|
||||||
return cert, fmt.Errorf("hostname '%s' does not qualify for certificate", hello.serverNameOrIP)
|
return cert, fmt.Errorf("hostname '%s' does not qualify for certificate", name)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Obtain certificate from the CA
|
// Obtain certificate from the CA
|
||||||
|
@ -204,7 +226,7 @@ func (cfg *Config) getCertDuringHandshake(hello wrappedClientHelloInfo, loadIfNe
|
||||||
return cert, nil
|
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
|
// 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.
|
// hello, it will wait and use what the other goroutine obtained.
|
||||||
//
|
//
|
||||||
// This function is safe for use by multiple concurrent goroutines.
|
// 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.
|
// We must protect this process from happening concurrently, so synchronize.
|
||||||
obtainCertWaitChansMu.Lock()
|
obtainCertWaitChansMu.Lock()
|
||||||
wait, ok := obtainCertWaitChans[hello.serverNameOrIP]
|
wait, ok := obtainCertWaitChans[name]
|
||||||
if ok {
|
if ok {
|
||||||
// lucky us -- another goroutine is already obtaining the certificate.
|
// lucky us -- another goroutine is already obtaining the certificate.
|
||||||
// wait for it to finish obtaining the cert and then we'll use it.
|
// 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.
|
// 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
|
// make a chan others can wait on if needed
|
||||||
wait = make(chan struct{})
|
wait = make(chan struct{})
|
||||||
obtainCertWaitChans[hello.serverNameOrIP] = wait
|
obtainCertWaitChans[name] = wait
|
||||||
obtainCertWaitChansMu.Unlock()
|
obtainCertWaitChansMu.Unlock()
|
||||||
|
|
||||||
// obtain the certificate
|
// obtain the certificate
|
||||||
log.Printf("[INFO] Obtaining new certificate for %s", hello.serverNameOrIP)
|
log.Printf("[INFO] Obtaining new certificate for %s", name)
|
||||||
err := cfg.ObtainCert(hello.serverNameOrIP, false)
|
err := cfg.ObtainCert(name, false)
|
||||||
|
|
||||||
// immediately unblock anyone waiting for it; doing this in
|
// immediately unblock anyone waiting for it; doing this in
|
||||||
// a defer would risk deadlock because of the recursive call
|
// a defer would risk deadlock because of the recursive call
|
||||||
// to getCertDuringHandshake below when we return!
|
// to getCertDuringHandshake below when we return!
|
||||||
obtainCertWaitChansMu.Lock()
|
obtainCertWaitChansMu.Lock()
|
||||||
close(wait)
|
close(wait)
|
||||||
delete(obtainCertWaitChans, hello.serverNameOrIP)
|
delete(obtainCertWaitChans, name)
|
||||||
obtainCertWaitChansMu.Unlock()
|
obtainCertWaitChansMu.Unlock()
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
// Failed to solve challenge, so don't allow another on-demand
|
// Failed to solve challenge, so don't allow another on-demand
|
||||||
// issue for this name to be attempted for a little while.
|
// issue for this name to be attempted for a little while.
|
||||||
failedIssuanceMu.Lock()
|
failedIssuanceMu.Lock()
|
||||||
failedIssuance[hello.serverNameOrIP] = time.Now()
|
failedIssuance[name] = time.Now()
|
||||||
go func(name string) {
|
go func(name string) {
|
||||||
time.Sleep(5 * time.Minute)
|
time.Sleep(5 * time.Minute)
|
||||||
failedIssuanceMu.Lock()
|
failedIssuanceMu.Lock()
|
||||||
delete(failedIssuance, name)
|
delete(failedIssuance, name)
|
||||||
failedIssuanceMu.Unlock()
|
failedIssuanceMu.Unlock()
|
||||||
}(hello.serverNameOrIP)
|
}(name)
|
||||||
failedIssuanceMu.Unlock()
|
failedIssuanceMu.Unlock()
|
||||||
return Certificate{}, err
|
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.
|
// handshakeMaintenance performs a check on cert for expiration and OCSP validity.
|
||||||
//
|
//
|
||||||
// This function is safe for use by multiple concurrent goroutines.
|
// 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
|
// Check cert expiration
|
||||||
timeLeft := cert.NotAfter.Sub(time.Now().UTC())
|
timeLeft := cert.NotAfter.Sub(time.Now().UTC())
|
||||||
if timeLeft < cfg.RenewDurationBefore {
|
if timeLeft < cfg.RenewDurationBefore {
|
||||||
|
@ -296,7 +320,7 @@ func (cfg *Config) handshakeMaintenance(hello wrappedClientHelloInfo, cert Certi
|
||||||
if err != nil {
|
if err != nil {
|
||||||
// An error with OCSP stapling is not the end of the world, and in fact, is
|
// 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.
|
// 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.mu.Lock()
|
||||||
cfg.certCache.cache[cert.Hash] = cert
|
cfg.certCache.cache[cert.Hash] = cert
|
||||||
|
@ -313,10 +337,11 @@ func (cfg *Config) handshakeMaintenance(hello wrappedClientHelloInfo, cert Certi
|
||||||
// ClientHello.
|
// ClientHello.
|
||||||
//
|
//
|
||||||
// This function is safe for use by multiple concurrent goroutines.
|
// 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()
|
obtainCertWaitChansMu.Lock()
|
||||||
wait, ok := obtainCertWaitChans[hello.serverNameOrIP]
|
wait, ok := obtainCertWaitChans[name]
|
||||||
if ok {
|
if ok {
|
||||||
// lucky us -- another goroutine is already renewing the certificate.
|
// lucky us -- another goroutine is already renewing the certificate.
|
||||||
// wait for it to finish, then we'll use the new one.
|
// 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
|
// looks like it's up to us to do all the work and renew the cert
|
||||||
wait = make(chan struct{})
|
wait = make(chan struct{})
|
||||||
obtainCertWaitChans[hello.serverNameOrIP] = wait
|
obtainCertWaitChans[name] = wait
|
||||||
obtainCertWaitChansMu.Unlock()
|
obtainCertWaitChansMu.Unlock()
|
||||||
|
|
||||||
// renew and reload the certificate
|
// renew and reload the certificate
|
||||||
log.Printf("[INFO] Renewing certificate for %s", hello.serverNameOrIP)
|
log.Printf("[INFO] Renewing certificate for %s", name)
|
||||||
err := cfg.RenewCert(hello.serverNameOrIP, false)
|
err := cfg.RenewCert(name, false)
|
||||||
if err == nil {
|
if err == nil {
|
||||||
// even though the recursive nature of the dynamic cert loading
|
// even though the recursive nature of the dynamic cert loading
|
||||||
// would just call this function anyway, we do it here to
|
// would just call this function anyway, we do it here to
|
||||||
// make the replacement as atomic as possible.
|
// 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 {
|
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 {
|
} else {
|
||||||
// replace the old certificate with the new one
|
// replace the old certificate with the new one
|
||||||
err = cfg.certCache.replaceCertificate(currentCert, newCert)
|
err = cfg.certCache.replaceCertificate(currentCert, newCert)
|
||||||
if err != nil {
|
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!
|
// to getCertDuringHandshake below when we return!
|
||||||
obtainCertWaitChansMu.Lock()
|
obtainCertWaitChansMu.Lock()
|
||||||
close(wait)
|
close(wait)
|
||||||
delete(obtainCertWaitChans, hello.serverNameOrIP)
|
delete(obtainCertWaitChans, name)
|
||||||
obtainCertWaitChansMu.Unlock()
|
obtainCertWaitChansMu.Unlock()
|
||||||
|
|
||||||
if err != nil {
|
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.
|
// is present, it makes a certificate to solve this challenge and returns it.
|
||||||
// A boolean true is returned if a valid certificate is returned.
|
// A boolean true is returned if a valid certificate is returned.
|
||||||
func (cfg *Config) tryDistributedChallengeSolver(clientHello *tls.ClientHelloInfo) (Certificate, bool, error) {
|
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)
|
chalInfoBytes, err := cfg.certCache.storage.Load(tokenKey)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if _, ok := err.(ErrNotExist); ok {
|
if _, ok := err.(ErrNotExist); ok {
|
||||||
|
@ -396,36 +421,10 @@ func (cfg *Config) tryDistributedChallengeSolver(clientHello *tls.ClientHelloInf
|
||||||
return Certificate{Certificate: *cert}, true, nil
|
return Certificate{Certificate: *cert}, true, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// CertNameFromClientHello returns a normalized name for which to
|
// NormalizedName returns a cleaned form of serverName that is
|
||||||
// look up a certificate given this ClientHelloInfo. If the client
|
// used for consistency when referring to a SNI value.
|
||||||
// did not send a ServerName value, the connection's local IP is
|
func NormalizedName(serverName string) string {
|
||||||
// assumed.
|
return strings.ToLower(strings.TrimSpace(serverName))
|
||||||
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
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// obtainCertWaitChans is used to coordinate obtaining certs for each hostname.
|
// obtainCertWaitChans is used to coordinate obtaining certs for each hostname.
|
||||||
|
|
2
vendor/github.com/mholt/certmagic/maintain.go
generated
vendored
2
vendor/github.com/mholt/certmagic/maintain.go
generated
vendored
|
@ -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:
|
||||||
|
|
2
vendor/github.com/mholt/certmagic/solvers.go
generated
vendored
2
vendor/github.com/mholt/certmagic/solvers.go
generated
vendored
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
2
vendor/manifest
vendored
2
vendor/manifest
vendored
|
@ -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
|
||||||
},
|
},
|
||||||
|
|
Loading…
Add table
Reference in a new issue