Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,13 @@ REDIS_PORT=6379
REDIS_PASSWORD=difyai123456
REDIS_DB=0

# redis TLS
REDIS_USE_SSL=false
## CERT_NONE | CERT_OPTIONAL | CERT_REQUIRED
#REDIS_SSL_CERT_REQS=CERT_REQUIRED
## Optional custom CA bundle (PEM) if not using public CAs
#REDIS_SSL_CA_CERTS=

# Whether to use Redis Sentinel mode.
# If set to true, the application will automatically discover and connect to the master node through Sentinel.
REDIS_USE_SENTINEL=false
Expand Down
8 changes: 8 additions & 0 deletions internal/core/plugin_manager/manager.go
Original file line number Diff line number Diff line change
Expand Up @@ -113,6 +113,12 @@ func (p *PluginManager) GetAsset(id string) ([]byte, error) {
func (p *PluginManager) Launch(configuration *app.Config) {
log.Info("start plugin manager daemon...")

// Build TLS config for Redis (nil when RedisUseSsl=false)
tlsConf, err := configuration.RedisTLSConfig()
if err != nil {
log.Panic("invalid Redis TLS config: %s", err.Error())
}

// init redis client
if configuration.RedisUseSentinel {
// use Redis Sentinel
Expand All @@ -127,6 +133,7 @@ func (p *PluginManager) Launch(configuration *app.Config) {
configuration.RedisUseSsl,
configuration.RedisDB,
configuration.RedisSentinelSocketTimeout,
tlsConf, // pass TLS to cache initializer
); err != nil {
log.Panic("init redis sentinel client failed: %s", err.Error())
}
Expand All @@ -137,6 +144,7 @@ func (p *PluginManager) Launch(configuration *app.Config) {
configuration.RedisPass,
configuration.RedisUseSsl,
configuration.RedisDB,
tlsConf, // pass TLS to cache initializer
); err != nil {
log.Panic("init redis client failed: %s", err.Error())
}
Expand Down
44 changes: 44 additions & 0 deletions internal/types/app/config.go
Original file line number Diff line number Diff line change
@@ -1,7 +1,11 @@
package app

import (
"crypto/tls"
"crypto/x509"
"fmt"
"os"
"strings"

"github.com/go-playground/validator/v10"
)
Expand Down Expand Up @@ -109,6 +113,10 @@ type Config struct {
RedisUseSsl bool `envconfig:"REDIS_USE_SSL"`
RedisDB int `envconfig:"REDIS_DB"`

// redis TLS extras
RedisSSLCertReqs string `envconfig:"REDIS_SSL_CERT_REQS" default:"CERT_REQUIRED"`
RedisSSLCACerts string `envconfig:"REDIS_SSL_CA_CERTS"`

// redis sentinel
RedisUseSentinel bool `envconfig:"REDIS_USE_SENTINEL"`
RedisSentinels string `envconfig:"REDIS_SENTINELS"`
Expand Down Expand Up @@ -253,6 +261,42 @@ func (c *Config) Validate() error {
return nil
}

// RedisTLSConfig builds a *tls.Config for Redis based on envs.
func (c *Config) RedisTLSConfig() (*tls.Config, error) {
if !c.RedisUseSsl {
return nil, nil
}

tlsConf := &tls.Config{
MinVersion: tls.VersionTLS12,
}

if strings.TrimSpace(c.RedisSSLCACerts) != "" {
pem, err := os.ReadFile(c.RedisSSLCACerts)
if err != nil {
return nil, fmt.Errorf("read REDIS_SSL_CA_CERTS: %w", err)
}
pool := x509.NewCertPool()
if !pool.AppendCertsFromPEM(pem) {
return nil, fmt.Errorf("failed to append CA certs from %s", c.RedisSSLCACerts)
}
tlsConf.RootCAs = pool
}

switch strings.ToUpper(strings.TrimSpace(c.RedisSSLCertReqs)) {
case "CERT_NONE":
tlsConf.InsecureSkipVerify = true
case "CERT_OPTIONAL":
tlsConf.InsecureSkipVerify = false
case "CERT_REQUIRED", "":
tlsConf.InsecureSkipVerify = false
default:
tlsConf.InsecureSkipVerify = false
}
Comment on lines +286 to +295
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

The handling of REDIS_SSL_CERT_REQS has a couple of issues:

  1. CERT_OPTIONAL is implemented identically to CERT_REQUIRED (InsecureSkipVerify = false). This is misleading, as CERT_OPTIONAL typically implies that certificate verification is attempted but not enforced. Go's crypto/tls package does not have a direct equivalent for this behavior. To avoid confusion, it would be better to explicitly disallow this value.
  2. The default case silently treats any invalid value as CERT_REQUIRED. This can hide misconfigurations. It would be safer to return an error for unknown values.

I suggest refactoring this switch statement to be stricter and clearer.

switch strings.ToUpper(strings.TrimSpace(c.RedisSSLCertReqs)) {
	case "CERT_NONE":
		tlsConf.InsecureSkipVerify = true
	case "CERT_REQUIRED", "":
		tlsConf.InsecureSkipVerify = false
	default:
		return nil, fmt.Errorf("invalid value for REDIS_SSL_CERT_REQS: %q. Supported values are 'CERT_NONE', 'CERT_REQUIRED'", c.RedisSSLCertReqs)
	}

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hi @Oak10, I'd like to follow the suggestions, Let's me know your thoughts


return tlsConf, nil
}

type PlatformType string

const (
Expand Down
42 changes: 25 additions & 17 deletions internal/utils/cache/redis.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,31 +20,42 @@ var (
ErrNotFound = errors.New("key not found")
)

func getRedisOptions(addr, username, password string, useSsl bool, db int) *redis.Options {
func getRedisOptions(addr, username, password string, useSsl bool, db int, tlsConf *tls.Config) *redis.Options {
opts := &redis.Options{
Addr: addr,
Username: username,
Password: password,
DB: db,
}
if useSsl {
opts.TLSConfig = &tls.Config{}
// Use provided TLS config (encodes CERT_NONE / REQUIRED policy). If nil, default to system roots.
if tlsConf != nil {
opts.TLSConfig = tlsConf
} else {
opts.TLSConfig = &tls.Config{}
}
}
return opts
}

func InitRedisClient(addr, username, password string, useSsl bool, db int) error {
opts := getRedisOptions(addr, username, password, useSsl, db)
func InitRedisClient(addr, username, password string, useSsl bool, db int, tlsConf *tls.Config) error {
opts := getRedisOptions(addr, username, password, useSsl, db, tlsConf)
client = redis.NewClient(opts)

if _, err := client.Ping(ctx).Result(); err != nil {
return err
}

return nil
}

func InitRedisSentinelClient(sentinels []string, masterName, username, password, sentinelUsername, sentinelPassword string, useSsl bool, db int, socketTimeout float64) error {
func InitRedisSentinelClient(
sentinels []string,
masterName, username, password, sentinelUsername, sentinelPassword string,
useSsl bool,
db int,
socketTimeout float64,
tlsConf *tls.Config,
) error {
opts := &redis.FailoverOptions{
MasterName: masterName,
SentinelAddrs: sentinels,
Expand All @@ -56,7 +67,12 @@ func InitRedisSentinelClient(sentinels []string, masterName, username, password,
}

if useSsl {
opts.TLSConfig = &tls.Config{}
// go-redis v9 uses TLSConfig for both Sentinel discovery and data connections
if tlsConf != nil {
opts.TLSConfig = tlsConf
} else {
opts.TLSConfig = &tls.Config{}
}
}

if socketTimeout > 0 {
Expand All @@ -68,7 +84,6 @@ func InitRedisSentinelClient(sentinels []string, masterName, username, password,
if _, err := client.Ping(ctx).Result(); err != nil {
return err
}

return nil
}

Expand All @@ -77,15 +92,13 @@ func Close() error {
if client == nil {
return ErrDBNotInit
}

return client.Close()
}

func getCmdable(context ...redis.Cmdable) redis.Cmdable {
if len(context) > 0 {
return context[0]
}

return client
}

Expand Down Expand Up @@ -365,13 +378,12 @@ func ScanMap[V any](key string, match string, context ...redis.Cmdable) (map[str

result := make(map[string]V)

ScanMapAsync[V](key, match, func(m map[string]V) error {
_ = ScanMapAsync[V](key, match, func(m map[string]V) error {
for k, v := range m {
result[k] = v
}

return nil
})
}, context...)

return result, nil
Comment on lines +381 to 388
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

This change correctly passes the context... to ScanMapAsync, which was a bug. However, it also explicitly ignores the error returned by ScanMapAsync by assigning it to _.

The ScanMapAsync function can return errors from the underlying Redis HSCAN command. By ignoring this error, ScanMap might return a partially populated map or an empty map without signaling that an error occurred.

The function signature of ScanMap is (map[string]V, error), which implies that errors should be propagated. The error should be handled and returned to the caller.

	if err := ScanMapAsync[V](key, match, func(m map[string]V) error {
		for k, v := range m {
			result[k] = v
		}
		return nil
	}, context...); err != nil {
		return nil, err
	}

	return result, nil

}
Expand All @@ -388,7 +400,6 @@ func ScanMapAsync[V any](key string, match string, fn func(map[string]V) error,
kvs, newCursor, err := getCmdable(context...).
HScan(ctx, serialKey(key), cursor, match, 32).
Result()

if err != nil {
return err
}
Expand All @@ -399,7 +410,6 @@ func ScanMapAsync[V any](key string, match string, fn func(map[string]V) error,
if err != nil {
continue
}

result[kvs[i]] = value
}

Expand All @@ -410,7 +420,6 @@ func ScanMapAsync[V any](key string, match string, fn func(map[string]V) error,
if newCursor == 0 {
break
}

cursor = newCursor
}

Expand Down Expand Up @@ -533,7 +542,6 @@ func Subscribe[T any](channel string) (<-chan T, func()) {
if err != nil {
continue
}

ch <- v
case *redis.Pong:
default:
Expand Down