Skip to content

Commit e44c216

Browse files
authored
feat: support redis TLS (#19666)
* docs: druid.cache.enableTls * feat: TLS for redis * docs: update description * feat: skip tls hostname verification * fix: avoid using database from user input when cluster mode * test: coverage
1 parent 3e59af1 commit e44c216

4 files changed

Lines changed: 149 additions & 24 deletions

File tree

docs/development/extensions-contrib/redis-cache.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -84,6 +84,8 @@ Except for the properties above, there are some extra properties which can be cu
8484
| Properties |Description|Default|Required|
8585
|--------------------|-----------|-------|--------|
8686
|`druid.cache.password`| Password to access redis server/cluster | None |no|
87+
|`druid.cache.enableTls`| Whether to connect to the redis server/cluster over TLS | false |no|
88+
|`druid.cache.skipTlsHostnameVerification`| When TLS is enabled, skip verifying that the server certificate matches the redis hostname | false |no|
8789
|`druid.cache.expiration`|Expiration for cache entries | P1D |no|
8890
|`druid.cache.timeout`|Timeout for connecting to Redis and reading entries from Redis|PT2S|no|
8991
|`druid.cache.maxTotalConnections`|Max total connections to Redis|8|no|

extensions-contrib/redis-cache/src/main/java/org/apache/druid/client/cache/RedisCacheConfig.java

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -144,6 +144,19 @@ public long getSeconds()
144144
@Min(0)
145145
private int database = Protocol.DEFAULT_DATABASE;
146146

147+
/**
148+
* whether to connect to redis server/cluster over TLS.
149+
*/
150+
@JsonProperty
151+
private boolean enableTls = false;
152+
153+
/**
154+
* whether to skip verifying that the server certificate matches the redis hostname when TLS is
155+
* enabled. defaults to false (the hostname is verified)
156+
*/
157+
@JsonProperty
158+
private boolean skipTlsHostnameVerification = false;
159+
147160
@JsonProperty
148161
private RedisClusterConfig cluster;
149162

@@ -196,4 +209,14 @@ public int getDatabase()
196209
{
197210
return database;
198211
}
212+
213+
public boolean getEnableTls()
214+
{
215+
return enableTls;
216+
}
217+
218+
public boolean getSkipTlsHostnameVerification()
219+
{
220+
return skipTlsHostnameVerification;
221+
}
199222
}

extensions-contrib/redis-cache/src/main/java/org/apache/druid/client/cache/RedisCacheFactory.java

Lines changed: 33 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -19,13 +19,18 @@
1919

2020
package org.apache.druid.client.cache;
2121

22+
import com.google.common.annotations.VisibleForTesting;
2223
import org.apache.commons.lang3.StringUtils;
2324
import org.apache.druid.java.util.common.IAE;
2425
import redis.clients.jedis.ConnectionPoolConfig;
26+
import redis.clients.jedis.DefaultJedisClientConfig;
2527
import redis.clients.jedis.HostAndPort;
28+
import redis.clients.jedis.JedisClientConfig;
2629
import redis.clients.jedis.JedisCluster;
2730
import redis.clients.jedis.JedisPool;
2831
import redis.clients.jedis.JedisPoolConfig;
32+
import redis.clients.jedis.SslOptions;
33+
import redis.clients.jedis.SslVerifyMode;
2934

3035
import java.util.Arrays;
3136
import java.util.Set;
@@ -65,24 +70,12 @@ public static Cache create(final RedisCacheConfig config)
6570
poolConfig.setMaxIdle(config.getMaxIdleConnections());
6671
poolConfig.setMinIdle(config.getMinIdleConnections());
6772

68-
JedisCluster cluster;
69-
if (config.getPassword() != null) {
70-
cluster = new JedisCluster(
71-
nodes,
72-
config.getTimeout().getMillisecondsAsInt(), //connection timeout
73-
config.getTimeout().getMillisecondsAsInt(), //read timeout
74-
config.getCluster().getMaxRedirection(),
75-
config.getPassword().getPassword(),
76-
poolConfig
77-
);
78-
} else {
79-
cluster = new JedisCluster(
80-
nodes,
81-
config.getTimeout().getMillisecondsAsInt(), //connection timeout and read timeout
82-
config.getCluster().getMaxRedirection(),
83-
poolConfig
84-
);
85-
}
73+
JedisCluster cluster = new JedisCluster(
74+
nodes,
75+
buildClientConfig(config, 0),
76+
config.getCluster().getMaxRedirection(),
77+
poolConfig
78+
);
8679

8780
return new RedisClusterCache(cluster, config);
8881

