mirror of
https://github.com/project-zot/zot.git
synced 2026-06-17 21:17:58 +08:00
fix: redact sensitive values in sanitized startup config logs
This commit is contained in:
committed by
GitHub
parent
44894652e3
commit
cea72a4c08
@@ -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 {
|
||||
|
||||
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user