From 12608842e1e510c6002c36215d9059959ae9dde5 Mon Sep 17 00:00:00 2001 From: Michal K Date: Wed, 3 Dec 2025 15:28:17 +0100 Subject: [PATCH 1/3] Adjust debug log message for TLS sanitization While debugging a TLS-related issue yesterday, I initially assumed that a certificate was missing because it did not appear in the debug configuration log. After some time I realized that Traefik intentionally hides TLS fields when logging the dynamic configuration. To avoid similar confusion in the future, I adjusted the debug log message to explicitly state that TLS data is sanitized. This is a small, non-functional change that improves clarity during debugging. Signed-off-by: Michal K --- pkg/server/configurationwatcher.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pkg/server/configurationwatcher.go b/pkg/server/configurationwatcher.go index 8142f8359..9764bffcf 100644 --- a/pkg/server/configurationwatcher.go +++ b/pkg/server/configurationwatcher.go @@ -224,7 +224,7 @@ func logConfiguration(logger zerolog.Logger, configMsg dynamic.Message) { logger.Error().Err(err).Msg("Could not marshal dynamic configuration") logger.Debug().Msgf("Configuration received: [struct] %#v", copyConf) } else { - logger.Debug().RawJSON("config", jsonConf).Msg("Configuration received") + logger.Debug().RawJSON("config", jsonConf).Msg("Configuration received (TLS data hidden)") } } From 48671ef4daa90beb8b4a990e90e70e9e6e64c977 Mon Sep 17 00:00:00 2001 From: Michal K Date: Thu, 25 Dec 2025 17:14:41 +0100 Subject: [PATCH 2/3] TLS Configuration - Sanitize + Summary --- pkg/server/configurationwatcher.go | 94 +++++++++++++++++++++++------- 1 file changed, 72 insertions(+), 22 deletions(-) diff --git a/pkg/server/configurationwatcher.go b/pkg/server/configurationwatcher.go index 9764bffcf..6591b9d1f 100644 --- a/pkg/server/configurationwatcher.go +++ b/pkg/server/configurationwatcher.go @@ -181,13 +181,49 @@ func logConfiguration(logger zerolog.Logger, configMsg dynamic.Message) { if logger.GetLevel() > zerolog.DebugLevel { return } + sanitizedConfiguration := getSanitizedConfigurationTLS(configMsg.Configuration) + summary := tlsSummary(configMsg.Configuration) + jsonConf, err := json.Marshal(sanitizedConfiguration) + if err != nil { + logger.Error().Err(err).Msg("Could not marshal dynamic configuration") + logger.Debug().Msgf("Configuration received: [struct] %#v", sanitizedConfiguration) + return + } + evt := logger.Debug().RawJSON("config", jsonConf) + if summary != nil { + evt = evt.Any("tls_summary", summary) + } + evt.Msg("Configuration received (TLS redacted)") +} + +func isEmptyConfiguration(conf *dynamic.Configuration) bool { + if conf.TCP == nil { + conf.TCP = &dynamic.TCPConfiguration{} + } + if conf.HTTP == nil { + conf.HTTP = &dynamic.HTTPConfiguration{} + } + if conf.UDP == nil { + conf.UDP = &dynamic.UDPConfiguration{} + } + + httpEmpty := conf.HTTP.Routers == nil && conf.HTTP.Services == nil && conf.HTTP.Middlewares == nil + tlsEmpty := conf.TLS == nil || conf.TLS.Certificates == nil && conf.TLS.Stores == nil && conf.TLS.Options == nil + tcpEmpty := conf.TCP.Routers == nil && conf.TCP.Services == nil && conf.TCP.Middlewares == nil + udpEmpty := conf.UDP.Routers == nil && conf.UDP.Services == nil + + return httpEmpty && tlsEmpty && tcpEmpty && udpEmpty +} + +func getSanitizedConfigurationTLS(configuration *dynamic.Configuration) *dynamic.Configuration { + copyConf := configuration.DeepCopy() - copyConf := configMsg.Configuration.DeepCopy() 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 = []types.FileOrContent{} cleanedOptions[name] = option @@ -219,30 +255,44 @@ func logConfiguration(logger zerolog.Logger, configMsg dynamic.Message) { } } - jsonConf, err := json.Marshal(copyConf) - if err != nil { - logger.Error().Err(err).Msg("Could not marshal dynamic configuration") - logger.Debug().Msgf("Configuration received: [struct] %#v", copyConf) - } else { - logger.Debug().RawJSON("config", jsonConf).Msg("Configuration received (TLS data hidden)") - } + return copyConf } -func isEmptyConfiguration(conf *dynamic.Configuration) bool { - if conf.TCP == nil { - conf.TCP = &dynamic.TCPConfiguration{} - } - if conf.HTTP == nil { - conf.HTTP = &dynamic.HTTPConfiguration{} - } - if conf.UDP == nil { - conf.UDP = &dynamic.UDPConfiguration{} +func tlsSummary(orig *dynamic.Configuration) map[string]any { + if orig == nil || orig.TLS == nil { + return nil } - httpEmpty := conf.HTTP.Routers == nil && conf.HTTP.Services == nil && conf.HTTP.Middlewares == nil - tlsEmpty := conf.TLS == nil || conf.TLS.Certificates == nil && conf.TLS.Stores == nil && conf.TLS.Options == nil - tcpEmpty := conf.TCP.Routers == nil && conf.TCP.Services == nil && conf.TCP.Middlewares == nil - udpEmpty := conf.UDP.Routers == nil && conf.UDP.Services == nil + stores := make([]string, 0, len(orig.TLS.Stores)) + defaultCerts := 0 + for name, st := range orig.TLS.Stores { + stores = append(stores, name) + if st.DefaultCertificate != nil { + defaultCerts++ + } + } - return httpEmpty && tlsEmpty && tcpEmpty && udpEmpty + caFiles := 0 + if orig.TLS.Options != nil { + for _, opt := range orig.TLS.Options { + caFiles += len(opt.ClientAuth.CAFiles) + } + } + + return map[string]any{ + "certificates": map[string]any{ + "present": len(orig.TLS.Certificates) > 0, + "count": len(orig.TLS.Certificates), + "value": "", + }, + "stores": map[string]any{ + "names": stores, + "default_cert_count": defaultCerts, + "default_cert": "", + }, + "clientAuth": map[string]any{ + "ca_files_total": caFiles, + "ca_files": "", + }, + } } From e95786e9a5ec26d1031d931143ce3a78be25da33 Mon Sep 17 00:00:00 2001 From: Michal K Date: Thu, 25 Dec 2025 17:41:07 +0100 Subject: [PATCH 3/3] implement *Config extenasions --- pkg/config/dynamic/config.go | 101 ++++++++++++++++++++++++++ pkg/server/configurationwatcher.go | 109 +---------------------------- 2 files changed, 104 insertions(+), 106 deletions(-) diff --git a/pkg/config/dynamic/config.go b/pkg/config/dynamic/config.go index 1e125ad78..33a1798a6 100644 --- a/pkg/config/dynamic/config.go +++ b/pkg/config/dynamic/config.go @@ -2,6 +2,7 @@ package dynamic import ( "github.com/traefik/traefik/v3/pkg/tls" + "github.com/traefik/traefik/v3/pkg/types" ) // +k8s:deepcopy-gen=true @@ -35,3 +36,103 @@ type TLSConfiguration struct { Options map[string]tls.Options `json:"options,omitempty" toml:"options,omitempty" yaml:"options,omitempty" label:"-" export:"true"` Stores map[string]tls.Store `json:"stores,omitempty" toml:"stores,omitempty" yaml:"stores,omitempty" export:"true"` } + +func (c *Configuration) IsEmpty() bool { + if c.TCP == nil { + c.TCP = &TCPConfiguration{} + } + if c.HTTP == nil { + c.HTTP = &HTTPConfiguration{} + } + if c.UDP == nil { + c.UDP = &UDPConfiguration{} + } + + httpEmpty := c.HTTP.Routers == nil && c.HTTP.Services == nil && c.HTTP.Middlewares == nil + tlsEmpty := c.TLS == nil || c.TLS.Certificates == nil && c.TLS.Stores == nil && c.TLS.Options == nil + tcpEmpty := c.TCP.Routers == nil && c.TCP.Services == nil && c.TCP.Middlewares == nil + udpEmpty := c.UDP.Routers == nil && c.UDP.Services == nil + + return httpEmpty && tlsEmpty && tcpEmpty && udpEmpty +} + +func (c *Configuration) GetSanitizedConfigurationTLS() *Configuration { + copyConf := c.DeepCopy() + + 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 = []types.FileOrContent{} + cleanedOptions[name] = option + } + + copyConf.TLS.Options = cleanedOptions + } + + for k := range copyConf.TLS.Stores { + st := copyConf.TLS.Stores[k] + st.DefaultCertificate = nil + copyConf.TLS.Stores[k] = st + } + } + + if copyConf.HTTP != nil { + for _, transport := range copyConf.HTTP.ServersTransports { + transport.Certificates = tls.Certificates{} + transport.RootCAs = []types.FileOrContent{} + } + } + + if copyConf.TCP != nil { + for _, transport := range copyConf.TCP.ServersTransports { + if transport.TLS != nil { + transport.TLS.Certificates = tls.Certificates{} + transport.TLS.RootCAs = []types.FileOrContent{} + } + } + } + return copyConf +} + +func (c *Configuration) SummaryTLS() map[string]any { + if c.TLS == nil { + return nil + } + + stores := make([]string, 0, len(c.TLS.Stores)) + defaultCerts := 0 + for name, st := range c.TLS.Stores { + stores = append(stores, name) + if st.DefaultCertificate != nil { + defaultCerts++ + } + } + + caFiles := 0 + if c.TLS.Options != nil { + for _, opt := range c.TLS.Options { + caFiles += len(opt.ClientAuth.CAFiles) + } + } + + return map[string]any{ + "certificates": map[string]any{ + "present": len(c.TLS.Certificates) > 0, + "count": len(c.TLS.Certificates), + "value": "", + }, + "stores": map[string]any{ + "names": stores, + "default_cert_count": defaultCerts, + "default_cert": "", + }, + "clientAuth": map[string]any{ + "ca_files_total": caFiles, + "ca_files": "", + }, + } +} diff --git a/pkg/server/configurationwatcher.go b/pkg/server/configurationwatcher.go index 6591b9d1f..75d53abf7 100644 --- a/pkg/server/configurationwatcher.go +++ b/pkg/server/configurationwatcher.go @@ -11,8 +11,6 @@ import ( "github.com/traefik/traefik/v3/pkg/observability/logs" "github.com/traefik/traefik/v3/pkg/provider" "github.com/traefik/traefik/v3/pkg/safe" - "github.com/traefik/traefik/v3/pkg/tls" - "github.com/traefik/traefik/v3/pkg/types" ) // ConfigurationWatcher watches configuration changes. @@ -115,7 +113,7 @@ func (c *ConfigurationWatcher) receiveConfigurations(ctx context.Context) { continue } - if isEmptyConfiguration(configMsg.Configuration) { + if configMsg.Configuration.IsEmpty() { logger.Debug().Msg("Skipping empty configuration") continue } @@ -181,8 +179,8 @@ func logConfiguration(logger zerolog.Logger, configMsg dynamic.Message) { if logger.GetLevel() > zerolog.DebugLevel { return } - sanitizedConfiguration := getSanitizedConfigurationTLS(configMsg.Configuration) - summary := tlsSummary(configMsg.Configuration) + sanitizedConfiguration := configMsg.Configuration.GetSanitizedConfigurationTLS() + summary := configMsg.Configuration.SummaryTLS() jsonConf, err := json.Marshal(sanitizedConfiguration) if err != nil { logger.Error().Err(err).Msg("Could not marshal dynamic configuration") @@ -195,104 +193,3 @@ func logConfiguration(logger zerolog.Logger, configMsg dynamic.Message) { } evt.Msg("Configuration received (TLS redacted)") } - -func isEmptyConfiguration(conf *dynamic.Configuration) bool { - if conf.TCP == nil { - conf.TCP = &dynamic.TCPConfiguration{} - } - if conf.HTTP == nil { - conf.HTTP = &dynamic.HTTPConfiguration{} - } - if conf.UDP == nil { - conf.UDP = &dynamic.UDPConfiguration{} - } - - httpEmpty := conf.HTTP.Routers == nil && conf.HTTP.Services == nil && conf.HTTP.Middlewares == nil - tlsEmpty := conf.TLS == nil || conf.TLS.Certificates == nil && conf.TLS.Stores == nil && conf.TLS.Options == nil - tcpEmpty := conf.TCP.Routers == nil && conf.TCP.Services == nil && conf.TCP.Middlewares == nil - udpEmpty := conf.UDP.Routers == nil && conf.UDP.Services == nil - - return httpEmpty && tlsEmpty && tcpEmpty && udpEmpty -} - -func getSanitizedConfigurationTLS(configuration *dynamic.Configuration) *dynamic.Configuration { - copyConf := configuration.DeepCopy() - - 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 = []types.FileOrContent{} - cleanedOptions[name] = option - } - - copyConf.TLS.Options = cleanedOptions - } - - for k := range copyConf.TLS.Stores { - st := copyConf.TLS.Stores[k] - st.DefaultCertificate = nil - copyConf.TLS.Stores[k] = st - } - } - - if copyConf.HTTP != nil { - for _, transport := range copyConf.HTTP.ServersTransports { - transport.Certificates = tls.Certificates{} - transport.RootCAs = []types.FileOrContent{} - } - } - - if copyConf.TCP != nil { - for _, transport := range copyConf.TCP.ServersTransports { - if transport.TLS != nil { - transport.TLS.Certificates = tls.Certificates{} - transport.TLS.RootCAs = []types.FileOrContent{} - } - } - } - - return copyConf -} - -func tlsSummary(orig *dynamic.Configuration) map[string]any { - if orig == nil || orig.TLS == nil { - return nil - } - - stores := make([]string, 0, len(orig.TLS.Stores)) - defaultCerts := 0 - for name, st := range orig.TLS.Stores { - stores = append(stores, name) - if st.DefaultCertificate != nil { - defaultCerts++ - } - } - - caFiles := 0 - if orig.TLS.Options != nil { - for _, opt := range orig.TLS.Options { - caFiles += len(opt.ClientAuth.CAFiles) - } - } - - return map[string]any{ - "certificates": map[string]any{ - "present": len(orig.TLS.Certificates) > 0, - "count": len(orig.TLS.Certificates), - "value": "", - }, - "stores": map[string]any{ - "names": stores, - "default_cert_count": defaultCerts, - "default_cert": "", - }, - "clientAuth": map[string]any{ - "ca_files_total": caFiles, - "ca_files": "", - }, - } -}