Detect certificates content modifications

Co-authored-by: Romain <rtribotte@users.noreply.github.com>
Co-authored-by: Mathieu Lonjaret <mathieu.lonjaret@gmail.com>
This commit is contained in:
Jean-Baptiste Doumenjou 2021-07-13 14:14:35 +02:00 committed by GitHub
parent 3c1ed0d9b2
commit e1f5866989
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
5 changed files with 140 additions and 3 deletions

View file

@ -124,3 +124,16 @@ http:
If there is a need for a response code other than a `503` and/or a custom message, If there is a need for a response code other than a `503` and/or a custom message,
the principle of the above example above (a catchall router) still stands, the principle of the above example above (a catchall router) still stands,
but the `unavailable` service should be adapted to fit such a need. but the `unavailable` service should be adapted to fit such a need.
## Why Is My TLS Certificate Not Reloaded When Its Contents Change ?
With the file provider,
a configuration update is only triggered when one of the [watched](../providers/file.md#provider-configuration) configuration files is modified.
Which is why, when a certificate is defined by path,
and the actual contents of this certificate change,
a configuration update is _not_ triggered.
To take into account the new certificate contents, the update of the dynamic configuration must be forced.
One way to achieve that, is to trigger a file notification,
for example, by using the `touch` command on the configuration file.

View file

@ -167,6 +167,86 @@ func (p *Provider) loadFileConfig(ctx context.Context, filename string, parseTem
if configuration.TLS != nil { if configuration.TLS != nil {
configuration.TLS.Certificates = flattenCertificates(ctx, configuration.TLS) configuration.TLS.Certificates = flattenCertificates(ctx, configuration.TLS)
// TLS Options
if configuration.TLS.Options != nil {
for name, options := range configuration.TLS.Options {
var caCerts []tls.FileOrContent
for _, caFile := range options.ClientAuth.CAFiles {
content, err := caFile.Read()
if err != nil {
log.FromContext(ctx).Error(err)
continue
}
caCerts = append(caCerts, tls.FileOrContent(content))
}
options.ClientAuth.CAFiles = caCerts
configuration.TLS.Options[name] = options
}
}
// TLS stores
if len(configuration.TLS.Stores) > 0 {
for name, store := range configuration.TLS.Stores {
content, err := store.DefaultCertificate.CertFile.Read()
if err != nil {
log.FromContext(ctx).Error(err)
continue
}
store.DefaultCertificate.CertFile = tls.FileOrContent(content)
content, err = store.DefaultCertificate.KeyFile.Read()
if err != nil {
log.FromContext(ctx).Error(err)
continue
}
store.DefaultCertificate.KeyFile = tls.FileOrContent(content)
configuration.TLS.Stores[name] = store
}
}
}
// ServersTransport
if configuration.HTTP != nil && len(configuration.HTTP.ServersTransports) > 0 {
for name, st := range configuration.HTTP.ServersTransports {
var certificates []tls.Certificate
for _, cert := range st.Certificates {
content, err := cert.CertFile.Read()
if err != nil {
log.FromContext(ctx).Error(err)
continue
}
cert.CertFile = tls.FileOrContent(content)
content, err = cert.KeyFile.Read()
if err != nil {
log.FromContext(ctx).Error(err)
continue
}
cert.KeyFile = tls.FileOrContent(content)
certificates = append(certificates, cert)
}
configuration.HTTP.ServersTransports[name].Certificates = certificates
var rootCAs []tls.FileOrContent
for _, rootCA := range st.RootCAs {
content, err := rootCA.Read()
if err != nil {
log.FromContext(ctx).Error(err)
continue
}
rootCAs = append(rootCAs, tls.FileOrContent(content))
}
st.RootCAs = rootCAs
}
} }
return configuration, nil return configuration, nil

View file

@ -25,19 +25,35 @@ type ProvideTestCase struct {
expectedNumTLSOptions int expectedNumTLSOptions int
} }
func TestTLSContent(t *testing.T) { func TestTLSCertificateContent(t *testing.T) {
tempDir := t.TempDir() tempDir := t.TempDir()
fileTLS, err := createTempFile("./fixtures/toml/tls_file.cert", tempDir) fileTLS, err := createTempFile("./fixtures/toml/tls_file.cert", tempDir)
require.NoError(t, err) require.NoError(t, err)
fileTLSKey, err := createTempFile("./fixtures/toml/tls_file_key.cert", tempDir)
require.NoError(t, err)
fileConfig, err := os.CreateTemp(tempDir, "temp*.toml") fileConfig, err := os.CreateTemp(tempDir, "temp*.toml")
require.NoError(t, err) require.NoError(t, err)
content := ` content := `
[[tls.certificates]] [[tls.certificates]]
certFile = "` + fileTLS.Name() + `" certFile = "` + fileTLS.Name() + `"
keyFile = "` + fileTLS.Name() + `" keyFile = "` + fileTLSKey.Name() + `"
[tls.options.default.clientAuth]
caFiles = ["` + fileTLS.Name() + `"]
[tls.stores.default.defaultCertificate]
certFile = "` + fileTLS.Name() + `"
keyFile = "` + fileTLSKey.Name() + `"
[http.serversTransports.default]
rootCAs = ["` + fileTLS.Name() + `"]
[[http.serversTransports.default.certificates]]
certFile = "` + fileTLS.Name() + `"
keyFile = "` + fileTLSKey.Name() + `"
` `
_, err = fileConfig.Write([]byte(content)) _, err = fileConfig.Write([]byte(content))
@ -48,7 +64,16 @@ func TestTLSContent(t *testing.T) {
require.NoError(t, err) require.NoError(t, err)
require.Equal(t, "CONTENT", configuration.TLS.Certificates[0].Certificate.CertFile.String()) require.Equal(t, "CONTENT", configuration.TLS.Certificates[0].Certificate.CertFile.String())
require.Equal(t, "CONTENT", configuration.TLS.Certificates[0].Certificate.KeyFile.String()) require.Equal(t, "CONTENTKEY", configuration.TLS.Certificates[0].Certificate.KeyFile.String())
require.Equal(t, "CONTENT", configuration.TLS.Options["default"].ClientAuth.CAFiles[0].String())
require.Equal(t, "CONTENT", configuration.TLS.Stores["default"].DefaultCertificate.CertFile.String())
require.Equal(t, "CONTENTKEY", configuration.TLS.Stores["default"].DefaultCertificate.KeyFile.String())
require.Equal(t, "CONTENT", configuration.HTTP.ServersTransports["default"].Certificates[0].CertFile.String())
require.Equal(t, "CONTENTKEY", configuration.HTTP.ServersTransports["default"].Certificates[0].KeyFile.String())
require.Equal(t, "CONTENT", configuration.HTTP.ServersTransports["default"].RootCAs[0].String())
} }
func TestErrorWhenEmptyConfig(t *testing.T) { func TestErrorWhenEmptyConfig(t *testing.T) {

View file

@ -0,0 +1 @@
CONTENTKEY

View file

@ -12,6 +12,7 @@ import (
"github.com/traefik/traefik/v2/pkg/log" "github.com/traefik/traefik/v2/pkg/log"
"github.com/traefik/traefik/v2/pkg/provider" "github.com/traefik/traefik/v2/pkg/provider"
"github.com/traefik/traefik/v2/pkg/safe" "github.com/traefik/traefik/v2/pkg/safe"
"github.com/traefik/traefik/v2/pkg/tls"
) )
// ConfigurationWatcher watches configuration changes. // ConfigurationWatcher watches configuration changes.
@ -164,6 +165,16 @@ func (c *ConfigurationWatcher) preLoadConfiguration(configMsg dynamic.Message) {
if copyConf.TLS != nil { if copyConf.TLS != nil {
copyConf.TLS.Certificates = nil copyConf.TLS.Certificates = nil
if copyConf.TLS.Options != nil {
cleanedOptions := make(map[string]tls.Options, len(copyConf.TLS.Options))
for name, option := range copyConf.TLS.Options {
option.ClientAuth.CAFiles = []tls.FileOrContent{}
cleanedOptions[name] = option
}
copyConf.TLS.Options = cleanedOptions
}
for k := range copyConf.TLS.Stores { for k := range copyConf.TLS.Stores {
st := copyConf.TLS.Stores[k] st := copyConf.TLS.Stores[k]
st.DefaultCertificate = nil st.DefaultCertificate = nil
@ -171,6 +182,13 @@ func (c *ConfigurationWatcher) preLoadConfiguration(configMsg dynamic.Message) {
} }
} }
if copyConf.HTTP != nil {
for _, transport := range copyConf.HTTP.ServersTransports {
transport.Certificates = tls.Certificates{}
transport.RootCAs = []tls.FileOrContent{}
}
}
jsonConf, err := json.Marshal(copyConf) jsonConf, err := json.Marshal(copyConf)
if err != nil { if err != nil {
logger.Errorf("Could not marshal dynamic configuration: %v", err) logger.Errorf("Could not marshal dynamic configuration: %v", err)