From e1f5866989514821535c8422a9d20bcb304ba826 Mon Sep 17 00:00:00 2001 From: Jean-Baptiste Doumenjou <925513+jbdoumenjou@users.noreply.github.com> Date: Tue, 13 Jul 2021 14:14:35 +0200 Subject: [PATCH] Detect certificates content modifications Co-authored-by: Romain Co-authored-by: Mathieu Lonjaret --- docs/content/getting-started/faq.md | 13 +++ pkg/provider/file/file.go | 80 +++++++++++++++++++ pkg/provider/file/file_test.go | 31 ++++++- .../file/fixtures/toml/tls_file_key.cert | 1 + pkg/server/configurationwatcher.go | 18 +++++ 5 files changed, 140 insertions(+), 3 deletions(-) create mode 100644 pkg/provider/file/fixtures/toml/tls_file_key.cert diff --git a/docs/content/getting-started/faq.md b/docs/content/getting-started/faq.md index 66ae963af..74ad989bf 100644 --- a/docs/content/getting-started/faq.md +++ b/docs/content/getting-started/faq.md @@ -124,3 +124,16 @@ http: 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, 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. diff --git a/pkg/provider/file/file.go b/pkg/provider/file/file.go index 7578cc62c..f6a48b613 100644 --- a/pkg/provider/file/file.go +++ b/pkg/provider/file/file.go @@ -167,6 +167,86 @@ func (p *Provider) loadFileConfig(ctx context.Context, filename string, parseTem if configuration.TLS != nil { 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 diff --git a/pkg/provider/file/file_test.go b/pkg/provider/file/file_test.go index 6dde44321..23d820891 100644 --- a/pkg/provider/file/file_test.go +++ b/pkg/provider/file/file_test.go @@ -25,19 +25,35 @@ type ProvideTestCase struct { expectedNumTLSOptions int } -func TestTLSContent(t *testing.T) { +func TestTLSCertificateContent(t *testing.T) { tempDir := t.TempDir() fileTLS, err := createTempFile("./fixtures/toml/tls_file.cert", tempDir) 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") require.NoError(t, err) content := ` [[tls.certificates]] 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)) @@ -48,7 +64,16 @@ func TestTLSContent(t *testing.T) { require.NoError(t, err) 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) { diff --git a/pkg/provider/file/fixtures/toml/tls_file_key.cert b/pkg/provider/file/fixtures/toml/tls_file_key.cert new file mode 100644 index 000000000..f196b32ee --- /dev/null +++ b/pkg/provider/file/fixtures/toml/tls_file_key.cert @@ -0,0 +1 @@ +CONTENTKEY \ No newline at end of file diff --git a/pkg/server/configurationwatcher.go b/pkg/server/configurationwatcher.go index 1d71fe472..a4b78e6ad 100644 --- a/pkg/server/configurationwatcher.go +++ b/pkg/server/configurationwatcher.go @@ -12,6 +12,7 @@ import ( "github.com/traefik/traefik/v2/pkg/log" "github.com/traefik/traefik/v2/pkg/provider" "github.com/traefik/traefik/v2/pkg/safe" + "github.com/traefik/traefik/v2/pkg/tls" ) // ConfigurationWatcher watches configuration changes. @@ -164,6 +165,16 @@ func (c *ConfigurationWatcher) preLoadConfiguration(configMsg dynamic.Message) { if copyConf.TLS != 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 { st := copyConf.TLS.Stores[k] 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) if err != nil { logger.Errorf("Could not marshal dynamic configuration: %v", err)