feat: enhance config sanitization to mask sensitive keys in storage a… (#4119)

* feat: enhance config sanitization to mask sensitive keys in storage and session drivers

Fixes issue #4117

Signed-off-by: Ramkumar Chinchani <rchincha.dev@gmail.com>

* fix: annotate test fixture tokens to suppress security linter warnings

Signed-off-by: Ramkumar Chinchani <rchincha.dev@gmail.com>

* fix: update test fixture credentials to suppress security linter warnings

Signed-off-by: Ramkumar Chinchani <rchincha.dev@gmail.com>

* fix: simplify slice value sanitization by removing unnecessary index handling

Signed-off-by: Ramkumar Chinchani <rchincha.dev@gmail.com>

* fix: update RedisDB test to use a guaranteed-invalid endpoint for CI stability

Signed-off-by: Ramkumar Chinchani <rchincha.dev@gmail.com>

* fix: update sanitization logic to handle empty credentials for event sinks

Signed-off-by: Ramkumar Chinchani <rchincha.dev@gmail.com>

* fix: format sensitive config map keys for improved readability

Signed-off-by: Ramkumar Chinchani <rchincha.dev@gmail.com>

---------

Signed-off-by: Ramkumar Chinchani <rchincha.dev@gmail.com>
This commit is contained in:
Ramkumar Chinchani
2026-06-06 22:18:20 -07:00
committed by GitHub
parent 3ff9d6ddc1
commit 879fcee3c3
3 changed files with 167 additions and 6 deletions
+84 -1
View File
@@ -5,6 +5,7 @@ import (
"maps"
"os"
"slices"
"strings"
"sync"
"sync/atomic"
"time"
@@ -819,6 +820,58 @@ func (c *Config) isTagsRetentionEnabled(tagRetentionPolicy KeepTagsPolicy) bool
return false
}
func isSensitiveConfigMapKey(key string) bool {
normalized := strings.ToLower(strings.ReplaceAll(key, "_", ""))
switch normalized {
case "accesskey", "secretkey", "password", "secret", "token",
"clientsecret", "sessionhashkey", "sessionencryptkey", "sentinelpassword":
return true
default:
return false
}
}
func sanitizeStringMapValues(values map[string]string) {
for key := range values {
if isSensitiveConfigMapKey(key) {
values[key] = "******"
}
}
}
func sanitizeMapValues(values map[string]any) {
for key, value := range values {
if isSensitiveConfigMapKey(key) {
values[key] = "******"
continue
}
switch typedVal := value.(type) {
case map[string]any:
sanitizeMapValues(typedVal)
case map[string]string:
sanitizeStringMapValues(typedVal)
case []any:
sanitizeSliceValues(typedVal)
}
}
}
func sanitizeSliceValues(values []any) {
for _, value := range values {
switch typedVal := value.(type) {
case map[string]any:
sanitizeMapValues(typedVal)
case map[string]string:
sanitizeStringMapValues(typedVal)
case []any:
sanitizeSliceValues(typedVal)
}
}
}
// Sanitize makes a sanitized copy of the config removing any secrets.
func (c *Config) Sanitize() *Config {
if c == nil {
@@ -834,8 +887,32 @@ func (c *Config) Sanitize() *Config {
panic(err)
}
if sanitizedConfig.Storage.StorageDriver != nil {
sanitizeMapValues(sanitizedConfig.Storage.StorageDriver)
}
if sanitizedConfig.Storage.CacheDriver != nil {
sanitizeMapValues(sanitizedConfig.Storage.CacheDriver)
}
for subPath, subPathCfg := range sanitizedConfig.Storage.SubPaths {
if subPathCfg.StorageDriver != nil {
sanitizeMapValues(subPathCfg.StorageDriver)
}
if subPathCfg.CacheDriver != nil {
sanitizeMapValues(subPathCfg.CacheDriver)
}
sanitizedConfig.Storage.SubPaths[subPath] = subPathCfg
}
// Sanitize HTTP config
if c.HTTP.Auth != nil {
if sanitizedConfig.HTTP.Auth.SessionDriver != nil {
sanitizeMapValues(sanitizedConfig.HTTP.Auth.SessionDriver)
}
// Sanitize LDAP bind password
if c.HTTP.Auth.LDAP != nil && c.HTTP.Auth.LDAP.bindPassword != "" {
sanitizedConfig.HTTP.Auth.LDAP = &LDAPConfig{}
@@ -880,7 +957,13 @@ func (c *Config) Sanitize() *Config {
panic(err)
}
sanitizedConfig.Extensions.Events.Sinks[i].Credentials.Password = "******"
if sanitizedConfig.Extensions.Events.Sinks[i].Credentials.Password != "" {
sanitizedConfig.Extensions.Events.Sinks[i].Credentials.Password = "******"
}
if sanitizedConfig.Extensions.Events.Sinks[i].Credentials.Token != "" {
sanitizedConfig.Extensions.Events.Sinks[i].Credentials.Token = "******"
}
}
}
+81 -3
View File
@@ -165,14 +165,16 @@ func TestConfig(t *testing.T) {
Credentials: &eventsconf.Credentials{
Username: "webhook-user",
Password: "webhook-password",
Token: "webhook-token",
},
},
{
Type: eventsconf.NATS,
Address: "nats://localhost:4222",
Credentials: &eventsconf.Credentials{
Credentials: &eventsconf.Credentials{ //nolint:gosec // test fixture
Username: "nats-user",
Password: "nats-token",
Token: "nats-auth-token",
},
},
},
@@ -186,6 +188,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 +200,80 @@ 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 storage and session driver secrets", func() {
conf := config.New()
So(conf, ShouldNotBeNil)
conf.Storage.StorageDriver = map[string]any{
"name": "s3",
"region": "us-east-1",
"accesskey": "storage-access",
"secretkey": "storage-secret",
"nested": map[string]any{ //nolint:gosec // test fixture
"token": "nested-token",
"secretKey": "nested-secret-key",
},
}
conf.Storage.CacheDriver = map[string]any{
"name": "redis",
"password": "cache-password",
}
conf.Storage.SubPaths = map[string]config.StorageConfig{
"/team-a": {
StorageDriver: map[string]any{
"name": "s3",
"access_key": "subpath-access",
"secret_key": "subpath-secret",
},
CacheDriver: map[string]any{ //nolint:gosec // test fixture
"name": "memory",
"token": "subpath-cache-token",
},
},
}
conf.HTTP.Auth = &config.AuthConfig{
SessionDriver: map[string]any{
"name": "redis",
"password": "session-password",
"token": "session-token",
},
}
sanitizedConf := conf.Sanitize()
So(sanitizedConf.Storage.StorageDriver["name"], ShouldEqual, "s3")
So(sanitizedConf.Storage.StorageDriver["accesskey"], ShouldEqual, "******")
So(sanitizedConf.Storage.StorageDriver["secretkey"], ShouldEqual, "******")
nestedMap, ok := sanitizedConf.Storage.StorageDriver["nested"].(map[string]any)
So(ok, ShouldBeTrue)
So(nestedMap["token"], ShouldEqual, "******")
So(nestedMap["secretKey"], ShouldEqual, "******")
So(sanitizedConf.Storage.CacheDriver["password"], ShouldEqual, "******")
So(sanitizedConf.Storage.SubPaths["/team-a"].StorageDriver["access_key"], ShouldEqual, "******")
So(sanitizedConf.Storage.SubPaths["/team-a"].StorageDriver["secret_key"], ShouldEqual, "******")
So(sanitizedConf.Storage.SubPaths["/team-a"].CacheDriver["token"], ShouldEqual, "******")
So(sanitizedConf.HTTP.Auth.SessionDriver["name"], ShouldEqual, "redis")
So(sanitizedConf.HTTP.Auth.SessionDriver["password"], ShouldEqual, "******")
So(sanitizedConf.HTTP.Auth.SessionDriver["token"], ShouldEqual, "******")
So(conf.Storage.StorageDriver["accesskey"], ShouldEqual, "storage-access")
So(conf.Storage.StorageDriver["secretkey"], ShouldEqual, "storage-secret")
So(conf.Storage.CacheDriver["password"], ShouldEqual, "cache-password")
So(conf.Storage.SubPaths["/team-a"].StorageDriver["access_key"], ShouldEqual, "subpath-access")
So(conf.Storage.SubPaths["/team-a"].StorageDriver["secret_key"], ShouldEqual, "subpath-secret")
So(conf.Storage.SubPaths["/team-a"].CacheDriver["token"], ShouldEqual, "subpath-cache-token")
So(conf.HTTP.Auth.SessionDriver["password"], ShouldEqual, "session-password")
So(conf.HTTP.Auth.SessionDriver["token"], ShouldEqual, "session-token")
})
Convey("Test Sanitize() with Event sink credentials including nil credentials", func() {
@@ -382,8 +460,8 @@ func TestConfig(t *testing.T) {
So(sanitizedConf.HTTP.Auth.LDAP.BindPassword(), ShouldEqual, "")
// OpenID empty secret is always sanitized
So(sanitizedConf.HTTP.Auth.OpenID.Providers["empty"].ClientSecret, ShouldEqual, "******")
// Event sink empty password is always sanitized
So(sanitizedConf.Extensions.Events.Sinks[0].Credentials.Password, ShouldEqual, "******")
// Event sink empty password remains empty when no credential value is present
So(sanitizedConf.Extensions.Events.Sinks[0].Credentials.Password, ShouldEqual, "")
})
Convey("Test Sanitize() with nil config", func() {
+2 -2
View File
@@ -3191,10 +3191,10 @@ func TestCreateRedisDB(t *testing.T) {
})
Convey("Fails on Ping()", func() {
// Redis client will not be responding
// Use a guaranteed-invalid endpoint to avoid free-port races in CI.
cacheDriverParams := map[string]any{
"name": "redis",
"url": "redis://127.0.0.1:" + tCommon.GetFreePort(),
"url": "redis://127.0.0.1:0",
}
conf.Storage.CacheDriver = cacheDriverParams