-
Notifications
You must be signed in to change notification settings - Fork 351
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
feature: allow overriding tls config based on client hello #3303
base: master
Are you sure you want to change the base?
Changes from all commits
b30d93f
35391e6
43dd917
4c74244
f8a2489
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,6 +1,7 @@ | ||
package certregistry | ||
|
||
import ( | ||
"bytes" | ||
"crypto/tls" | ||
"crypto/x509" | ||
"fmt" | ||
|
@@ -9,62 +10,114 @@ import ( | |
log "github.com/sirupsen/logrus" | ||
) | ||
|
||
// CertRegistry object holds TLS certificates to be used to terminate TLS connections | ||
// Config holds a certificate registry TLS configuration. | ||
type Config struct { | ||
ClientAuth tls.ClientAuthType | ||
Certificate tls.Certificate | ||
} | ||
|
||
// CertRegistry object holds TLS Config to be used to terminate TLS connections | ||
// ensuring synchronized access to them. | ||
type CertRegistry struct { | ||
mu sync.Mutex | ||
lookup map[string]*tls.Certificate | ||
lookup map[string]*tlsConfigWrapper | ||
|
||
// defaultTLSConfig is TLS config to be used as a base config for all host configs. | ||
defaultConfig *tls.Config | ||
} | ||
|
||
// tlsConfigWrapper holds the tls.Config and a hash of a host configuration. | ||
type tlsConfigWrapper struct { | ||
config *tls.Config | ||
hash []byte | ||
} | ||
|
||
// NewCertRegistry initializes the certificate registry. | ||
func NewCertRegistry() *CertRegistry { | ||
l := make(map[string]*tls.Certificate) | ||
l := make(map[string]*tlsConfigWrapper) | ||
|
||
return &CertRegistry{ | ||
lookup: l, | ||
lookup: l, | ||
defaultConfig: &tls.Config{}, | ||
} | ||
} | ||
|
||
// Configures certificate for the host if no configuration exists or | ||
// if certificate is valid (`NotBefore` field) after previously configured certificate. | ||
func (r *CertRegistry) ConfigureCertificate(host string, cert *tls.Certificate) error { | ||
if cert == nil { | ||
return fmt.Errorf("cannot configure nil certificate") | ||
// Configures TLS for the host if no configuration exists or | ||
// if config certificate is valid (`NotBefore` field) after previously configured certificate. | ||
func (r *CertRegistry) SetTLSConfig(host string, config *Config) error { | ||
if config == nil { | ||
return fmt.Errorf("cannot configure nil tls config") | ||
} | ||
// loading parsed leaf certificate to certificate | ||
leaf, err := x509.ParseCertificate(cert.Certificate[0]) | ||
leaf, err := x509.ParseCertificate(config.Certificate.Certificate[0]) | ||
if err != nil { | ||
return fmt.Errorf("failed parsing leaf certificate: %w", err) | ||
} | ||
cert.Leaf = leaf | ||
config.Certificate.Leaf = leaf | ||
|
||
// Get tls.config and hash from the config | ||
tlsConfig, configHash := r.configToTLSConfig(config) | ||
|
||
r.mu.Lock() | ||
defer r.mu.Unlock() | ||
|
||
// Check if the config is already set | ||
curr, found := r.lookup[host] | ||
if found { | ||
if cert.Leaf.NotBefore.After(curr.Leaf.NotBefore) { | ||
log.Infof("updating certificate in registry - %s", host) | ||
r.lookup[host] = cert | ||
return nil | ||
} else { | ||
if found && bytes.Equal(curr.hash, configHash) { | ||
return nil | ||
} | ||
|
||
if found && !bytes.Equal(curr.hash, configHash) { | ||
if !config.Certificate.Leaf.NotBefore.After(curr.config.Certificates[0].Leaf.NotBefore) { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. problem: this causes config to be skipped when separate ingresses have different annotations. We'd need to set the annotation for all hosts regardless of the ingress. |
||
return nil | ||
} | ||
} else { | ||
log.Infof("adding certificate to registry - %s", host) | ||
r.lookup[host] = cert | ||
return nil | ||
} | ||
|
||
log.Infof("setting tls config in registry - %s", host) | ||
wrapper := &tlsConfigWrapper{ | ||
config: tlsConfig, | ||
hash: configHash, | ||
} | ||
r.lookup[host] = wrapper | ||
|
||
return nil | ||
} | ||
|
||
// GetCertFromHello reads the SNI from a TLS client and returns the appropriate certificate. | ||
// If no certificate is found for the host it will return nil. | ||
func (r *CertRegistry) GetCertFromHello(hello *tls.ClientHelloInfo) (*tls.Certificate, error) { | ||
// GetConfigFromHello reads the SNI from a TLS client and returns the appropriate config. | ||
func (r *CertRegistry) GetConfigFromHello(hello *tls.ClientHelloInfo) (*tls.Config, error) { | ||
r.mu.Lock() | ||
cert, found := r.lookup[hello.ServerName] | ||
entry, found := r.lookup[hello.ServerName] | ||
r.mu.Unlock() | ||
if found { | ||
return cert, nil | ||
return entry.config, nil | ||
} | ||
return nil, nil | ||
return entry.config, nil | ||
} | ||
|
||
// configToTLSConfig converts a Config to a tls.Config and returns the hash of the config. | ||
func (r *CertRegistry) configToTLSConfig(config *Config) (*tls.Config, []byte) { | ||
if config == nil { | ||
return nil, nil | ||
} | ||
|
||
var hash []byte | ||
|
||
tlsConfig := r.defaultConfig.Clone() | ||
|
||
// Add client auth settings | ||
tlsConfig.ClientAuth = config.ClientAuth | ||
hash = append(hash, byte(config.ClientAuth>>8), byte(config.ClientAuth)) | ||
|
||
// Add certificate | ||
tlsConfig.Certificates = append(tlsConfig.Certificates, config.Certificate) | ||
for _, certData := range config.Certificate.Certificate { | ||
hash = append(hash, certData...) | ||
} | ||
|
||
return tlsConfig, hash | ||
} | ||
|
||
// SetDefaultTLSConfig sets the default TLS config which should be used as a base for all host specific configs. | ||
func (r *CertRegistry) SetDefaultTLSConfig(config *tls.Config) { | ||
r.defaultConfig = config | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -1210,7 +1210,8 @@ func (o *Options) tlsConfig(cr *certregistry.CertRegistry) (*tls.Config, error) | |
} | ||
|
||
if cr != nil { | ||
config.GetCertificate = cr.GetCertFromHello | ||
cr.SetDefaultTLSConfig(config) | ||
config.GetConfigForClient = cr.GetConfigFromHello | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. problem: the
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Check if the new one is non nil and if so only use that one, else fallback to current behavior. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Makes sense. I was also thinking we could also use |
||
} | ||
|
||
if o.CertPathTLS == "" && o.KeyPathTLS == "" { | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
just an idea to support casing as well