@@ -100,15 +93,31 @@ public static Cache create(final RedisCacheConfig config)
10093
return new RedisStandaloneCache(
10194
new JedisPool(
10295
poolConfig,
103-
config.getHost(),
104-
config.getPort(),
105-
config.getTimeout().getMillisecondsAsInt(), //connection timeout and read timeout
106-
config.getPassword() == null ? null : config.getPassword().getPassword(),
107-
config.getDatabase(),
108-
null
96+
new HostAndPort(config.getHost(), config.getPort()),
97+
buildClientConfig(config, config.getDatabase())
10998
),
11099
config
111100
);
112101
}
113102
}
103+
104+
@VisibleForTesting
105+
static JedisClientConfig buildClientConfig(RedisCacheConfig config, int database)
106+
{
107+
DefaultJedisClientConfig.Builder builder = DefaultJedisClientConfig
108+
.builder()
109+
.connectionTimeoutMillis(config.getTimeout().getMillisecondsAsInt())
110+
.socketTimeoutMillis(config.getTimeout().getMillisecondsAsInt())
111+
.password(config.getPassword() == null ? null : config.getPassword().getPassword())
112+
.database(database)
113+
.ssl(config.getEnableTls());
114+
115+
if (config.getEnableTls()) {
116+
SslVerifyMode verifyMode =
117+
config.getSkipTlsHostnameVerification() ? SslVerifyMode.CA : SslVerifyMode.FULL;
118+
builder.sslOptions(SslOptions.builder().sslVerifyMode(verifyMode).build());
119+
}
120+
121+
return builder.build();
122+
}
114123
}

extensions-contrib/redis-cache/src/test/java/org/apache/druid/client/cache/RedisCacheConfigTest.java

Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,12 +24,15 @@
2424
import com.github.fppt.jedismock.RedisServer;
2525
import com.github.fppt.jedismock.server.ServiceOptions;
2626
import org.apache.druid.java.util.common.IAE;
27+
import org.apache.druid.metadata.PasswordProvider;
2728
import org.hamcrest.Description;
2829
import org.hamcrest.Matcher;
2930
import org.junit.Assert;
3031
import org.junit.Rule;
3132
import org.junit.Test;
3233
import org.junit.rules.ExpectedException;
34+
import redis.clients.jedis.JedisClientConfig;
35+
import redis.clients.jedis.SslVerifyMode;
3336

3437
import java.io.IOException;
3538

@@ -232,4 +235,92 @@ public void testNoClusterAndHost() throws IOException
232235
));
233236
RedisCacheFactory.create(fromJson);
234237
}
238+
239+
@Test
240+
public void testEnableTls() throws IOException
241+
{
242+
ObjectMapper mapper = new ObjectMapper();
243+
244+
RedisCacheConfig defaultConfig = mapper.readValue(
245+
"{\"host\": \"localhost\", \"port\": 6379}",
246+
RedisCacheConfig.class
247+
);
248+
Assert.assertFalse(defaultConfig.getEnableTls());
249+
250+
RedisCacheConfig tlsConfig = mapper.readValue(
251+
"{\"host\": \"localhost\", \"port\": 6379, \"enableTls\": true}",
252+
RedisCacheConfig.class
253+
);
254+
Assert.assertTrue(tlsConfig.getEnableTls());
255+
}
256+
257+
@Test
258+
public void testSkipTlsHostnameVerification() throws IOException
259+
{
260+
ObjectMapper mapper = new ObjectMapper();
261+
262+
RedisCacheConfig defaultConfig = mapper.readValue(
263+
"{\"host\": \"localhost\", \"port\": 6379}",
264+
RedisCacheConfig.class
265+
);
266+
Assert.assertFalse(defaultConfig.getSkipTlsHostnameVerification());
267+
268+
RedisCacheConfig skipConfig = mapper.readValue(
269+
"{\"host\": \"localhost\", \"port\": 6379, \"skipTlsHostnameVerification\": true}",
270+
RedisCacheConfig.class
271+
);
272+
Assert.assertTrue(skipConfig.getSkipTlsHostnameVerification());
273+
}
274+
275+
@Test
276+
public void testBuildClientConfig()
277+
{
278+
// TLS disabled: not SSL, no SSL options, database argument passed through, null password.
279+
JedisClientConfig plain = RedisCacheFactory.buildClientConfig(new RedisCacheConfig(), 3);
280+
Assert.assertFalse(plain.isSsl());
281+
Assert.assertNull(plain.getSslOptions());
282+
Assert.assertEquals(3, plain.getDatabase());
283+
Assert.assertNull(plain.getPassword());
284+
285+
// TLS enabled with hostname verification (default) maps to SslVerifyMode.FULL, and a
286+
// non-null password is forwarded.
287+
RedisCacheConfig verifyConfig = new RedisCacheConfig()
288+
{
289+
@Override
290+
public boolean getEnableTls()
291+
{
292+
return true;
293+
}
294+
295+
@Override
296+
public PasswordProvider getPassword()
297+
{
298+
return () -> "secret";
299+
}
300+
};
301+
JedisClientConfig verify = RedisCacheFactory.buildClientConfig(verifyConfig, 0);
302+
Assert.assertTrue(verify.isSsl());
303+
Assert.assertEquals(SslVerifyMode.FULL, verify.getSslOptions().getSslVerifyMode());
304+
Assert.assertEquals("secret", verify.getPassword());
305+
306+
// skipTlsHostnameVerification maps to SslVerifyMode.CA (chain verified, hostname skipped).
307+
RedisCacheConfig skipConfig = new RedisCacheConfig()
308+
{
309+
@Override
310+
public boolean getEnableTls()
311+
{
312+
return true;
313+
}
314+
315+
@Override
316+
public boolean getSkipTlsHostnameVerification()
317+
{
318+
return true;
319+
}
320+
};
321+
Assert.assertEquals(
322+
SslVerifyMode.CA,
323+
RedisCacheFactory.buildClientConfig(skipConfig, 0).getSslOptions().getSslVerifyMode()
324+
);
325+
}
235326
}

0 commit comments

Comments
 (0)