From 547cded8c68c92700cca9a23c50eeac5e13d21bc Mon Sep 17 00:00:00 2001 From: lani_karrot Date: Wed, 8 Jul 2026 17:13:03 +0900 Subject: [PATCH 1/6] docs: druid.cache.enableTls --- docs/development/extensions-contrib/redis-cache.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/development/extensions-contrib/redis-cache.md b/docs/development/extensions-contrib/redis-cache.md index 63e0b9e509c7..8f29bac7c03f 100644 --- a/docs/development/extensions-contrib/redis-cache.md +++ b/docs/development/extensions-contrib/redis-cache.md @@ -84,6 +84,7 @@ Except for the properties above, there are some extra properties which can be cu | Properties |Description|Default|Required| |--------------------|-----------|-------|--------| |`druid.cache.password`| Password to access redis server/cluster | None |no| +|`druid.cache.enableTls`| Whether to connect to the redis server/cluster over TLS. Uses the JVM default trust store and hostname verification | false |no| |`druid.cache.expiration`|Expiration for cache entries | P1D |no| |`druid.cache.timeout`|Timeout for connecting to Redis and reading entries from Redis|PT2S|no| |`druid.cache.maxTotalConnections`|Max total connections to Redis|8|no| From 85ee77a9132af57cab0f14b17481a77710ab8836 Mon Sep 17 00:00:00 2001 From: lani_karrot Date: Wed, 8 Jul 2026 17:39:00 +0900 Subject: [PATCH 2/6] feat: TLS for redis --- .../druid/client/cache/RedisCacheConfig.java | 11 +++++ .../druid/client/cache/RedisCacheFactory.java | 48 +++++++++---------- .../client/cache/RedisCacheConfigTest.java | 18 +++++++ 3 files changed, 53 insertions(+), 24 deletions(-) diff --git a/extensions-contrib/redis-cache/src/main/java/org/apache/druid/client/cache/RedisCacheConfig.java b/extensions-contrib/redis-cache/src/main/java/org/apache/druid/client/cache/RedisCacheConfig.java index 4f36ea8383b3..af4f88f7df84 100644 --- a/extensions-contrib/redis-cache/src/main/java/org/apache/druid/client/cache/RedisCacheConfig.java +++ b/extensions-contrib/redis-cache/src/main/java/org/apache/druid/client/cache/RedisCacheConfig.java @@ -144,6 +144,12 @@ public long getSeconds() @Min(0) private int database = Protocol.DEFAULT_DATABASE; + /** + * whether to connect to redis server/cluster over TLS. + */ + @JsonProperty + private boolean enableTls = false; + @JsonProperty private RedisClusterConfig cluster; @@ -196,4 +202,9 @@ public int getDatabase() { return database; } + + public boolean getEnableTls() + { + return enableTls; + } } diff --git a/extensions-contrib/redis-cache/src/main/java/org/apache/druid/client/cache/RedisCacheFactory.java b/extensions-contrib/redis-cache/src/main/java/org/apache/druid/client/cache/RedisCacheFactory.java index 2ac5cbad9e09..df08406891d4 100644 --- a/extensions-contrib/redis-cache/src/main/java/org/apache/druid/client/cache/RedisCacheFactory.java +++ b/extensions-contrib/redis-cache/src/main/java/org/apache/druid/client/cache/RedisCacheFactory.java @@ -22,7 +22,9 @@ import org.apache.commons.lang3.StringUtils; import org.apache.druid.java.util.common.IAE; import redis.clients.jedis.ConnectionPoolConfig; +import redis.clients.jedis.DefaultJedisClientConfig; import redis.clients.jedis.HostAndPort; +import redis.clients.jedis.JedisClientConfig; import redis.clients.jedis.JedisCluster; import redis.clients.jedis.JedisPool; import redis.clients.jedis.JedisPoolConfig; @@ -65,24 +67,12 @@ public static Cache create(final RedisCacheConfig config) poolConfig.setMaxIdle(config.getMaxIdleConnections()); poolConfig.setMinIdle(config.getMinIdleConnections()); - JedisCluster cluster; - if (config.getPassword() != null) { - cluster = new JedisCluster( - nodes, - config.getTimeout().getMillisecondsAsInt(), //connection timeout - config.getTimeout().getMillisecondsAsInt(), //read timeout - config.getCluster().getMaxRedirection(), - config.getPassword().getPassword(), - poolConfig - ); - } else { - cluster = new JedisCluster( - nodes, - config.getTimeout().getMillisecondsAsInt(), //connection timeout and read timeout - config.getCluster().getMaxRedirection(), - poolConfig - ); - } + JedisCluster cluster = new JedisCluster( + nodes, + buildClientConfig(config), + config.getCluster().getMaxRedirection(), + poolConfig + ); return new RedisClusterCache(cluster, config); @@ -100,15 +90,25 @@ public static Cache create(final RedisCacheConfig config) return new RedisStandaloneCache( new JedisPool( poolConfig, - config.getHost(), - config.getPort(), - config.getTimeout().getMillisecondsAsInt(), //connection timeout and read timeout - config.getPassword() == null ? null : config.getPassword().getPassword(), - config.getDatabase(), - null + new HostAndPort(config.getHost(), config.getPort()), + buildClientConfig(config) ), config ); } } + + private static JedisClientConfig buildClientConfig(RedisCacheConfig config) + { + return DefaultJedisClientConfig + .builder() + .connectionTimeoutMillis(config.getTimeout().getMillisecondsAsInt()) + .socketTimeoutMillis(config.getTimeout().getMillisecondsAsInt()) + .password(config.getPassword() == null ? null : config.getPassword().getPassword()) + // database applies to standalone only; Redis Cluster supports database 0 only, so this + // is a no-op in cluster mode (the default is 0). + .database(config.getDatabase()) + .ssl(config.getEnableTls()) + .build(); + } } diff --git a/extensions-contrib/redis-cache/src/test/java/org/apache/druid/client/cache/RedisCacheConfigTest.java b/extensions-contrib/redis-cache/src/test/java/org/apache/druid/client/cache/RedisCacheConfigTest.java index d1860636348d..3cbddaff4484 100644 --- a/extensions-contrib/redis-cache/src/test/java/org/apache/druid/client/cache/RedisCacheConfigTest.java +++ b/extensions-contrib/redis-cache/src/test/java/org/apache/druid/client/cache/RedisCacheConfigTest.java @@ -232,4 +232,22 @@ public void testNoClusterAndHost() throws IOException )); RedisCacheFactory.create(fromJson); } + + @Test + public void testEnableTls() throws IOException + { + ObjectMapper mapper = new ObjectMapper(); + + RedisCacheConfig defaultConfig = mapper.readValue( + "{\"host\": \"localhost\", \"port\": 6379}", + RedisCacheConfig.class + ); + Assert.assertFalse(defaultConfig.getEnableTls()); + + RedisCacheConfig tlsConfig = mapper.readValue( + "{\"host\": \"localhost\", \"port\": 6379, \"enableTls\": true}", + RedisCacheConfig.class + ); + Assert.assertTrue(tlsConfig.getEnableTls()); + } } From 48c7db8b7de17760755e0d8ea0c01adb08cbeedd Mon Sep 17 00:00:00 2001 From: lani_karrot Date: Wed, 8 Jul 2026 17:41:53 +0900 Subject: [PATCH 3/6] docs: update description --- docs/development/extensions-contrib/redis-cache.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/development/extensions-contrib/redis-cache.md b/docs/development/extensions-contrib/redis-cache.md index 8f29bac7c03f..61aa6e27bd6d 100644 --- a/docs/development/extensions-contrib/redis-cache.md +++ b/docs/development/extensions-contrib/redis-cache.md @@ -84,7 +84,7 @@ Except for the properties above, there are some extra properties which can be cu | Properties |Description|Default|Required| |--------------------|-----------|-------|--------| |`druid.cache.password`| Password to access redis server/cluster | None |no| -|`druid.cache.enableTls`| Whether to connect to the redis server/cluster over TLS. Uses the JVM default trust store and hostname verification | false |no| +|`druid.cache.enableTls`| Whether to connect to the redis server/cluster over TLS | false |no| |`druid.cache.expiration`|Expiration for cache entries | P1D |no| |`druid.cache.timeout`|Timeout for connecting to Redis and reading entries from Redis|PT2S|no| |`druid.cache.maxTotalConnections`|Max total connections to Redis|8|no| From 2bf334aa85c00751959c08a3cb464f447cc490ef Mon Sep 17 00:00:00 2001 From: lani_karrot Date: Thu, 9 Jul 2026 01:02:20 +0900 Subject: [PATCH 4/6] feat: skip tls hostname verification --- docs/development/extensions-contrib/redis-cache.md | 1 + .../apache/druid/client/cache/RedisCacheConfig.java | 12 ++++++++++++ 2 files changed, 13 insertions(+) diff --git a/docs/development/extensions-contrib/redis-cache.md b/docs/development/extensions-contrib/redis-cache.md index 61aa6e27bd6d..c1c76fe74883 100644 --- a/docs/development/extensions-contrib/redis-cache.md +++ b/docs/development/extensions-contrib/redis-cache.md @@ -85,6 +85,7 @@ Except for the properties above, there are some extra properties which can be cu |--------------------|-----------|-------|--------| |`druid.cache.password`| Password to access redis server/cluster | None |no| |`druid.cache.enableTls`| Whether to connect to the redis server/cluster over TLS | false |no| +|`druid.cache.skipTlsHostnameVerification`| When TLS is enabled, skip verifying that the server certificate matches the redis hostname | false |no| |`druid.cache.expiration`|Expiration for cache entries | P1D |no| |`druid.cache.timeout`|Timeout for connecting to Redis and reading entries from Redis|PT2S|no| |`druid.cache.maxTotalConnections`|Max total connections to Redis|8|no| diff --git a/extensions-contrib/redis-cache/src/main/java/org/apache/druid/client/cache/RedisCacheConfig.java b/extensions-contrib/redis-cache/src/main/java/org/apache/druid/client/cache/RedisCacheConfig.java index af4f88f7df84..522b0677468a 100644 --- a/extensions-contrib/redis-cache/src/main/java/org/apache/druid/client/cache/RedisCacheConfig.java +++ b/extensions-contrib/redis-cache/src/main/java/org/apache/druid/client/cache/RedisCacheConfig.java @@ -150,6 +150,13 @@ public long getSeconds() @JsonProperty private boolean enableTls = false; + /** + * whether to skip verifying that the server certificate matches the redis hostname when TLS is + * enabled. defaults to false (the hostname is verified) + */ + @JsonProperty + private boolean skipTlsHostnameVerification = false; + @JsonProperty private RedisClusterConfig cluster; @@ -207,4 +214,9 @@ public boolean getEnableTls() { return enableTls; } + + public boolean getSkipTlsHostnameVerification() + { + return skipTlsHostnameVerification; + } } From 603f1cd4fd7843e2f7882bee3669f7b2758b2d77 Mon Sep 17 00:00:00 2001 From: lani_karrot Date: Thu, 9 Jul 2026 01:02:51 +0900 Subject: [PATCH 5/6] fix: avoid using database from user input when cluster mode --- .../druid/client/cache/RedisCacheFactory.java | 25 ++++++++++++------- .../client/cache/RedisCacheConfigTest.java | 18 +++++++++++++ 2 files changed, 34 insertions(+), 9 deletions(-) diff --git a/extensions-contrib/redis-cache/src/main/java/org/apache/druid/client/cache/RedisCacheFactory.java b/extensions-contrib/redis-cache/src/main/java/org/apache/druid/client/cache/RedisCacheFactory.java index df08406891d4..d3da12d5bfc6 100644 --- a/extensions-contrib/redis-cache/src/main/java/org/apache/druid/client/cache/RedisCacheFactory.java +++ b/extensions-contrib/redis-cache/src/main/java/org/apache/druid/client/cache/RedisCacheFactory.java @@ -28,6 +28,8 @@ import redis.clients.jedis.JedisCluster; import redis.clients.jedis.JedisPool; import redis.clients.jedis.JedisPoolConfig; +import redis.clients.jedis.SslOptions; +import redis.clients.jedis.SslVerifyMode; import java.util.Arrays; import java.util.Set; @@ -69,7 +71,7 @@ public static Cache create(final RedisCacheConfig config) JedisCluster cluster = new JedisCluster( nodes, - buildClientConfig(config), + buildClientConfig(config, 0), config.getCluster().getMaxRedirection(), poolConfig ); @@ -91,24 +93,29 @@ public static Cache create(final RedisCacheConfig config) new JedisPool( poolConfig, new HostAndPort(config.getHost(), config.getPort()), - buildClientConfig(config) + buildClientConfig(config, config.getDatabase()) ), config ); } } - private static JedisClientConfig buildClientConfig(RedisCacheConfig config) + private static JedisClientConfig buildClientConfig(RedisCacheConfig config, int database) { - return DefaultJedisClientConfig + DefaultJedisClientConfig.Builder builder = DefaultJedisClientConfig .builder() .connectionTimeoutMillis(config.getTimeout().getMillisecondsAsInt()) .socketTimeoutMillis(config.getTimeout().getMillisecondsAsInt()) .password(config.getPassword() == null ? null : config.getPassword().getPassword()) - // database applies to standalone only; Redis Cluster supports database 0 only, so this - // is a no-op in cluster mode (the default is 0). - .database(config.getDatabase()) - .ssl(config.getEnableTls()) - .build(); + .database(database) + .ssl(config.getEnableTls()); + + if (config.getEnableTls()) { + SslVerifyMode verifyMode = + config.getSkipTlsHostnameVerification() ? SslVerifyMode.CA : SslVerifyMode.FULL; + builder.sslOptions(SslOptions.builder().sslVerifyMode(verifyMode).build()); + } + + return builder.build(); } } diff --git a/extensions-contrib/redis-cache/src/test/java/org/apache/druid/client/cache/RedisCacheConfigTest.java b/extensions-contrib/redis-cache/src/test/java/org/apache/druid/client/cache/RedisCacheConfigTest.java index 3cbddaff4484..bf585af70c5b 100644 --- a/extensions-contrib/redis-cache/src/test/java/org/apache/druid/client/cache/RedisCacheConfigTest.java +++ b/extensions-contrib/redis-cache/src/test/java/org/apache/druid/client/cache/RedisCacheConfigTest.java @@ -250,4 +250,22 @@ public void testEnableTls() throws IOException ); Assert.assertTrue(tlsConfig.getEnableTls()); } + + @Test + public void testSkipTlsHostnameVerification() throws IOException + { + ObjectMapper mapper = new ObjectMapper(); + + RedisCacheConfig defaultConfig = mapper.readValue( + "{\"host\": \"localhost\", \"port\": 6379}", + RedisCacheConfig.class + ); + Assert.assertFalse(defaultConfig.getSkipTlsHostnameVerification()); + + RedisCacheConfig skipConfig = mapper.readValue( + "{\"host\": \"localhost\", \"port\": 6379, \"skipTlsHostnameVerification\": true}", + RedisCacheConfig.class + ); + Assert.assertTrue(skipConfig.getSkipTlsHostnameVerification()); + } } From 9644a9b9d8ef5a8fba11d83e5abfad60edf5fafa Mon Sep 17 00:00:00 2001 From: lani_karrot Date: Thu, 9 Jul 2026 15:26:25 +0900 Subject: [PATCH 6/6] test: coverage --- .../druid/client/cache/RedisCacheFactory.java | 4 +- .../client/cache/RedisCacheConfigTest.java | 55 +++++++++++++++++++ 2 files changed, 58 insertions(+), 1 deletion(-) diff --git a/extensions-contrib/redis-cache/src/main/java/org/apache/druid/client/cache/RedisCacheFactory.java b/extensions-contrib/redis-cache/src/main/java/org/apache/druid/client/cache/RedisCacheFactory.java index d3da12d5bfc6..2efe59b02928 100644 --- a/extensions-contrib/redis-cache/src/main/java/org/apache/druid/client/cache/RedisCacheFactory.java +++ b/extensions-contrib/redis-cache/src/main/java/org/apache/druid/client/cache/RedisCacheFactory.java @@ -19,6 +19,7 @@ package org.apache.druid.client.cache; +import com.google.common.annotations.VisibleForTesting; import org.apache.commons.lang3.StringUtils; import org.apache.druid.java.util.common.IAE; import redis.clients.jedis.ConnectionPoolConfig; @@ -100,7 +101,8 @@ public static Cache create(final RedisCacheConfig config) } } - private static JedisClientConfig buildClientConfig(RedisCacheConfig config, int database) + @VisibleForTesting + static JedisClientConfig buildClientConfig(RedisCacheConfig config, int database) { DefaultJedisClientConfig.Builder builder = DefaultJedisClientConfig .builder() diff --git a/extensions-contrib/redis-cache/src/test/java/org/apache/druid/client/cache/RedisCacheConfigTest.java b/extensions-contrib/redis-cache/src/test/java/org/apache/druid/client/cache/RedisCacheConfigTest.java index bf585af70c5b..ecbe5cfd051d 100644 --- a/extensions-contrib/redis-cache/src/test/java/org/apache/druid/client/cache/RedisCacheConfigTest.java +++ b/extensions-contrib/redis-cache/src/test/java/org/apache/druid/client/cache/RedisCacheConfigTest.java @@ -24,12 +24,15 @@ import com.github.fppt.jedismock.RedisServer; import com.github.fppt.jedismock.server.ServiceOptions; import org.apache.druid.java.util.common.IAE; +import org.apache.druid.metadata.PasswordProvider; import org.hamcrest.Description; import org.hamcrest.Matcher; import org.junit.Assert; import org.junit.Rule; import org.junit.Test; import org.junit.rules.ExpectedException; +import redis.clients.jedis.JedisClientConfig; +import redis.clients.jedis.SslVerifyMode; import java.io.IOException; @@ -268,4 +271,56 @@ public void testSkipTlsHostnameVerification() throws IOException ); Assert.assertTrue(skipConfig.getSkipTlsHostnameVerification()); } + + @Test + public void testBuildClientConfig() + { + // TLS disabled: not SSL, no SSL options, database argument passed through, null password. + JedisClientConfig plain = RedisCacheFactory.buildClientConfig(new RedisCacheConfig(), 3); + Assert.assertFalse(plain.isSsl()); + Assert.assertNull(plain.getSslOptions()); + Assert.assertEquals(3, plain.getDatabase()); + Assert.assertNull(plain.getPassword()); + + // TLS enabled with hostname verification (default) maps to SslVerifyMode.FULL, and a + // non-null password is forwarded. + RedisCacheConfig verifyConfig = new RedisCacheConfig() + { + @Override + public boolean getEnableTls() + { + return true; + } + + @Override + public PasswordProvider getPassword() + { + return () -> "secret"; + } + }; + JedisClientConfig verify = RedisCacheFactory.buildClientConfig(verifyConfig, 0); + Assert.assertTrue(verify.isSsl()); + Assert.assertEquals(SslVerifyMode.FULL, verify.getSslOptions().getSslVerifyMode()); + Assert.assertEquals("secret", verify.getPassword()); + + // skipTlsHostnameVerification maps to SslVerifyMode.CA (chain verified, hostname skipped). + RedisCacheConfig skipConfig = new RedisCacheConfig() + { + @Override + public boolean getEnableTls() + { + return true; + } + + @Override + public boolean getSkipTlsHostnameVerification() + { + return true; + } + }; + Assert.assertEquals( + SslVerifyMode.CA, + RedisCacheFactory.buildClientConfig(skipConfig, 0).getSslOptions().getSslVerifyMode() + ); + } }