Repository: ContentSquare/chproxy

Issue:
[BUG] Elastic cache TLS configuration
**Describe the bug**
Cannot connect to AWS elasticcache endpoint (redis OSS caches)
the output is 
```bash
INFO: 2025/08/01 11:48:24 main.go:50: chproxy ver. 1.26.6, rev. f3e128843392c502dd82709f073af73bb5b56d6c, built at 2024-11-21T11:20:35Z
INFO: 2025/08/01 11:48:24 main.go:51: Loading config: /etc/chproxy/config.yaml
FATAL: 2025/08/01 11:48:27 main.go:58: error while applying config: failed to reach redis: i/o timeout
```

**To Reproduce**
create chproxy config and TLS redis endpoint

**Expected behavior**
A clear and concise description of what you expected to happen.

**Environment information**
chproxy running in EKS based on 1.26.6

configuration for cache block is 
```yaml
    caches:
      - name: "redis"
        mode: "redis"
        redis:
          addresses:
            - "${ELASTIC_HOST}"
          password: ${ELASTIC_TOKEN}
          insecure_skip_verify: false
        expire: 10m
```
Tried adding `tls: true`

**Additional context**
I ran tcpdump on the same host and was able to see all data (token) in plain text, meaning tls configuration doesn't work.
I used to have redis-cache as a stateful set in the same namespace without TLS. worked perfectly.


Hints from issue comments:
@sbulatnikov: Absolutely the same issue we faced today
@mga-chka is it possible to figure out how to establish secure connection to AWS ElastiCache with default TLS options with current configuration of Chproxy?@hileef: Hello @hornv , @sbulatnikov , 

Starting with CHProxy `v1.30.0` , you can now specify the `enable_tls` flag in order to turn on TLS support on the redis connection without a client cert & key pair.

In other words, the configuration can be adapted as such : 

```yaml
    caches:
      - name: "redis"
        mode: "redis"
        redis:
          addresses:
            - "${ELASTIC_HOST}"
          password: ${ELASTIC_TOKEN}
          enable_tls: true
          insecure_skip_verify: false
        expire: 10m
```

If you experience additional issues regarding TLS conns with redis ( or elasticache ),
please let us know so we re-open the issue 🙏 

Solution patch:
diff --git a/clients/redis.go b/clients/redis.go
index 3dbe3f4..5ed4d94 100644
--- a/clients/redis.go
+++ b/clients/redis.go
@@ -22,7 +22,8 @@ func NewRedisClient(cfg config.RedisCacheConfig) (redis.UniversalClient, error)
 		options.DB = cfg.DBIndex
 	}
 
-	if len(cfg.CertFile) != 0 || len(cfg.KeyFile) != 0 {
+	// maintain backwards compatibility in case of non-presence of enable_tls
+	if len(cfg.CertFile) != 0 || len(cfg.KeyFile) != 0 || cfg.EnableTLS {
 		tlsConfig, err := cfg.TLS.BuildTLSConfig(nil)
 		if err != nil {
 			return nil, err
diff --git a/config/config.go b/config/config.go
index 3691c79..a8f94c4 100644
--- a/config/config.go
+++ b/config/config.go
@@ -367,12 +367,10 @@ func (c *TLS) BuildTLSConfig(acm *autocert.Manager) (*tls.Config, error) {
 				c.CertFile, c.KeyFile, err)
 		}
 		tlsCfg.Certificates = []tls.Certificate{cert}
-	} else {
-		if acm == nil {
-			return nil, fmt.Errorf("autocert manager is not configured")
-		}
+	} else if acm != nil {
 		tlsCfg.GetCertificate = acm.GetCertificate
 	}
+
 	return &tlsCfg, nil
 }
 
@@ -965,7 +963,8 @@ type FileSystemCacheConfig struct {
 }
 
 type RedisCacheConfig struct {
-	TLS `yaml:",inline"`
+	TLS       `yaml:",inline"`
+	EnableTLS bool `yaml:"enable_tls,omitempty"`
 
 	Username  string                 `yaml:"username,omitempty"`
 	Password  string                 `yaml:"password,omitempty"`
diff --git a/docs/src/content/docs/configuration/default.md b/docs/src/content/docs/configuration/default.md
index b554ba7..1076d72 100644
--- a/docs/src/content/docs/configuration/default.md
+++ b/docs/src/content/docs/configuration/default.md
@@ -67,18 +67,22 @@ caches:
     # Applicable for cache mode: redis
     # You should use multiple addresses only if they all belong to the same redis cluster.
     redis:
-      # Paths to TLS cert and key files for the redis server.
-      # If you change the cert & key files while chproxy is running, you have to restart chproxy so that it loads them.
-      # Triggering a SIGHUP signal won't work as for the rest of the configuration.
-      cert_file: "redis tls cert file path"
-      key_file: "redis tls key file apth"
-      # Allow to skip the verification of the redis server certificate.
-      insecure_skip_verify: true
-
       addresses:
         - "localhost:16379"
       username: "user"
       password: "pass"
+      
+      # TLS: For backwards compatibility, having a non-empty cert_file and key_file also enables TLS configuration.
+      enable_tls: false
+
+      # TLS: Switch to true to disable server certificate validation ( e.g. when using self-signed certificates )
+      insecure_skip_verify: false
+      
+      # TLS: Paths to cert and key file for client-side X.509/mTLS authentication.
+      # Reload is NOT automatic : SIGHUP insufficient, chproxy must be restarted.
+      cert_file: "path to of tls client certificate to present to redis conn"
+      key_file: "path to of tls client cert key to present to redis conn"
+
     expire: 10s
 
 # Optional network lists, might be used as values for `allowed_networks`.



Test patch:
diff --git a/cache/async_cache_test.go b/cache/async_cache_test.go
index aef2ab1..9e29bcc 100644
--- a/cache/async_cache_test.go
+++ b/cache/async_cache_test.go
@@ -299,6 +299,45 @@ func TestAsyncCache_RedisCache_TLS(t *testing.T) {
 	}
 }
 
+func TestAsyncCache_RedisCache_ServerOnlyTLS(t *testing.T) {
+	serverCfg := config.TLS{
+		CertFile: "../testdata/example.com.cert",
+		KeyFile:  "../testdata/example.com.key",
+	}
+
+	clientCfg := config.TLS{
+		InsecureSkipVerify: true,
+	}
+
+	tlsServerConfig, err := serverCfg.BuildTLSConfig(nil)
+	if err != nil {
+		t.Fatalf("could not build tls config: %s", err)
+	}
+	s := miniredis.NewMiniRedis()
+	if err := s.StartTLS(tlsServerConfig); err != nil {
+		t.Fatalf("could not start miniredis: %s", err.Error())
+		// not reached
+	}
+	t.Cleanup(s.Close)
+
+	var redisCfg = config.Cache{
+		Name: "test",
+		Mode: "redis",
+		Redis: config.RedisCacheConfig{
+			EnableTLS: true,
+			TLS:       clientCfg,
+			Addresses: []string{s.Addr()},
+		},
+		Expire:         config.Duration(cacheTTL),
+		MaxPayloadSize: config.ByteSize(100000000),
+	}
+
+	_, err = NewAsyncCache(redisCfg, 1*time.Second)
+	if err != nil {
+		t.Fatalf("could not instanciate redis async cache because of the following error: %s", err.Error())
+	}
+}
+
 func TestAsyncCache_RedisCache_wrong_instantiation(t *testing.T) {
 	var redisCfg = config.Cache{
 		Name: "test",