From 6e1489c4deba694c743c88d8bff54de822dd530f Mon Sep 17 00:00:00 2001 From: Clement Escoffier Date: Thu, 6 Jun 2024 14:44:22 +0200 Subject: [PATCH] Add support for the TLS registry to the Redis client extension It is now possible to use the TLS registry to configure Redis client extension. Note that it's required to use a named configuration in this case, to avoid conflicts. --- docs/src/main/asciidoc/redis-reference.adoc | 14 ++- extensions/redis-client/deployment/pom.xml | 4 + .../client/RedisClientProcessor.java | 6 +- .../client/RedisDatasourceProcessor.java | 6 +- extensions/redis-client/runtime/pom.xml | 4 + .../runtime/client/RedisClientRecorder.java | 15 ++- .../client/VertxRedisClientFactory.java | 112 ++++++++++++++---- .../client/config/RedisClientConfig.java | 12 ++ 8 files changed, 140 insertions(+), 33 deletions(-) diff --git a/docs/src/main/asciidoc/redis-reference.adoc b/docs/src/main/asciidoc/redis-reference.adoc index 67bc15af5477d..0de838ec7041c 100644 --- a/docs/src/main/asciidoc/redis-reference.adoc +++ b/docs/src/main/asciidoc/redis-reference.adoc @@ -247,11 +247,23 @@ quarkus.redis.password= To use TLS, you need to: -1. Set the `quarkus.redis.tls.enabled=true` property +1. Set the `quarkus.redis.tls.enabled=true` property or use the xref:./tls-registry-reference.adoc[TLS registry] (recommended) 2. Make sure that your URL starts with `rediss://` (with two `s`) + +When using the TLS registry, you need to use a named configuration to avoid conflicts with other TLS configurations: + +[source,properties] +---- +quarkus.tls.redis.trust-store.p12.path=client.p12 +quarkus.tls.redis.trust-store.p12.password=secret + +quarkus.redis.tls-configuration-name=redis # Reference the named configuration +---- + IMPORTANT: The default hostname verifier is set to `NONE`, meaning it does not verify the host name. You can change this behavior by setting the `quarkus.redis.tls.hostname-verification-algorithm` property, to `HTTPS` for example. + === Configure the authentication The Redis password can be set in the `redis://` URL or with the `quarkus.redis.password` property. diff --git a/extensions/redis-client/deployment/pom.xml b/extensions/redis-client/deployment/pom.xml index 36f29f62c7c00..4061e6a14da54 100644 --- a/extensions/redis-client/deployment/pom.xml +++ b/extensions/redis-client/deployment/pom.xml @@ -27,6 +27,10 @@ io.quarkus quarkus-devservices-deployment + + io.quarkus + quarkus-tls-registry-deployment + io.quarkus quarkus-redis-client diff --git a/extensions/redis-client/deployment/src/main/java/io/quarkus/redis/deployment/client/RedisClientProcessor.java b/extensions/redis-client/deployment/src/main/java/io/quarkus/redis/deployment/client/RedisClientProcessor.java index 55308b824e85d..62a6763695361 100644 --- a/extensions/redis-client/deployment/src/main/java/io/quarkus/redis/deployment/client/RedisClientProcessor.java +++ b/extensions/redis-client/deployment/src/main/java/io/quarkus/redis/deployment/client/RedisClientProcessor.java @@ -53,6 +53,7 @@ import io.quarkus.runtime.LaunchMode; import io.quarkus.runtime.configuration.ConfigurationException; import io.quarkus.smallrye.health.deployment.spi.HealthBuildItem; +import io.quarkus.tls.TlsRegistryBuildItem; import io.quarkus.vertx.deployment.VertxBuildItem; import io.vertx.redis.client.impl.types.BulkType; @@ -127,7 +128,8 @@ public void init( VertxBuildItem vertxBuildItem, ApplicationArchivesBuildItem applicationArchivesBuildItem, LaunchModeBuildItem launchMode, BuildProducer nativeImageResources, - BuildProducer hotDeploymentWatchedFiles) { + BuildProducer hotDeploymentWatchedFiles, + TlsRegistryBuildItem tlsRegistryBuildItem) { // Collect the used redis clients, the unused clients will not be instantiated. Set names = new HashSet<>(); @@ -156,7 +158,7 @@ public void init( .ifPresent(x -> names.addAll(configuredClientNames(buildTimeConfig, ConfigProvider.getConfig()))); // Inject the creation of the client when the application starts. - recorder.initialize(vertxBuildItem.getVertx(), names); + recorder.initialize(vertxBuildItem.getVertx(), names, tlsRegistryBuildItem.registry()); // Create the supplier and define the beans. for (String name : names) { diff --git a/extensions/redis-client/deployment/src/main/java/io/quarkus/redis/deployment/client/RedisDatasourceProcessor.java b/extensions/redis-client/deployment/src/main/java/io/quarkus/redis/deployment/client/RedisDatasourceProcessor.java index a9c8679b768a2..1754e3fc85eaf 100644 --- a/extensions/redis-client/deployment/src/main/java/io/quarkus/redis/deployment/client/RedisDatasourceProcessor.java +++ b/extensions/redis-client/deployment/src/main/java/io/quarkus/redis/deployment/client/RedisDatasourceProcessor.java @@ -31,6 +31,7 @@ import io.quarkus.redis.datasource.RedisDataSource; import io.quarkus.redis.datasource.codecs.Codec; import io.quarkus.redis.runtime.client.RedisClientRecorder; +import io.quarkus.tls.TlsRegistryBuildItem; import io.quarkus.vertx.deployment.VertxBuildItem; public class RedisDatasourceProcessor { @@ -84,7 +85,8 @@ public void init(RedisClientRecorder recorder, List clients, ShutdownContextBuildItem shutdown, BuildProducer syntheticBeans, - VertxBuildItem vertxBuildItem) { + VertxBuildItem vertxBuildItem, + TlsRegistryBuildItem tlsRegistryBuildItem) { if (clients.isEmpty()) { return; @@ -94,7 +96,7 @@ public void init(RedisClientRecorder recorder, names.add(client.name); } // Inject the creation of the client when the application starts. - recorder.initialize(vertxBuildItem.getVertx(), names); + recorder.initialize(vertxBuildItem.getVertx(), names, tlsRegistryBuildItem.registry()); // Create the supplier and define the beans. for (String name : names) { diff --git a/extensions/redis-client/runtime/pom.xml b/extensions/redis-client/runtime/pom.xml index f00b8e2c6715e..4518d61454f59 100644 --- a/extensions/redis-client/runtime/pom.xml +++ b/extensions/redis-client/runtime/pom.xml @@ -23,6 +23,10 @@ io.quarkus quarkus-jackson + + io.quarkus + quarkus-tls-registry + io.smallrye.reactive smallrye-mutiny-vertx-redis-client diff --git a/extensions/redis-client/runtime/src/main/java/io/quarkus/redis/runtime/client/RedisClientRecorder.java b/extensions/redis-client/runtime/src/main/java/io/quarkus/redis/runtime/client/RedisClientRecorder.java index cec469fc5d52d..1aa23cb5b5bfd 100644 --- a/extensions/redis-client/runtime/src/main/java/io/quarkus/redis/runtime/client/RedisClientRecorder.java +++ b/extensions/redis-client/runtime/src/main/java/io/quarkus/redis/runtime/client/RedisClientRecorder.java @@ -27,6 +27,7 @@ import io.quarkus.runtime.ShutdownContext; import io.quarkus.runtime.annotations.Recorder; import io.quarkus.runtime.configuration.ConfigurationException; +import io.quarkus.tls.TlsConfigurationRegistry; import io.vertx.mutiny.core.Vertx; import io.vertx.mutiny.redis.client.Command; import io.vertx.mutiny.redis.client.Redis; @@ -48,7 +49,8 @@ public RedisClientRecorder(RedisConfig rc) { this.config = rc; } - public void initialize(RuntimeValue vertx, Set names) { + public void initialize(RuntimeValue vertx, Set names, + Supplier registry) { Instance instance = CDI.current().select(ObservableRedisMetrics.class); if (instance.isResolvable()) { this.metrics = instance.get(); @@ -58,9 +60,11 @@ public void initialize(RuntimeValue vertx, Set name this.vertx = Vertx.newInstance(vertx.getValue()); + TlsConfigurationRegistry tlsRegistry = registry.get(); + _registerCodecs(); - _initialize(vertx.getValue(), names); + _initialize(vertx.getValue(), names, tlsRegistry); } private static void _registerCodecs() { @@ -69,7 +73,7 @@ private static void _registerCodecs() { Codecs.register(codecs.stream()); } - public void _initialize(io.vertx.core.Vertx vertx, Set names) { + public void _initialize(io.vertx.core.Vertx vertx, Set names, TlsConfigurationRegistry tlsRegistry) { for (String name : names) { // Search if we have an associated config: // - if default -> Default @@ -89,11 +93,12 @@ public ConfigurationException get() { } }); clients.computeIfAbsent(name, - x -> new RedisClientAndApi(name, VertxRedisClientFactory.create(name, vertx, actualConfig), metrics)); + x -> new RedisClientAndApi(name, VertxRedisClientFactory.create(name, vertx, actualConfig, tlsRegistry), + metrics)); } else if (DEFAULT_CLIENT_NAME.equalsIgnoreCase(name) && maybe.isPresent()) { clients.computeIfAbsent(name, x -> new RedisClientAndApi(name, - VertxRedisClientFactory.create(DEFAULT_CLIENT_NAME, vertx, maybe.get()), metrics)); + VertxRedisClientFactory.create(DEFAULT_CLIENT_NAME, vertx, maybe.get(), tlsRegistry), metrics)); } // Do not throw an error. We would need to check if the default redis client is used. } diff --git a/extensions/redis-client/runtime/src/main/java/io/quarkus/redis/runtime/client/VertxRedisClientFactory.java b/extensions/redis-client/runtime/src/main/java/io/quarkus/redis/runtime/client/VertxRedisClientFactory.java index e21e438e23998..02f8171f3ad7c 100644 --- a/extensions/redis-client/runtime/src/main/java/io/quarkus/redis/runtime/client/VertxRedisClientFactory.java +++ b/extensions/redis-client/runtime/src/main/java/io/quarkus/redis/runtime/client/VertxRedisClientFactory.java @@ -10,8 +10,11 @@ import java.net.URI; import java.util.ArrayList; import java.util.List; +import java.util.Optional; import java.util.Set; +import org.jboss.logging.Logger; + import io.quarkus.arc.Arc; import io.quarkus.arc.ArcContainer; import io.quarkus.arc.InjectableInstance; @@ -20,12 +23,15 @@ import io.quarkus.redis.client.RedisOptionsCustomizer; import io.quarkus.redis.runtime.client.config.NetConfig; import io.quarkus.redis.runtime.client.config.RedisClientConfig; -import io.quarkus.redis.runtime.client.config.TlsConfig; import io.quarkus.runtime.configuration.ConfigurationException; +import io.quarkus.tls.TlsConfiguration; +import io.quarkus.tls.TlsConfigurationRegistry; import io.smallrye.common.annotation.Identifier; import io.vertx.core.Vertx; +import io.vertx.core.buffer.Buffer; import io.vertx.core.net.NetClientOptions; import io.vertx.core.net.ProxyOptions; +import io.vertx.core.net.SSLOptions; import io.vertx.redis.client.Redis; import io.vertx.redis.client.RedisClientType; import io.vertx.redis.client.RedisOptions; @@ -37,11 +43,13 @@ public class VertxRedisClientFactory { public static final String DEFAULT_CLIENT = ""; + private static final Logger LOGGER = Logger.getLogger(VertxRedisClientFactory.class); + private VertxRedisClientFactory() { // Avoid direct instantiation. } - public static Redis create(String name, Vertx vertx, RedisClientConfig config) { + public static Redis create(String name, Vertx vertx, RedisClientConfig config, TlsConfigurationRegistry tlsRegistry) { RedisOptions options = new RedisOptions(); List hosts = new ArrayList<>(); @@ -86,6 +94,7 @@ public static Redis create(String name, Vertx vertx, RedisClientConfig config) { config.replicas().ifPresent(options::setUseReplicas); options.setNetClientOptions(toNetClientOptions(config)); + configureTLS(name, config, tlsRegistry, options.getNetClientOptions()); options.setPoolName(name); // Use the convention defined by Quarkus Micrometer Vert.x metrics to create metrics prefixed with redis. @@ -110,35 +119,14 @@ private static void customize(String name, RedisOptions options) { private static NetClientOptions toNetClientOptions(RedisClientConfig config) { NetConfig tcp = config.tcp(); - TlsConfig tls = config.tls(); NetClientOptions net = new NetClientOptions(); - tcp.alpn().ifPresent(net::setUseAlpn); tcp.applicationLayerProtocols().ifPresent(net::setApplicationLayerProtocols); tcp.connectionTimeout().ifPresent(d -> net.setConnectTimeout((int) d.toMillis())); - - String verificationAlgorithm = tls.hostnameVerificationAlgorithm(); - if ("NONE".equalsIgnoreCase(verificationAlgorithm)) { - net.setHostnameVerificationAlgorithm(""); - } else { - net.setHostnameVerificationAlgorithm(verificationAlgorithm); - } - tcp.idleTimeout().ifPresent(d -> net.setIdleTimeout((int) d.toSeconds())); - tcp.keepAlive().ifPresent(b -> net.setTcpKeepAlive(true)); tcp.noDelay().ifPresent(b -> net.setTcpNoDelay(true)); - net.setSsl(tls.enabled()).setTrustAll(tls.trustAll()); - - configurePemTrustOptions(net, tls.trustCertificatePem()); - configureJksTrustOptions(net, tls.trustCertificateJks()); - configurePfxTrustOptions(net, tls.trustCertificatePfx()); - - configurePemKeyCertOptions(net, tls.keyCertificatePem()); - configureJksKeyCertOptions(net, tls.keyCertificateJks()); - configurePfxKeyCertOptions(net, tls.keyCertificatePfx()); - net.setReconnectAttempts(config.reconnectAttempts()); net.setReconnectInterval(config.reconnectInterval().toMillis()); @@ -191,4 +179,82 @@ public static RedisHostsProvider findProvider(String name) { return providers.get(); } + private static void configureTLS(String name, RedisClientConfig config, TlsConfigurationRegistry tlsRegistry, + NetClientOptions net) { + TlsConfiguration configuration = null; + boolean defaultTrustAll = false; + + // Check if we have a named TLS configuration or a default configuration: + if (config.tlsConfigurationName().isPresent()) { + Optional maybeConfiguration = tlsRegistry.get(config.tlsConfigurationName().get()); + if (maybeConfiguration.isEmpty()) { + throw new IllegalStateException("Unable to find the TLS configuration " + + config.tlsConfigurationName().get() + " for the Redis client " + name + "."); + } + configuration = maybeConfiguration.get(); + } else if (tlsRegistry.getDefault().isPresent() && (tlsRegistry.getDefault().get().isTrustAll())) { + defaultTrustAll = tlsRegistry.getDefault().get().isTrustAll(); + if (defaultTrustAll) { + LOGGER.warn("The default TLS configuration is set to trust all certificates. This is a security risk." + + "Please use a named TLS configuration for the Redis client " + name + " to avoid this warning."); + } + } + + // Apply the configuration + if (configuration != null) { + // This part is often the same (or close) for every Vert.x client: + net.setSsl(true); + + if (configuration.getTrustStoreOptions() != null) { + net.setTrustOptions(configuration.getTrustStoreOptions()); + } + + // For mTLS: + if (configuration.getKeyStoreOptions() != null) { + net.setKeyCertOptions(configuration.getKeyStoreOptions()); + } + + if (configuration.isTrustAll()) { + net.setTrustAll(true); + } + if (configuration.getHostnameVerificationAlgorithm().isPresent()) { + net.setHostnameVerificationAlgorithm(configuration.getHostnameVerificationAlgorithm().get()); + } + + SSLOptions sslOptions = configuration.getSSLOptions(); + if (sslOptions != null) { + net.setSslHandshakeTimeout(sslOptions.getSslHandshakeTimeout()); + net.setSslHandshakeTimeoutUnit(sslOptions.getSslHandshakeTimeoutUnit()); + for (String suite : sslOptions.getEnabledCipherSuites()) { + net.addEnabledCipherSuite(suite); + } + for (Buffer buffer : sslOptions.getCrlValues()) { + net.addCrlValue(buffer); + } + net.setEnabledSecureTransportProtocols(sslOptions.getEnabledSecureTransportProtocols()); + net.setUseAlpn(sslOptions.isUseAlpn()); + } + + } else { + config.tcp().alpn().ifPresent(net::setUseAlpn); + + String verificationAlgorithm = config.tls().hostnameVerificationAlgorithm(); + if ("NONE".equalsIgnoreCase(verificationAlgorithm)) { + net.setHostnameVerificationAlgorithm(""); + } else { + net.setHostnameVerificationAlgorithm(verificationAlgorithm); + } + net.setSsl(config.tls().enabled() || defaultTrustAll); + net.setTrustAll(config.tls().trustAll() || defaultTrustAll); + + configurePemTrustOptions(net, config.tls().trustCertificatePem()); + configureJksTrustOptions(net, config.tls().trustCertificateJks()); + configurePfxTrustOptions(net, config.tls().trustCertificatePfx()); + + configurePemKeyCertOptions(net, config.tls().keyCertificatePem()); + configureJksKeyCertOptions(net, config.tls().keyCertificateJks()); + configurePfxKeyCertOptions(net, config.tls().keyCertificatePfx()); + } + } + } diff --git a/extensions/redis-client/runtime/src/main/java/io/quarkus/redis/runtime/client/config/RedisClientConfig.java b/extensions/redis-client/runtime/src/main/java/io/quarkus/redis/runtime/client/config/RedisClientConfig.java index f2413f87f3bdb..6b47bd51be284 100644 --- a/extensions/redis-client/runtime/src/main/java/io/quarkus/redis/runtime/client/config/RedisClientConfig.java +++ b/extensions/redis-client/runtime/src/main/java/io/quarkus/redis/runtime/client/config/RedisClientConfig.java @@ -168,6 +168,18 @@ public interface RedisClientConfig { @ConfigDocSection TlsConfig tls(); + /** + * The name of the TLS configuration to use. + *

+ * If a name is configured, it uses the configuration from {@code quarkus.tls..*} + * If a name is configured, but no TLS configuration is found with that name then an error will be thrown. + *

+ * If no TLS configuration name is set then, {@code quarkus.redis.$client-name.tls} will be used. + *

+ * The default TLS configuration is not used by default. + */ + Optional tlsConfigurationName(); + default String toDebugString() { return "RedisClientConfig{" + "hosts=" + hosts() +