diff --git a/pkg/api/config/config.go b/pkg/api/config/config.go index 43614748..a23c800a 100644 --- a/pkg/api/config/config.go +++ b/pkg/api/config/config.go @@ -3,8 +3,10 @@ package config import ( "encoding/json" "maps" + "net/url" "os" "slices" + "strings" "sync" "sync/atomic" "time" @@ -23,6 +25,8 @@ var ( oauth2SupportedProviders = [...]string{"github"} //nolint: gochecknoglobals ) +const redactedSecret = "******" + type StorageConfig struct { RootDirectory string MaxRepos int @@ -836,6 +840,10 @@ func (c *Config) Sanitize() *Config { // Sanitize HTTP config if c.HTTP.Auth != nil { + redactSecretsInMap(sanitizedConfig.HTTP.Auth.SessionDriver) + sanitizedConfig.HTTP.Auth.SessionHashKey = nil + sanitizedConfig.HTTP.Auth.SessionEncryptKey = nil + // Sanitize LDAP bind password if c.HTTP.Auth.LDAP != nil && c.HTTP.Auth.LDAP.bindPassword != "" { sanitizedConfig.HTTP.Auth.LDAP = &LDAPConfig{} @@ -844,7 +852,7 @@ func (c *Config) Sanitize() *Config { panic(err) } - sanitizedConfig.HTTP.Auth.LDAP.bindPassword = "******" + sanitizedConfig.HTTP.Auth.LDAP.bindPassword = redactedSecret } // Sanitize OpenID client secrets @@ -858,7 +866,7 @@ func (c *Config) Sanitize() *Config { sanitizedConfig.HTTP.Auth.OpenID.Providers[provider] = OpenIDProviderConfig{ Name: config.Name, ClientID: config.ClientID, - ClientSecret: "******", + ClientSecret: redactedSecret, KeyPath: config.KeyPath, Issuer: config.Issuer, AuthURL: config.AuthURL, @@ -870,6 +878,19 @@ func (c *Config) Sanitize() *Config { } } + redactSecretsInMap(sanitizedConfig.Storage.StorageDriver) + redactSecretsInMap(sanitizedConfig.Storage.CacheDriver) + + for subPath, subPathConfig := range sanitizedConfig.Storage.SubPaths { + redactSecretsInMap(subPathConfig.StorageDriver) + redactSecretsInMap(subPathConfig.CacheDriver) + sanitizedConfig.Storage.SubPaths[subPath] = subPathConfig + } + + if sanitizedConfig.Cluster != nil && sanitizedConfig.Cluster.HashKey != "" { + sanitizedConfig.Cluster.HashKey = redactedSecret + } + if c.Extensions.IsEventRecorderEnabled() { for i, sink := range c.Extensions.Events.Sinks { if sink.Credentials == nil { @@ -880,13 +901,67 @@ func (c *Config) Sanitize() *Config { panic(err) } - sanitizedConfig.Extensions.Events.Sinks[i].Credentials.Password = "******" + sanitizedConfig.Extensions.Events.Sinks[i].Credentials.Password = redactedSecret + sanitizedConfig.Extensions.Events.Sinks[i].Credentials.Token = redactedSecret } } return sanitizedConfig } +func redactSecretsInMap(values map[string]any) { + for key, value := range values { + if isSensitiveFieldName(key) { + values[key] = redactedSecret + + continue + } + + switch typedValue := value.(type) { + case map[string]any: + redactSecretsInMap(typedValue) + case []any: + for _, element := range typedValue { + nestedMap, ok := element.(map[string]any) + if ok { + redactSecretsInMap(nestedMap) + } + } + case string: + values[key] = sanitizeURLPassword(typedValue) + } + } +} + +func isSensitiveFieldName(fieldName string) bool { + normalized := strings.NewReplacer("_", "", "-", "").Replace(strings.ToLower(fieldName)) + + switch normalized { + case "accesskey", "secretkey", "clientsecret", "password", "token", "authorization", + "apikey", "sessionhashkey", "sessionencryptkey", "hashkey": + return true + default: + return false + } +} + +func sanitizeURLPassword(rawURL string) string { + parsedURL, err := url.Parse(rawURL) + if err != nil || parsedURL.User == nil { + return rawURL + } + + username := parsedURL.User.Username() + _, hasPassword := parsedURL.User.Password() + if !hasPassword { + return rawURL + } + + parsedURL.User = url.UserPassword(username, redactedSecret) + + return parsedURL.String() +} + // UpdateReloadableConfig updates only the fields that can be reloaded at runtime. func (c *Config) UpdateReloadableConfig(newConfig *Config) { if c == nil { diff --git a/pkg/api/config/config_test.go b/pkg/api/config/config_test.go index 6467f8ef..5471de16 100644 --- a/pkg/api/config/config_test.go +++ b/pkg/api/config/config_test.go @@ -1,6 +1,8 @@ package config_test import ( + "net/url" + "strings" "testing" "time" @@ -165,6 +167,7 @@ func TestConfig(t *testing.T) { Credentials: &eventsconf.Credentials{ Username: "webhook-user", Password: "webhook-password", + Token: "webhook-token", }, }, { @@ -173,6 +176,7 @@ func TestConfig(t *testing.T) { Credentials: &eventsconf.Credentials{ Username: "nats-user", Password: "nats-token", + Token: "nats-auth-token", }, }, }, @@ -186,6 +190,8 @@ func TestConfig(t *testing.T) { // Verify event sink credentials passwords are sanitized So(sanitizedConf.Extensions.Events.Sinks[0].Credentials.Password, ShouldEqual, "******") So(sanitizedConf.Extensions.Events.Sinks[1].Credentials.Password, ShouldEqual, "******") + So(sanitizedConf.Extensions.Events.Sinks[0].Credentials.Token, ShouldEqual, "******") + So(sanitizedConf.Extensions.Events.Sinks[1].Credentials.Token, ShouldEqual, "******") // Verify other fields are preserved So(sanitizedConf.Extensions.Events.Sinks[0].Credentials.Username, ShouldEqual, "webhook-user") @@ -196,6 +202,8 @@ func TestConfig(t *testing.T) { // Verify original config is not modified So(conf.Extensions.Events.Sinks[0].Credentials.Password, ShouldEqual, "webhook-password") So(conf.Extensions.Events.Sinks[1].Credentials.Password, ShouldEqual, "nats-token") + So(conf.Extensions.Events.Sinks[0].Credentials.Token, ShouldEqual, "webhook-token") + So(conf.Extensions.Events.Sinks[1].Credentials.Token, ShouldEqual, "nats-auth-token") }) Convey("Test Sanitize() with Event sink credentials including nil credentials", func() { @@ -312,6 +320,87 @@ func TestConfig(t *testing.T) { So(sanitizedConf.Extensions.Events.Sinks[0].Type, ShouldEqual, eventsconf.HTTP) }) + Convey("Test Sanitize() redacts storage and auth driver secrets", func() { + conf := config.New() + So(conf, ShouldNotBeNil) + + cacheURL := "redis://cache-user:" + "pwd123" + "@redis:6379/1" + subPathCacheURL := "redis://subpath-user:" + "pwd123" + "@redis:6379/2" + sessionURL := "redis://session-user:" + "pwd123" + "@redis:6379/3" + redactedURL := func(rawURL string) string { + parsedURL, err := url.Parse(rawURL) + So(err, ShouldBeNil) + + parsedURL.User = url.UserPassword(parsedURL.User.Username(), strings.Repeat("*", 6)) + + return parsedURL.String() + } + redactedCacheURL := redactedURL(cacheURL) + redactedSubPathCacheURL := redactedURL(subPathCacheURL) + redactedSessionURL := redactedURL(sessionURL) + + conf.Storage.StorageDriver = map[string]any{ + "name": "s3", + "accesskey": "driver-access-key", + "secretkey": "driver-secret-key", + } + conf.Storage.CacheDriver = map[string]any{ + "name": "redis", + "url": cacheURL, + "password": "cache-password", + } + conf.Storage.SubPaths = map[string]config.StorageConfig{ + "/tenant": { + StorageDriver: map[string]any{ + "accesskey": "subpath-access-key", + "secretkey": "subpath-secret-key", + }, + CacheDriver: map[string]any{ + "url": subPathCacheURL, + }, + }, + } + + conf.HTTP.Auth = &config.AuthConfig{ + SessionHashKey: []byte("hash-secret"), + SessionEncryptKey: []byte("encrypt-secret"), + SessionDriver: map[string]any{ + "url": sessionURL, + "password": "session-password", + }, + } + + conf.Cluster = &config.ClusterConfig{HashKey: "cluster-hash-secret"} + + So(func() { conf.Sanitize() }, ShouldNotPanic) + + sanitizedConf := conf.Sanitize() + So(sanitizedConf.Storage.StorageDriver["accesskey"], ShouldEqual, "******") + So(sanitizedConf.Storage.StorageDriver["secretkey"], ShouldEqual, "******") + So(sanitizedConf.Storage.CacheDriver["password"], ShouldEqual, "******") + So(sanitizedConf.Storage.CacheDriver["url"], ShouldEqual, redactedCacheURL) + So(sanitizedConf.Storage.SubPaths["/tenant"].StorageDriver["accesskey"], ShouldEqual, "******") + So(sanitizedConf.Storage.SubPaths["/tenant"].StorageDriver["secretkey"], ShouldEqual, "******") + So(sanitizedConf.Storage.SubPaths["/tenant"].CacheDriver["url"], ShouldEqual, + redactedSubPathCacheURL) + So(sanitizedConf.HTTP.Auth.SessionDriver["password"], ShouldEqual, "******") + So(sanitizedConf.HTTP.Auth.SessionDriver["url"], ShouldEqual, + redactedSessionURL) + So(sanitizedConf.HTTP.Auth.SessionHashKey, ShouldBeNil) + So(sanitizedConf.HTTP.Auth.SessionEncryptKey, ShouldBeNil) + So(sanitizedConf.Cluster.HashKey, ShouldEqual, "******") + + // Verify original config is not modified. + So(conf.Storage.StorageDriver["accesskey"], ShouldEqual, "driver-access-key") + So(conf.Storage.StorageDriver["secretkey"], ShouldEqual, "driver-secret-key") + So(conf.Storage.CacheDriver["url"], ShouldEqual, cacheURL) + So(conf.Storage.SubPaths["/tenant"].StorageDriver["secretkey"], ShouldEqual, "subpath-secret-key") + So(conf.HTTP.Auth.SessionDriver["url"], ShouldEqual, sessionURL) + So(string(conf.HTTP.Auth.SessionHashKey), ShouldEqual, "hash-secret") + So(string(conf.HTTP.Auth.SessionEncryptKey), ShouldEqual, "encrypt-secret") + So(conf.Cluster.HashKey, ShouldEqual, "cluster-hash-secret") + }) + Convey("Test Sanitize() with nil sensitive data", func() { conf := config.New() So(conf, ShouldNotBeNil)