Skip to content

Commit 2be1f65

Browse files
authored
feat(#450): add Redis SSL/TLS configuration support (#558)
* feat(#450): add Redis SSL/TLS configuration support Add comprehensive SSL/TLS support for Redis connections with configurable certificate verification modes. Introduces new environment variables for SSL configuration including REDIS_USE_SSL, REDIS_SSL_CERT_REQS (supporting CERT_NONE, CERT_OPTIONAL, CERT_REQUIRED), and REDIS_SSL_CA_CERTS for custom CA certificates. Changes: - Add Redis SSL configuration options to .env.example - Implement RedisTLSConfig() method to build tls.Config based on environment settings - Pass TLS config to both standard Redis and Sentinel mode initializers - Support custom CA certificate loading and verification modes - Set minimum TLS version to 1.2 for security - Minor whitespace cleanup in existing config comments This enables secure Redis connections in production environments with flexible certificate verification options. * fix(#450): prevent reference cycle in TLS config and simplify SSL setup - Capture only RootCAs in VerifyConnection closure to avoid retaining entire tlsConf and potential reference cycles - Remove redundant nil checks for tlsConf in Redis client initialization since tlsConf is guaranteed to be non-nil when useSsl is true - Update comments to reflect actual behavior and constraints * fix(#450): improve Redis TLS certificate verification logic for optional certificates * fix(#450): simplify Redis TLS certificate verification logic for optional and required certificates * docs(#450): add note for CA certificate file path in Redis SSL configuration * test(#450): add comprehensive tests for Redis TLS configuration * fix(#450): enhance Redis SSL configuration documentation and enforce CA cert requirement * fix(#450): add nil TLS parameter to InitRedisClient calls in tests Update all InitRedisClient function calls across test files to include the new nil parameter for TLS configuration. This change maintains backward compatibility by explicitly passing nil for TLS settings in non-TLS test scenarios. * fix(#450): add default TLS configuration for Redis client when no tlsConf is provided
1 parent 91c5db6 commit 2be1f65

12 files changed

Lines changed: 544 additions & 33 deletions

File tree

.env.example

Lines changed: 22 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -80,6 +80,26 @@ REDIS_HOST=127.0.0.1
8080
REDIS_PORT=6379
8181
REDIS_PASSWORD=difyai123456
8282
REDIS_DB=0
83+
REDIS_USE_SSL=false
84+
# SSL configuration for Redis (when REDIS_USE_SSL=true)
85+
REDIS_SSL_CERT_REQS=CERT_NONE
86+
# REDIS_SSL_CERT_REQS controls how server certificates are verified:
87+
# - CERT_NONE: Skips all certificate verification (insecure, sets InsecureSkipVerify=true)
88+
# Use only in development/testing environments. This is the default in this example file.
89+
# - CERT_OPTIONAL: Requires valid certificate verification (same as CERT_REQUIRED for client-side TLS)
90+
# CERT_OPTIONAL is treated as CERT_REQUIRED because servers almost always present
91+
# certificates, and the client's choice is whether to validate them or not
92+
# Uses system's default CA certificates if REDIS_SSL_CA_CERTS is not provided
93+
# - CERT_REQUIRED: Requires valid certificate verification (most secure, sets InsecureSkipVerify=false)
94+
# Recommended for production environments
95+
# IMPORTANT: REDIS_SSL_CA_CERTS must be provided, otherwise the application will fail to start
96+
# - Empty string: Behaves like CERT_OPTIONAL (secure, enables verification, but allows system CA certificates)
97+
# This is the default when REDIS_SSL_CERT_REQS is not set
98+
REDIS_SSL_CA_CERTS=
99+
# Path to the CA certificate file for SSL verification, e.g. /path/to/ca.crt
100+
# REQUIRED when REDIS_SSL_CERT_REQS=CERT_REQUIRED
101+
# Optional for CERT_OPTIONAL (uses system's default CA certificates if not provided)
102+
# Ignored when REDIS_SSL_CERT_REQS=CERT_NONE
83103

84104
# Whether to use Redis Sentinel mode.
85105
# If set to true, the application will automatically discover and connect to the master node through Sentinel.
@@ -99,8 +119,8 @@ DB_PASSWORD=difyai123456
99119
DB_HOST=localhost
100120
DB_PORT=5432
101121
DB_DATABASE=dify_plugin
102-
# Specifies the SSL mode for the database connection.
103-
# Possible values include 'disable', 'require', 'verify-ca', and 'verify-full'.
122+
# Specifies the SSL mode for the database connection.
123+
# Possible values include 'disable', 'require', 'verify-ca', and 'verify-full'.
104124
# 'disable' means SSL is not used for the connection.
105125
DB_SSL_MODE=disable
106126
# database connection pool settings

internal/cluster/clutser_test.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ import (
1010
)
1111

1212
func createSimulationCluster(nums int) ([]*Cluster, error) {
13-
err := cache.InitRedisClient("0.0.0.0:6379", "", "difyai123456", false, 0)
13+
err := cache.InitRedisClient("0.0.0.0:6379", "", "difyai123456", false, 0, nil)
1414
if err != nil {
1515
return nil, err
1616
}

internal/core/debugging_runtime/server_test.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -113,7 +113,7 @@ func (n *TestPluginRuntimeNotifier) OnServerShutdown(reason ServerShutdownReason
113113

114114
// TestAcceptConnection tests the acceptance of the connection
115115
func TestAcceptConnection(t *testing.T) {
116-
if cache.InitRedisClient("0.0.0.0:6379", "", "difyai123456", false, 0) != nil {
116+
if cache.InitRedisClient("0.0.0.0:6379", "", "difyai123456", false, 0, nil) != nil {
117117
t.Errorf("failed to init redis client")
118118
return
119119
}
@@ -338,7 +338,7 @@ func TestNoHandleShakeIn10Seconds(t *testing.T) {
338338
}
339339

340340
func TestIncorrectHandshake(t *testing.T) {
341-
if cache.InitRedisClient("0.0.0.0:6379", "", "difyai123456", false, 0) != nil {
341+
if cache.InitRedisClient("0.0.0.0:6379", "", "difyai123456", false, 0, nil) != nil {
342342
t.Errorf("failed to init redis client")
343343
return
344344
}

internal/core/persistence/persistence_test.go

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ import (
1414
)
1515

1616
func TestPersistenceStoreAndLoad(t *testing.T) {
17-
err := cache.InitRedisClient("localhost:6379", "", "difyai123456", false, 0)
17+
err := cache.InitRedisClient("localhost:6379", "", "difyai123456", false, 0, nil)
1818
if err != nil {
1919
t.Fatalf("Failed to init redis client: %v", err)
2020
}
@@ -70,7 +70,7 @@ func TestPersistenceStoreAndLoad(t *testing.T) {
7070
}
7171

7272
func TestPersistenceSaveAndLoadWithLongKey(t *testing.T) {
73-
err := cache.InitRedisClient("localhost:6379", "", "difyai123456", false, 0)
73+
err := cache.InitRedisClient("localhost:6379", "", "difyai123456", false, 0, nil)
7474
assert.Nil(t, err)
7575
defer cache.Close()
7676
db.Init(&app.Config{
@@ -104,7 +104,7 @@ func TestPersistenceSaveAndLoadWithLongKey(t *testing.T) {
104104
}
105105

106106
func TestPersistenceDelete(t *testing.T) {
107-
err := cache.InitRedisClient("localhost:6379", "", "difyai123456", false, 0)
107+
err := cache.InitRedisClient("localhost:6379", "", "difyai123456", false, 0, nil)
108108
assert.Nil(t, err)
109109
defer cache.Close()
110110
db.Init(&app.Config{
@@ -151,7 +151,7 @@ func TestPersistenceDelete(t *testing.T) {
151151
}
152152

153153
func TestPersistencePathTraversal(t *testing.T) {
154-
err := cache.InitRedisClient("localhost:6379", "", "difyai123456", false, 0)
154+
err := cache.InitRedisClient("localhost:6379", "", "difyai123456", false, 0, nil)
155155
if err != nil {
156156
t.Fatalf("Failed to init redis client: %v", err)
157157
}

internal/core/plugin_manager/manager.go

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -102,6 +102,12 @@ func (p *PluginManager) GetAsset(id string) ([]byte, error) {
102102
func (p *PluginManager) Launch(configuration *app.Config) {
103103
log.Info("start plugin manager daemon")
104104

105+
// Build TLS config for Redis (nil when RedisUseSsl=false)
106+
tlsConf, err := configuration.RedisTLSConfig()
107+
if err != nil {
108+
log.Panic("invalid Redis TLS config: %s", err.Error())
109+
}
110+
105111
// init redis client
106112
if configuration.RedisUseSentinel {
107113
// use Redis Sentinel
@@ -116,6 +122,7 @@ func (p *PluginManager) Launch(configuration *app.Config) {
116122
configuration.RedisUseSsl,
117123
configuration.RedisDB,
118124
configuration.RedisSentinelSocketTimeout,
125+
tlsConf, // pass TLS to cache initializer
119126
); err != nil {
120127
log.Panic("init redis sentinel client failed", "error", err)
121128
}
@@ -126,6 +133,7 @@ func (p *PluginManager) Launch(configuration *app.Config) {
126133
configuration.RedisPass,
127134
configuration.RedisUseSsl,
128135
configuration.RedisDB,
136+
tlsConf, // pass TLS to cache initializer
129137
); err != nil {
130138
log.Panic("init redis client failed", "error", err)
131139
}

internal/core/session_manager/session_trace_test.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -63,7 +63,7 @@ func TestGetSessionTraceInMemory(t *testing.T) {
6363
}
6464

6565
func TestGetSessionTraceFromDistributedCache(t *testing.T) {
66-
require.NoError(t, cache.InitRedisClient("127.0.0.1:6379", "", "difyai123456", false, 0))
66+
require.NoError(t, cache.InitRedisClient("127.0.0.1:6379", "", "difyai123456", false, 0, nil))
6767
t.Cleanup(func() {
6868
cache.Close()
6969
})

internal/service/debugging_service/connection_key_test.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ import (
88
)
99

1010
func TestConnectionKey(t *testing.T) {
11-
err := cache.InitRedisClient("0.0.0.0:6379", "", "difyai123456", false, 0)
11+
err := cache.InitRedisClient("0.0.0.0:6379", "", "difyai123456", false, 0, nil)
1212
if err != nil {
1313
t.Errorf("init redis client failed: %v", err)
1414
return

internal/types/app/config.go

Lines changed: 61 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,11 @@
11
package app
22

33
import (
4+
"crypto/tls"
5+
"crypto/x509"
46
"fmt"
7+
"os"
8+
"strings"
59

610
"github.com/go-playground/validator/v10"
711
"github.com/langgenius/dify-plugin-daemon/pkg/entities/plugin_entities"
@@ -69,7 +73,7 @@ type Config struct {
6973
HuaweiOBSAccessKey string `envconfig:"HUAWEI_OBS_ACCESS_KEY"`
7074
HuaweiOBSSecretKey string `envconfig:"HUAWEI_OBS_SECRET_KEY"`
7175
HuaweiOBSServer string `envconfig:"HUAWEI_OBS_SERVER"`
72-
HuaweiOBSPathStyle bool `envconfig:"HUAWEI_OBS_PATH_STYLE" default:"false"`
76+
HuaweiOBSPathStyle bool `envconfig:"HUAWEI_OBS_PATH_STYLE" default:"false"`
7377

7478
// volcengine tos
7579
VolcengineTOSEndpoint string `envconfig:"VOLCENGINE_TOS_ENDPOINT"`
@@ -110,12 +114,14 @@ type Config struct {
110114
RoutinePoolSize int `envconfig:"ROUTINE_POOL_SIZE" validate:"required"`
111115

112116
// redis
113-
RedisHost string `envconfig:"REDIS_HOST"`
114-
RedisPort uint16 `envconfig:"REDIS_PORT"`
115-
RedisPass string `envconfig:"REDIS_PASSWORD"`
116-
RedisUser string `envconfig:"REDIS_USERNAME"`
117-
RedisUseSsl bool `envconfig:"REDIS_USE_SSL"`
118-
RedisDB int `envconfig:"REDIS_DB"`
117+
RedisHost string `envconfig:"REDIS_HOST"`
118+
RedisPort uint16 `envconfig:"REDIS_PORT"`
119+
RedisPass string `envconfig:"REDIS_PASSWORD"`
120+
RedisUser string `envconfig:"REDIS_USERNAME"`
121+
RedisDB int `envconfig:"REDIS_DB"`
122+
RedisUseSsl bool `envconfig:"REDIS_USE_SSL"`
123+
RedisSSLCertReqs string `envconfig:"REDIS_SSL_CERT_REQS"`
124+
RedisSSLCACerts string `envconfig:"REDIS_SSL_CA_CERTS"`
119125

120126
// redis sentinel
121127
RedisUseSentinel bool `envconfig:"REDIS_USE_SENTINEL"`
@@ -282,6 +288,54 @@ func (c *Config) GetLocalRuntimeMaxBufferSize() int {
282288
return c.PluginRuntimeMaxBufferSize
283289
}
284290

291+
// RedisTLSConfig builds a *tls.Config for Redis based on envs.
292+
func (c *Config) RedisTLSConfig() (*tls.Config, error) {
293+
if !c.RedisUseSsl {
294+
return nil, nil
295+
}
296+
297+
tlsConf := &tls.Config{
298+
MinVersion: tls.VersionTLS12,
299+
}
300+
301+
// Load custom CA certificates if provided
302+
if strings.TrimSpace(c.RedisSSLCACerts) != "" {
303+
pem, err := os.ReadFile(c.RedisSSLCACerts)
304+
if err != nil {
305+
return nil, fmt.Errorf("read REDIS_SSL_CA_CERTS: %w", err)
306+
}
307+
pool := x509.NewCertPool()
308+
if !pool.AppendCertsFromPEM(pem) {
309+
return nil, fmt.Errorf("failed to append CA certs from %s", c.RedisSSLCACerts)
310+
}
311+
tlsConf.RootCAs = pool
312+
}
313+
314+
// Configure certificate verification based on REDIS_SSL_CERT_REQS
315+
certReqs := strings.ToUpper(strings.TrimSpace(c.RedisSSLCertReqs))
316+
switch certReqs {
317+
case "CERT_NONE":
318+
// Skip all certificate verification (insecure)
319+
tlsConf.InsecureSkipVerify = true
320+
case "CERT_OPTIONAL", "CERT_REQUIRED", "":
321+
// Require valid certificate verification (default and most secure)
322+
// CERT_OPTIONAL is treated as CERT_REQUIRED for client-side TLS,
323+
// as servers almost always present certificates and the client's
324+
// choice is whether to validate them or not
325+
tlsConf.InsecureSkipVerify = false
326+
327+
// Require CA certs to be explicitly provided when CERT_REQUIRED is set
328+
if certReqs == "CERT_REQUIRED" && strings.TrimSpace(c.RedisSSLCACerts) == "" {
329+
return nil, fmt.Errorf("REDIS_SSL_CA_CERTS must be provided when REDIS_SSL_CERT_REQS is set to CERT_REQUIRED")
330+
}
331+
default:
332+
// Invalid value - return an error instead of silently defaulting
333+
return nil, fmt.Errorf("invalid REDIS_SSL_CERT_REQS value: %s (valid options: CERT_NONE, CERT_OPTIONAL, CERT_REQUIRED)", certReqs)
334+
}
335+
336+
return tlsConf, nil
337+
}
338+
285339
type PlatformType string
286340

287341
const (

0 commit comments

Comments
 (0)