This is an automated email from the ASF dual-hosted git repository.

bernardobotella pushed a commit to branch trunk
in repository https://gitbox.apache.org/repos/asf/cassandra-sidecar.git


The following commit(s) were added to refs/heads/trunk by this push:
     new 62da993b CASSSIDECAR-228: Enable SSL hot reload certificates (#208)
62da993b is described below

commit 62da993b064dd135d2fd719ed6a0154d5571c076
Author: Bernardo Botella <bbote...@users.noreply.github.com>
AuthorDate: Fri Mar 21 16:24:13 2025 -0700

    CASSSIDECAR-228: Enable SSL hot reload certificates (#208)
    
    Patch by Bernardo Botella; reviewed by Francisco Guerrero for 
CASSSIDECAR-228
---
 CHANGES.txt                                        |   1 +
 .../cassandra/sidecar/client/SidecarClient.java    |   4 +-
 .../DynamicSidecarInstancesProvider.java           |  56 ++++++
 .../coordination/SidecarHttpHealthProvider.java    |   2 +-
 .../cassandra/sidecar/modules/CdcModule.java       |  11 +-
 .../sidecar/modules/SchedulingModule.java          |  20 +-
 .../modules/multibindings/PeriodicTaskMapKeys.java |   1 +
 .../sidecar/tasks/KeyStoreCheckPeriodicTask.java   |  87 +++++---
 .../sidecar/utils/SidecarClientProvider.java       | 103 ++++++----
 .../org/apache/cassandra/sidecar/TestModule.java   |  24 ++-
 .../apache/cassandra/sidecar/TestSslModule.java    |   2 +-
 .../MutualTLSAuthenticationHandlerTest.java        |   2 +-
 .../sidecar/utils/SidecarClientProviderTest.java   | 223 +++++++++++++++++++++
 13 files changed, 454 insertions(+), 82 deletions(-)

diff --git a/CHANGES.txt b/CHANGES.txt
index b51f2024..b47c5dc2 100644
--- a/CHANGES.txt
+++ b/CHANGES.txt
@@ -1,5 +1,6 @@
 0.2.0
 -----
+ * Hot Reload client and server SSL certificates (CASSSIDECAR-228)
  * Enhance the Cluster Lease Claim task feature (CASSSIDECAR-232)
  * Capture Metrics for Schema Reporting (CASSSIDECAR-216)
  * SidecarInstanceCodec is failing to find codec for type (CASSSIDECAR-229)
diff --git 
a/client/src/main/java/org/apache/cassandra/sidecar/client/SidecarClient.java 
b/client/src/main/java/org/apache/cassandra/sidecar/client/SidecarClient.java
index db1e2c85..71910285 100644
--- 
a/client/src/main/java/org/apache/cassandra/sidecar/client/SidecarClient.java
+++ 
b/client/src/main/java/org/apache/cassandra/sidecar/client/SidecarClient.java
@@ -24,6 +24,7 @@ import java.util.Map;
 import java.util.Objects;
 import java.util.UUID;
 import java.util.concurrent.CompletableFuture;
+
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
@@ -124,9 +125,10 @@ public class SidecarClient implements AutoCloseable, 
SidecarClientBlobRestoreExt
     /**
      * Executes the Sidecar health request using the configured selection 
policy and with no retries
      *
+     * @param instance the instance where the request will be executed
      * @return a completable future of the Sidecar health response
      */
-    public CompletableFuture<HealthResponse> sidecarPeerHealth(SidecarInstance 
instance)
+    public CompletableFuture<HealthResponse> sidecarHealth(SidecarInstance 
instance)
     {
         return executor.executeRequestAsync(requestBuilder()
                                             
.singleInstanceSelectionPolicy(instance)
diff --git 
a/server/src/main/java/org/apache/cassandra/sidecar/coordination/DynamicSidecarInstancesProvider.java
 
b/server/src/main/java/org/apache/cassandra/sidecar/coordination/DynamicSidecarInstancesProvider.java
new file mode 100644
index 00000000..7352741d
--- /dev/null
+++ 
b/server/src/main/java/org/apache/cassandra/sidecar/coordination/DynamicSidecarInstancesProvider.java
@@ -0,0 +1,56 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.cassandra.sidecar.coordination;
+
+import java.util.List;
+import java.util.stream.Collectors;
+
+import org.apache.cassandra.sidecar.client.SidecarInstancesProvider;
+import org.apache.cassandra.sidecar.cluster.InstancesMetadata;
+import org.apache.cassandra.sidecar.common.client.SidecarInstance;
+import org.apache.cassandra.sidecar.common.client.SidecarInstanceImpl;
+import org.apache.cassandra.sidecar.config.ServiceConfiguration;
+
+/**
+ * A {@link SidecarInstancesProvider} implementation that returns Sidecar 
instances based on the configured
+ * {@link InstancesMetadata} for the local Sidecar
+ */
+public class DynamicSidecarInstancesProvider implements 
SidecarInstancesProvider
+{
+    private final InstancesMetadata instancesMetadata;
+    private final ServiceConfiguration serviceConfiguration;
+
+    public DynamicSidecarInstancesProvider(InstancesMetadata 
instancesMetadata, ServiceConfiguration serviceConfiguration)
+    {
+        this.instancesMetadata = instancesMetadata;
+        this.serviceConfiguration = serviceConfiguration;
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    @Override
+    public List<SidecarInstance> instances()
+    {
+        return instancesMetadata.instances()
+                                .stream()
+                                .map(instanceMetadata -> new 
SidecarInstanceImpl(instanceMetadata.host(), serviceConfiguration.port()))
+                                .collect(Collectors.toList());
+    }
+}
diff --git 
a/server/src/main/java/org/apache/cassandra/sidecar/coordination/SidecarHttpHealthProvider.java
 
b/server/src/main/java/org/apache/cassandra/sidecar/coordination/SidecarHttpHealthProvider.java
index 8cf0cd89..63eef1a3 100644
--- 
a/server/src/main/java/org/apache/cassandra/sidecar/coordination/SidecarHttpHealthProvider.java
+++ 
b/server/src/main/java/org/apache/cassandra/sidecar/coordination/SidecarHttpHealthProvider.java
@@ -50,7 +50,7 @@ public class SidecarHttpHealthProvider implements 
SidecarPeerHealthProvider
         try
         {
             SidecarClient client = clientProvider.get();
-            CompletableFuture<HealthResponse> healthRequest = 
client.sidecarPeerHealth(instance);
+            CompletableFuture<HealthResponse> healthRequest = 
client.sidecarHealth(instance);
             return Future.fromCompletionStage(healthRequest)
                          .map(healthResponse -> healthResponse.isOk()
                                                 ? Health.UP
diff --git 
a/server/src/main/java/org/apache/cassandra/sidecar/modules/CdcModule.java 
b/server/src/main/java/org/apache/cassandra/sidecar/modules/CdcModule.java
index 450151cb..3b6da2c4 100644
--- a/server/src/main/java/org/apache/cassandra/sidecar/modules/CdcModule.java
+++ b/server/src/main/java/org/apache/cassandra/sidecar/modules/CdcModule.java
@@ -23,10 +23,12 @@ import com.google.inject.Provides;
 import com.google.inject.Singleton;
 import com.google.inject.multibindings.ProvidesIntoMap;
 import org.apache.cassandra.sidecar.cdc.CdcLogCache;
+import org.apache.cassandra.sidecar.client.SidecarInstancesProvider;
 import org.apache.cassandra.sidecar.cluster.InstancesMetadata;
 import org.apache.cassandra.sidecar.concurrent.ExecutorPools;
 import org.apache.cassandra.sidecar.config.ServiceConfiguration;
 import org.apache.cassandra.sidecar.config.SidecarConfiguration;
+import 
org.apache.cassandra.sidecar.coordination.DynamicSidecarInstancesProvider;
 import 
org.apache.cassandra.sidecar.coordination.InnerDcTokenAdjacentPeerProvider;
 import org.apache.cassandra.sidecar.coordination.SidecarHttpHealthProvider;
 import org.apache.cassandra.sidecar.coordination.SidecarPeerHealthMonitorTask;
@@ -67,7 +69,7 @@ public class CdcModule extends AbstractModule
     {
         return new ConfigsSchema(serviceConfiguration);
     }
-    
+
     @ProvidesIntoMap
     @KeyClassMapKey(VertxRouteMapKeys.ListCdcSegmentsRouteKey.class)
     VertxRoute listCdcSegmentsRoute(RouteBuilder.Factory factory,
@@ -130,4 +132,11 @@ public class CdcModule extends AbstractModule
     {
         return innerDcTokenAdjacentPeerProvider;
     }
+
+    @Provides
+    @Singleton
+    public SidecarInstancesProvider sidecarInstancesProvider(InstancesMetadata 
instancesMetadata, ServiceConfiguration serviceConfiguration)
+    {
+        return new DynamicSidecarInstancesProvider(instancesMetadata, 
serviceConfiguration);
+    }
 }
diff --git 
a/server/src/main/java/org/apache/cassandra/sidecar/modules/SchedulingModule.java
 
b/server/src/main/java/org/apache/cassandra/sidecar/modules/SchedulingModule.java
index 6ad8693e..b6d34a67 100644
--- 
a/server/src/main/java/org/apache/cassandra/sidecar/modules/SchedulingModule.java
+++ 
b/server/src/main/java/org/apache/cassandra/sidecar/modules/SchedulingModule.java
@@ -18,6 +18,8 @@
 
 package org.apache.cassandra.sidecar.modules;
 
+import java.util.function.Function;
+
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
@@ -26,6 +28,7 @@ import com.google.inject.Provider;
 import com.google.inject.Provides;
 import com.google.inject.Singleton;
 import com.google.inject.multibindings.ProvidesIntoMap;
+import io.vertx.core.Future;
 import io.vertx.core.Vertx;
 import org.apache.cassandra.sidecar.concurrent.ExecutorPools;
 import org.apache.cassandra.sidecar.config.SidecarConfiguration;
@@ -37,6 +40,7 @@ import org.apache.cassandra.sidecar.server.Server;
 import org.apache.cassandra.sidecar.tasks.KeyStoreCheckPeriodicTask;
 import org.apache.cassandra.sidecar.tasks.PeriodicTask;
 import org.apache.cassandra.sidecar.tasks.PeriodicTaskExecutor;
+import org.apache.cassandra.sidecar.utils.SidecarClientProvider;
 
 /**
  * Provides the scheduling capability in Sidecar. Periodic tasks are deployed 
to {@link PeriodicTaskExecutor}
@@ -71,6 +75,20 @@ public class SchedulingModule extends AbstractModule
     @KeyClassMapKey(PeriodicTaskMapKeys.KeyStoreCheckPeriodicTaskKey.class)
     PeriodicTask keyStoreCheckPeriodicTask(Vertx vertx, Provider<Server> 
server, SidecarConfiguration configuration)
     {
-        return new KeyStoreCheckPeriodicTask(vertx, server, configuration);
+        Function<Long, Future<Boolean>> updateServerSSLOptionsFunction =
+        lastModifiedTime -> 
server.get().updateSSLOptions(lastModifiedTime).compose(v -> 
Future.succeededFuture(true));
+
+        return KeyStoreCheckPeriodicTask.forServer(vertx, configuration, 
updateServerSSLOptionsFunction);
+    }
+
+    @ProvidesIntoMap
+    
@KeyClassMapKey(PeriodicTaskMapKeys.ClientKeyStoreCheckPeriodicTaskKey.class)
+    PeriodicTask clientKeyStoreCheckPeriodicTask(Vertx vertx,
+                                                 SidecarClientProvider 
sidecarClientProvider,
+                                                 SidecarConfiguration 
configuration)
+    {
+        Function<Long, Future<Boolean>> updateClientSSLOptionsFunction = 
sidecarClientProvider::updateSSLOptions;
+
+        return KeyStoreCheckPeriodicTask.forClient(vertx, configuration, 
updateClientSSLOptionsFunction);
     }
 }
diff --git 
a/server/src/main/java/org/apache/cassandra/sidecar/modules/multibindings/PeriodicTaskMapKeys.java
 
b/server/src/main/java/org/apache/cassandra/sidecar/modules/multibindings/PeriodicTaskMapKeys.java
index c6040f59..7099c7dc 100644
--- 
a/server/src/main/java/org/apache/cassandra/sidecar/modules/multibindings/PeriodicTaskMapKeys.java
+++ 
b/server/src/main/java/org/apache/cassandra/sidecar/modules/multibindings/PeriodicTaskMapKeys.java
@@ -26,6 +26,7 @@ public interface PeriodicTaskMapKeys
     interface ClusterLeaseClaimTaskKey extends ClassKey {}
     interface HealthCheckPeriodicTaskKey extends ClassKey {}
     interface KeyStoreCheckPeriodicTaskKey extends ClassKey {}
+    interface ClientKeyStoreCheckPeriodicTaskKey extends ClassKey {}
     interface RestoreJobDiscovererKey extends ClassKey {}
     interface RestoreProcessorKey extends ClassKey {}
     interface RingTopologyRefresherKey extends ClassKey {}
diff --git 
a/server/src/main/java/org/apache/cassandra/sidecar/tasks/KeyStoreCheckPeriodicTask.java
 
b/server/src/main/java/org/apache/cassandra/sidecar/tasks/KeyStoreCheckPeriodicTask.java
index e079359b..b7fe64ad 100644
--- 
a/server/src/main/java/org/apache/cassandra/sidecar/tasks/KeyStoreCheckPeriodicTask.java
+++ 
b/server/src/main/java/org/apache/cassandra/sidecar/tasks/KeyStoreCheckPeriodicTask.java
@@ -18,16 +18,17 @@
 
 package org.apache.cassandra.sidecar.tasks;
 
+import java.util.function.Function;
+
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
-import com.google.inject.Provider;
+import io.vertx.core.Future;
 import io.vertx.core.Promise;
 import io.vertx.core.Vertx;
 import org.apache.cassandra.sidecar.common.server.utils.DurationSpec;
 import org.apache.cassandra.sidecar.config.SidecarConfiguration;
 import org.apache.cassandra.sidecar.config.SslConfiguration;
-import org.apache.cassandra.sidecar.server.Server;
 import org.apache.cassandra.sidecar.utils.EventBusUtils;
 
 import static 
org.apache.cassandra.sidecar.server.SidecarServerEvents.ON_SERVER_START;
@@ -41,25 +42,56 @@ public class KeyStoreCheckPeriodicTask implements 
PeriodicTask
     private static final Logger LOGGER = 
LoggerFactory.getLogger(KeyStoreCheckPeriodicTask.class);
 
     private final Vertx vertx;
-    private final Provider<Server> server;
-    private final SslConfiguration configuration;
+    private final SslConfiguration sslConfiguration;
+    private final Function<Long, Future<Boolean>> updateSSLOptionsFunction;
     private long lastModifiedTime = 0; // records the last modified timestamp
+    private final String taskName;
 
-    public KeyStoreCheckPeriodicTask(Vertx vertx, Provider<Server> server, 
SidecarConfiguration configuration)
+    protected KeyStoreCheckPeriodicTask(Vertx vertx,
+                                        SslConfiguration sslConfiguration,
+                                        Function<Long, Future<Boolean>> 
updateSSLOptionsFunction,
+                                        String taskName)
     {
         this.vertx = vertx;
-        this.server = server;
-        this.configuration = configuration.sslConfiguration();
+        this.sslConfiguration = sslConfiguration;
+        this.updateSSLOptionsFunction = updateSSLOptionsFunction;
+        this.taskName = taskName;
+    }
+
+    public static KeyStoreCheckPeriodicTask forServer(Vertx vertx,
+                                                      SidecarConfiguration 
configuration,
+                                                      Function<Long, 
Future<Boolean>> updateSSLOptionsFunction)
+    {
+        return new KeyStoreCheckPeriodicTask(vertx,
+                                             configuration.sslConfiguration(),
+                                             updateSSLOptionsFunction,
+                                             
"ServerKeyStoreCheckPeriodicTask");
+    }
+
+    public static KeyStoreCheckPeriodicTask forClient(Vertx vertx,
+                                                      SidecarConfiguration 
configuration,
+                                                      Function<Long, 
Future<Boolean>> updateSSLOptionsFunction)
+    {
+        return new KeyStoreCheckPeriodicTask(vertx,
+                                             
configuration.sidecarClientConfiguration().sslConfiguration(),
+                                             updateSSLOptionsFunction,
+                                             
"ClientKeyStoreCheckPeriodicTask");
+    }
+
+    @Override
+    public String name()
+    {
+        return taskName;
     }
 
     @Override
     public void deploy(Vertx vertx, PeriodicTaskExecutor executor)
     {
-        if (configuration != null
-            && configuration.enabled()
-            && configuration.keystore() != null
-            && configuration.keystore().isConfigured()
-            && configuration.keystore().reloadStore())
+        if (sslConfiguration != null
+            && sslConfiguration.enabled()
+            && sslConfiguration.keystore() != null
+            && sslConfiguration.keystore().isConfigured()
+            && sslConfiguration.keystore().reloadStore())
         {
             maybeRecordLastModifiedTime();
             EventBusUtils.onceLocalConsumer(vertx.eventBus(), 
ON_SERVER_START.address(), message -> executor.schedule(this));
@@ -82,14 +114,14 @@ public class KeyStoreCheckPeriodicTask implements 
PeriodicTask
     @Override
     public DurationSpec delay()
     {
-        return configuration.keystore().checkInterval();
+        return sslConfiguration.keystore().checkInterval();
     }
 
     @Override
     public void execute(Promise<Void> promise)
     {
         LOGGER.info("Running periodic key store checker");
-        String keyStorePath = configuration.keystore().path();
+        String keyStorePath = sslConfiguration.keystore().path();
         vertx.fileSystem().props(keyStorePath)
              .onSuccess(props -> {
                  long previousLastModifiedTime = lastModifiedTime;
@@ -99,17 +131,16 @@ public class KeyStoreCheckPeriodicTask implements 
PeriodicTask
                                  "lastModifiedTime={}", keyStorePath, 
previousLastModifiedTime,
                                  props.lastModifiedTime());
 
-                     server.get()
-                           .updateSSLOptions(props.lastModifiedTime())
-                           .onSuccess(v -> {
-                               lastModifiedTime = props.lastModifiedTime();
-                               LOGGER.info("Completed reloading certificates 
from path={}", keyStorePath);
-                               promise.complete(); // propagate successful 
completion
-                           })
-                           .onFailure(cause -> {
-                               LOGGER.error("Failed to reload certificate from 
path={}", keyStorePath, cause);
-                               promise.fail(cause);
-                           });
+                     updateSSLOptionsFunction.apply(props.lastModifiedTime())
+                                             .onSuccess(v -> {
+                                                 lastModifiedTime = 
props.lastModifiedTime();
+                                                 LOGGER.info("Completed 
reloading certificates from path={}", keyStorePath);
+                                                 promise.complete(); // 
propagate successful completion
+                                             })
+                                             .onFailure(cause -> {
+                                                 LOGGER.error("Failed to 
reload certificate from path={}", keyStorePath, cause);
+                                                 promise.fail(cause);
+                                             });
                  }
                  else
                  {
@@ -128,7 +159,7 @@ public class KeyStoreCheckPeriodicTask implements 
PeriodicTask
         {
             return;
         }
-        String keyStorePath = configuration.keystore().path();
+        String keyStorePath = sslConfiguration.keystore().path();
         vertx.fileSystem().props(keyStorePath)
              .onSuccess(props -> lastModifiedTime = props.lastModifiedTime())
              .onFailure(err -> {
@@ -145,7 +176,7 @@ public class KeyStoreCheckPeriodicTask implements 
PeriodicTask
      */
     private boolean shouldSkip()
     {
-        return !configuration.isKeystoreConfigured()
-               || !configuration.keystore().reloadStore();
+        return !sslConfiguration.isKeystoreConfigured()
+               || !sslConfiguration.keystore().reloadStore();
     }
 }
diff --git 
a/server/src/main/java/org/apache/cassandra/sidecar/utils/SidecarClientProvider.java
 
b/server/src/main/java/org/apache/cassandra/sidecar/utils/SidecarClientProvider.java
index a5d3ea68..6d06a866 100644
--- 
a/server/src/main/java/org/apache/cassandra/sidecar/utils/SidecarClientProvider.java
+++ 
b/server/src/main/java/org/apache/cassandra/sidecar/utils/SidecarClientProvider.java
@@ -18,7 +18,7 @@
 
 package org.apache.cassandra.sidecar.utils;
 
-import java.util.ArrayList;
+import java.util.LinkedHashSet;
 import java.util.concurrent.atomic.AtomicBoolean;
 
 import org.slf4j.Logger;
@@ -27,10 +27,10 @@ import org.slf4j.LoggerFactory;
 import com.google.inject.Inject;
 import com.google.inject.Provider;
 import com.google.inject.Singleton;
+import io.vertx.core.Future;
 import io.vertx.core.Vertx;
-import io.vertx.core.http.HttpClient;
-import io.vertx.core.net.JksOptions;
 import io.vertx.core.net.OpenSSLEngineOptions;
+import io.vertx.core.net.SSLOptions;
 import io.vertx.ext.web.client.WebClient;
 import io.vertx.ext.web.client.WebClientOptions;
 import org.apache.cassandra.sidecar.client.HttpClientConfig;
@@ -38,16 +38,16 @@ import org.apache.cassandra.sidecar.client.SidecarClient;
 import org.apache.cassandra.sidecar.client.SidecarClientConfig;
 import org.apache.cassandra.sidecar.client.SidecarClientConfigImpl;
 import org.apache.cassandra.sidecar.client.SidecarClientVertxRequestExecutor;
-import org.apache.cassandra.sidecar.client.SimpleSidecarInstancesProvider;
+import org.apache.cassandra.sidecar.client.SidecarInstancesProvider;
 import org.apache.cassandra.sidecar.client.VertxHttpClient;
+import org.apache.cassandra.sidecar.client.VertxRequestExecutor;
 import org.apache.cassandra.sidecar.client.retry.ExponentialBackoffRetryPolicy;
 import org.apache.cassandra.sidecar.client.retry.RetryPolicy;
-import org.apache.cassandra.sidecar.common.client.SidecarInstance;
-import org.apache.cassandra.sidecar.common.client.SidecarInstanceImpl;
 import org.apache.cassandra.sidecar.common.server.utils.SidecarVersionProvider;
 import org.apache.cassandra.sidecar.common.server.utils.ThrowableUtils;
 import org.apache.cassandra.sidecar.config.SidecarClientConfiguration;
 import org.apache.cassandra.sidecar.config.SidecarConfiguration;
+import org.apache.cassandra.sidecar.config.SslConfiguration;
 
 /**
  * Provider class for retrieving the singleton {@link SidecarClient} instance
@@ -57,21 +57,28 @@ public class SidecarClientProvider implements 
Provider<SidecarClient>
 {
     private static final Logger LOGGER = 
LoggerFactory.getLogger(SidecarClientProvider.class);
     private final Vertx vertx;
-    private final SidecarClientConfiguration clientConfig;
+    private final SidecarInstancesProvider sidecarInstancesProvider;
     private final SidecarVersionProvider sidecarVersionProvider;
     private final SidecarClient client;
+    private final WebClient webClient;
 
     private final AtomicBoolean isClosing = new AtomicBoolean(false);
+    private final WebClientOptions webClientOptions;
+    private final SidecarClientConfiguration sidecarClientConfiguration;
 
     @Inject
     public SidecarClientProvider(Vertx vertx,
                                  SidecarConfiguration sidecarConfiguration,
+                                 SidecarInstancesProvider 
sidecarInstancesProvider,
                                  SidecarVersionProvider sidecarVersionProvider)
     {
         this.vertx = vertx;
-        this.clientConfig = sidecarConfiguration.sidecarClientConfiguration();
+        this.sidecarInstancesProvider = sidecarInstancesProvider;
         this.sidecarVersionProvider = sidecarVersionProvider;
-        this.client = initializeSidecarClient();
+        this.sidecarClientConfiguration = 
sidecarConfiguration.sidecarClientConfiguration();
+        this.webClientOptions = webClientOptions(sidecarClientConfiguration);
+        this.webClient = WebClient.create(vertx, webClientOptions);
+        this.client = initializeSidecarClient(sidecarClientConfiguration);
     }
 
     @Override
@@ -80,6 +87,19 @@ public class SidecarClientProvider implements 
Provider<SidecarClient>
         return client;
     }
 
+    /**
+     * Updates the SSL Options for the client
+     *
+     * @param lastModifiedTime the time of last modification for the file
+     * @return a future with the result of the update
+     */
+    public Future<Boolean> updateSSLOptions(long lastModifiedTime)
+    {
+        SSLOptions sslOptions = webClientOptions.getSslOptions();
+        configureSSLOptions(sslOptions, 
sidecarClientConfiguration.sslConfiguration(), lastModifiedTime);
+        return webClient.updateSSLOptions(sslOptions);
+    }
+
     public void close()
     {
         if (isClosing.compareAndSet(false, true))
@@ -89,14 +109,10 @@ public class SidecarClientProvider implements 
Provider<SidecarClient>
         }
     }
 
-    private SidecarClient initializeSidecarClient()
+    private SidecarClient initializeSidecarClient(SidecarClientConfiguration 
clientConfig)
     {
-        WebClientOptions webClientOptions = webClientOptions();
-        HttpClient httpClient = vertx.createHttpClient(webClientOptions);
-        WebClient webClient = WebClient.wrap(httpClient, webClientOptions);
-
         HttpClientConfig httpClientConfig = new HttpClientConfig.Builder<>()
-                                            .ssl(webClientOptions().isSsl())
+                                            .ssl(webClientOptions.isSsl())
                                             
.timeoutMillis(clientConfig.requestTimeout().toMillis())
                                             
.idleTimeoutMillis(clientConfig.requestIdleTimeout().toIntMillis())
                                             .userAgent("cassandra-sidecar/" + 
sidecarVersionProvider.sidecarVersion())
@@ -105,26 +121,21 @@ public class SidecarClientProvider implements 
Provider<SidecarClient>
         VertxHttpClient vertxHttpClient = new VertxHttpClient(vertx, 
webClient, httpClientConfig);
         RetryPolicy defaultRetryPolicy = new 
ExponentialBackoffRetryPolicy(clientConfig.maxRetries(),
                                                                            
clientConfig.retryDelay().toMillis(),
-                                                                           
clientConfig.retryDelay().toMillis());
-        SidecarClientVertxRequestExecutor requestExecutor = new 
SidecarClientVertxRequestExecutor(vertxHttpClient);
-        SidecarInstance instance = new 
SidecarInstanceImpl(webClientOptions.getDefaultHost(), 
webClientOptions.getDefaultPort());
-        ArrayList<SidecarInstance> instances = new ArrayList<>();
-        instances.add(instance);
-        SimpleSidecarInstancesProvider instancesProvider = new 
SimpleSidecarInstancesProvider(instances);
+                                                                           
clientConfig.maxRetryDelay().toMillis());
+        VertxRequestExecutor requestExecutor = new 
SidecarClientVertxRequestExecutor(vertxHttpClient);
 
         SidecarClientConfig config = SidecarClientConfigImpl.builder()
                                                             
.retryDelayMillis(clientConfig.retryDelay().toMillis())
                                                             
.maxRetryDelayMillis(clientConfig.maxRetryDelay().toMillis())
                                                             
.maxRetries(clientConfig.maxRetries())
                                                             .build();
-
-        return new SidecarClient(instancesProvider,
+        return new SidecarClient(sidecarInstancesProvider,
                                  requestExecutor,
                                  config,
                                  defaultRetryPolicy);
     }
 
-    private WebClientOptions webClientOptions()
+    static WebClientOptions webClientOptions(SidecarClientConfiguration 
clientConfig)
     {
         WebClientOptions options = new WebClientOptions();
         options.getPoolOptions()
@@ -133,31 +144,49 @@ public class SidecarClientProvider implements 
Provider<SidecarClient>
                .setHttp1MaxSize(clientConfig.connectionPoolMaxSize())
                
.setMaxWaitQueueSize(clientConfig.connectionPoolMaxWaitQueueSize());
 
-        boolean useSsl = clientConfig.sslConfiguration() != null && 
clientConfig.sslConfiguration().enabled();
-        if (clientConfig.sslConfiguration() != null && 
clientConfig.sslConfiguration().isKeystoreConfigured())
+        SslConfiguration ssl = clientConfig.sslConfiguration();
+        if (ssl != null && ssl.enabled())
         {
-            options.setKeyStoreOptions(new 
JksOptions().setPath(clientConfig.sslConfiguration().keystore().path())
-                                                       
.setPassword(clientConfig.sslConfiguration().keystore().password()));
-            if (clientConfig.sslConfiguration().preferOpenSSL() && 
OpenSSLEngineOptions.isAvailable())
+            options.setSsl(true);
+
+            if (!ssl.secureTransportProtocols().isEmpty())
+            {
+                // Use LinkedHashSet to preserve input order
+                options.setEnabledSecureTransportProtocols(new 
LinkedHashSet<>(ssl.secureTransportProtocols()));
+            }
+
+            for (String cipherSuite : ssl.cipherSuites())
+            {
+                options.addEnabledCipherSuite(cipherSuite);
+            }
+
+            if (ssl.preferOpenSSL() && OpenSSLEngineOptions.isAvailable())
             {
                 LOGGER.info("Using OpenSSL for encryption in Webclient 
Options");
-                useSsl = true;
                 options.setSslEngineOptions(new 
OpenSSLEngineOptions().setSessionCacheEnabled(true));
             }
             else
             {
                 LOGGER.warn("OpenSSL not enabled, using JDK for TLS in 
Webclient Options");
             }
+
+            configureSSLOptions(options.getSslOptions(), ssl, 0);
         }
+        return options;
+    }
+
+    static void configureSSLOptions(SSLOptions options, SslConfiguration ssl, 
long timestamp)
+    {
+        options.setSslHandshakeTimeout(ssl.handshakeTimeout().quantity())
+               .setSslHandshakeTimeoutUnit(ssl.handshakeTimeout().unit());
 
-        if (clientConfig.sslConfiguration() != null && 
clientConfig.sslConfiguration().truststore() != null
-            && clientConfig.sslConfiguration().truststore().isConfigured())
+        if (ssl.isKeystoreConfigured())
         {
-            options.setTrustStoreOptions(new 
JksOptions().setPath(clientConfig.sslConfiguration().truststore().path())
-                                                         
.setPassword(clientConfig.sslConfiguration().truststore().password()));
+            SslUtils.setKeyStoreConfiguration(options, ssl.keystore(), 
timestamp);
+        }
+        if (ssl.isTrustStoreConfigured())
+        {
+            SslUtils.setTrustStoreConfiguration(options, ssl.truststore());
         }
-
-        options.setSsl(useSsl);
-        return options;
     }
 }
diff --git a/server/src/test/java/org/apache/cassandra/sidecar/TestModule.java 
b/server/src/test/java/org/apache/cassandra/sidecar/TestModule.java
index 7649b11b..5644f67f 100644
--- a/server/src/test/java/org/apache/cassandra/sidecar/TestModule.java
+++ b/server/src/test/java/org/apache/cassandra/sidecar/TestModule.java
@@ -23,6 +23,7 @@ import java.util.ArrayList;
 import java.util.Collections;
 import java.util.List;
 import java.util.concurrent.TimeUnit;
+import java.util.function.Function;
 
 import com.google.common.util.concurrent.SidecarRateLimiter;
 
@@ -45,7 +46,6 @@ import 
org.apache.cassandra.sidecar.common.server.StorageOperations;
 import org.apache.cassandra.sidecar.common.server.dns.DnsResolver;
 import 
org.apache.cassandra.sidecar.common.server.utils.MillisecondBoundConfiguration;
 import 
org.apache.cassandra.sidecar.common.server.utils.SecondBoundConfiguration;
-import org.apache.cassandra.sidecar.config.AccessControlConfiguration;
 import org.apache.cassandra.sidecar.config.CdcConfiguration;
 import org.apache.cassandra.sidecar.config.PeriodicTaskConfiguration;
 import org.apache.cassandra.sidecar.config.RestoreJobConfiguration;
@@ -55,7 +55,6 @@ import 
org.apache.cassandra.sidecar.config.ServiceConfiguration;
 import org.apache.cassandra.sidecar.config.SidecarConfiguration;
 import org.apache.cassandra.sidecar.config.SslConfiguration;
 import org.apache.cassandra.sidecar.config.ThrottleConfiguration;
-import org.apache.cassandra.sidecar.config.yaml.AccessControlConfigurationImpl;
 import org.apache.cassandra.sidecar.config.yaml.CdcConfigurationImpl;
 import org.apache.cassandra.sidecar.config.yaml.PeriodicTaskConfigurationImpl;
 import org.apache.cassandra.sidecar.config.yaml.RestoreJobConfigurationImpl;
@@ -102,11 +101,11 @@ public class TestModule extends AbstractModule
 
     protected SidecarConfigurationImpl abstractConfig(SslConfiguration 
sslConfiguration)
     {
-        return abstractConfig(sslConfiguration, new 
AccessControlConfigurationImpl());
+        return abstractConfig(sslConfiguration, null);
     }
 
     protected SidecarConfigurationImpl abstractConfig(SslConfiguration 
sslConfiguration,
-                                                      
AccessControlConfiguration accessControlConfiguration)
+                                                      
Function<SidecarConfigurationImpl.Builder, SidecarConfigurationImpl.Builder> 
configurationOverrides)
     {
         ThrottleConfiguration throttleConfiguration = new 
ThrottleConfigurationImpl(5, SecondBoundConfiguration.parse("5s"));
         SSTableUploadConfiguration uploadConfiguration = new 
SSTableUploadConfigurationImpl(0F);
@@ -136,13 +135,16 @@ public class TestModule extends AbstractModule
         = new PeriodicTaskConfigurationImpl(true,
                                             
MillisecondBoundConfiguration.parse("200ms"),
                                             
MillisecondBoundConfiguration.parse("1s"));
-        return SidecarConfigurationImpl.builder()
-                                       
.serviceConfiguration(serviceConfiguration)
-                                       .sslConfiguration(sslConfiguration)
-                                       
.accessControlConfiguration(accessControlConfiguration)
-                                       
.restoreJobConfiguration(restoreJobConfiguration)
-                                       
.healthCheckConfiguration(healthCheckConfiguration)
-                                       .build();
+        SidecarConfigurationImpl.Builder builder = 
SidecarConfigurationImpl.builder()
+                                                                           
.serviceConfiguration(serviceConfiguration)
+                                                                           
.sslConfiguration(sslConfiguration)
+                                                                           
.restoreJobConfiguration(restoreJobConfiguration)
+                                                                           
.healthCheckConfiguration(healthCheckConfiguration);
+        if (configurationOverrides != null)
+        {
+            builder = configurationOverrides.apply(builder);
+        }
+        return builder.build();
     }
 
     @Provides
diff --git 
a/server/src/test/java/org/apache/cassandra/sidecar/TestSslModule.java 
b/server/src/test/java/org/apache/cassandra/sidecar/TestSslModule.java
index 43b2603d..1f074ff6 100644
--- a/server/src/test/java/org/apache/cassandra/sidecar/TestSslModule.java
+++ b/server/src/test/java/org/apache/cassandra/sidecar/TestSslModule.java
@@ -58,7 +58,7 @@ public class TestSslModule extends TestModule
 
         if (!Files.exists(keyStorePath))
         {
-            logger.error("JMX password file not found in path={}", 
keyStorePath);
+            logger.error("Keystore file not found in path={}", keyStorePath);
         }
         if (!Files.exists(trustStorePath))
         {
diff --git 
a/server/src/test/java/org/apache/cassandra/sidecar/acl/authentication/MutualTLSAuthenticationHandlerTest.java
 
b/server/src/test/java/org/apache/cassandra/sidecar/acl/authentication/MutualTLSAuthenticationHandlerTest.java
index b6923406..0b44cb6c 100644
--- 
a/server/src/test/java/org/apache/cassandra/sidecar/acl/authentication/MutualTLSAuthenticationHandlerTest.java
+++ 
b/server/src/test/java/org/apache/cassandra/sidecar/acl/authentication/MutualTLSAuthenticationHandlerTest.java
@@ -333,7 +333,7 @@ class MutualTLSAuthenticationHandlerTest
                                                  
Collections.singleton(ADMIN_IDENTITY),
                                                  new CacheConfigurationImpl());
 
-            return super.abstractConfig(sslConfiguration, 
accessControlConfiguration);
+            return super.abstractConfig(sslConfiguration, builder -> 
builder.accessControlConfiguration(accessControlConfiguration));
         }
 
         @Provides
diff --git 
a/server/src/test/java/org/apache/cassandra/sidecar/utils/SidecarClientProviderTest.java
 
b/server/src/test/java/org/apache/cassandra/sidecar/utils/SidecarClientProviderTest.java
new file mode 100644
index 00000000..1f3d7187
--- /dev/null
+++ 
b/server/src/test/java/org/apache/cassandra/sidecar/utils/SidecarClientProviderTest.java
@@ -0,0 +1,223 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.cassandra.sidecar.utils;
+
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.StandardCopyOption;
+import java.time.Instant;
+import java.time.temporal.ChronoUnit;
+import java.util.concurrent.TimeUnit;
+import java.util.function.Function;
+
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.BeforeAll;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.io.TempDir;
+
+import com.google.inject.Guice;
+import com.google.inject.Injector;
+import com.google.inject.util.Modules;
+import io.vertx.core.Vertx;
+import org.apache.cassandra.sidecar.TestModule;
+import org.apache.cassandra.sidecar.client.SidecarClient;
+import org.apache.cassandra.sidecar.common.client.SidecarInstanceImpl;
+import org.apache.cassandra.sidecar.common.response.HealthResponse;
+import 
org.apache.cassandra.sidecar.common.server.utils.SecondBoundConfiguration;
+import org.apache.cassandra.sidecar.config.SidecarClientConfiguration;
+import org.apache.cassandra.sidecar.config.SslConfiguration;
+import org.apache.cassandra.sidecar.config.yaml.KeyStoreConfigurationImpl;
+import org.apache.cassandra.sidecar.config.yaml.SidecarClientConfigurationImpl;
+import org.apache.cassandra.sidecar.config.yaml.SidecarConfigurationImpl;
+import org.apache.cassandra.sidecar.config.yaml.SslConfigurationImpl;
+import org.apache.cassandra.sidecar.modules.SidecarModules;
+import org.apache.cassandra.sidecar.server.Server;
+import org.apache.cassandra.testing.utils.tls.CertificateBuilder;
+import org.apache.cassandra.testing.utils.tls.CertificateBundle;
+
+import static org.apache.cassandra.testing.utils.AssertionUtils.getBlocking;
+import static org.apache.cassandra.testing.utils.AssertionUtils.loopAssert;
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assertions.assertThatThrownBy;
+import static org.assertj.core.api.Assertions.fail;
+
+/**
+ * Unit test for the {@link SidecarClientProvider} class
+ */
+class SidecarClientProviderTest
+{
+    public static final char[] EMPTY_PASSWORD = new char[0];
+
+    @TempDir
+    static Path secretsPath;
+    static Path truststorePath;
+    static Path serverKeyStorePath;
+    static Path validClientCertPath;
+    static Path clientCertPath;
+
+    Injector injector;
+    private Vertx vertx;
+    private Server server;
+
+    SidecarClient client;
+    TestModule testModule;
+
+    private SidecarClientProvider provider;
+
+    @BeforeAll
+    static void configureCertificates() throws Exception
+    {
+        CertificateBundle certificateAuthority = new 
CertificateBuilder().subject("CN=Apache Cassandra Root CA, OU=Certification 
Authority, O=Unknown, C=Unknown")
+                                                                         
.alias("fakerootca")
+                                                                         
.isCertificateAuthority(true)
+                                                                         
.buildSelfSigned();
+        truststorePath = certificateAuthority.toTempKeyStorePath(secretsPath, 
EMPTY_PASSWORD, EMPTY_PASSWORD);
+
+        CertificateBuilder serverKeyStoreBuilder =
+        new CertificateBuilder().subject("CN=Apache Cassandra, OU=mtls_test, 
O=Unknown, L=Unknown, ST=Unknown, C=Unknown")
+                                .addSanDnsName("localhost");
+        CertificateBundle serverKeyStore = 
serverKeyStoreBuilder.buildIssuedBy(certificateAuthority);
+        serverKeyStorePath = serverKeyStore.toTempKeyStorePath(secretsPath, 
EMPTY_PASSWORD, EMPTY_PASSWORD);
+
+        CertificateBundle expiredClientKeyStore = new 
CertificateBuilder().subject("CN=Apache Cassandra, OU=mtls_test, O=Unknown, 
L=Unknown, ST=Unknown, C=Unknown")
+                                                                          
.addSanDnsName("localhost")
+                                                                          
.notBefore(Instant.now().minus(7, ChronoUnit.DAYS))
+                                                                          
.notAfter(Instant.now().minus(1, ChronoUnit.DAYS))
+                                                                          
.buildIssuedBy(certificateAuthority);
+        // Assign the expired client cert to the cert path
+        clientCertPath = expiredClientKeyStore.toTempKeyStorePath(secretsPath, 
EMPTY_PASSWORD, EMPTY_PASSWORD);
+
+        CertificateBundle validClientKeyStore = new 
CertificateBuilder().subject("CN=Apache Cassandra, OU=mtls_test, O=Unknown, 
L=Unknown, ST=Unknown, C=Unknown")
+                                                                        
.addSanDnsName("localhost")
+                                                                        
.buildIssuedBy(certificateAuthority);
+        validClientCertPath = 
validClientKeyStore.toTempKeyStorePath(secretsPath, EMPTY_PASSWORD, 
EMPTY_PASSWORD);
+    }
+
+    @BeforeEach
+    void setup()
+    {
+        testModule = new SidecarClientProviderModule();
+
+        injector = 
Guice.createInjector(Modules.override(SidecarModules.all()).with(testModule));
+        vertx = injector.getInstance(Vertx.class);
+        server = getMTLSServerAndStart();
+        provider = injector.getInstance(SidecarClientProvider.class);
+        client = provider.get();
+    }
+
+    @AfterEach
+    void cleanup()
+    {
+        if (server != null)
+        {
+            getBlocking(server.close(), 10, TimeUnit.SECONDS, "Close server");
+        }
+        getBlocking(vertx.close(), 10, TimeUnit.SECONDS, "Close vertx");
+    }
+
+    @Test
+    void testSidecarClientIsSingleton()
+    {
+        SidecarClient client1 = provider.get();
+        SidecarClient client2 = provider.get();
+
+        assertThat(client1).isSameAs(client2);
+    }
+
+    @Test
+    void testHotReloadOfClientCerts() throws Exception
+    {
+        // the certificate should be expired at the beginning of the test
+        unsuccessfulClientRequest(client);
+
+        // Replace the expired certificated with a good certificate we can use
+        Files.copy(validClientCertPath, clientCertPath, 
StandardCopyOption.REPLACE_EXISTING);
+
+        // Wait until the client reloads the certificate
+        loopAssert(10, () -> successfulClientRequest(client));
+
+        // Execute requests with the client. We should see successful requests 
go through now
+        successfulClientRequest(client);
+    }
+
+    private void unsuccessfulClientRequest(SidecarClient client)
+    {
+        assertThatThrownBy(() -> client.sidecarHealth(new 
SidecarInstanceImpl("localhost", server.actualPort())).get(30, 
TimeUnit.SECONDS))
+        .describedAs("Unsuccessful client requests are expected to fail")
+        .isNotNull();
+    }
+
+    private void successfulClientRequest(SidecarClient client)
+    {
+        HealthResponse healthResponse = null;
+        try
+        {
+            healthResponse = client.sidecarHealth(new 
SidecarInstanceImpl("localhost", server.actualPort())).get(30, 
TimeUnit.SECONDS);
+        }
+        catch (Exception exception)
+        {
+            fail("Client request was expected to succeed", exception);
+        }
+        assertThat(healthResponse).isNotNull();
+        assertThat(healthResponse.isOk()).isTrue();
+    }
+
+    Server getMTLSServerAndStart()
+    {
+        // Start server and wait for it to be running
+        Server server = injector.getInstance(Server.class);
+        getBlocking(server.start(), 30, TimeUnit.SECONDS, "Server start");
+        return server;
+    }
+
+    static class SidecarClientProviderModule extends TestModule
+    {
+        @Override
+        public SidecarConfigurationImpl abstractConfig()
+        {
+            SslConfiguration serverSslConfiguration =
+            SslConfigurationImpl.builder()
+                                .enabled(true)
+                                .useOpenSsl(true)
+                                
.handshakeTimeout(SecondBoundConfiguration.parse("10s"))
+                                .clientAuth("REQUIRED")
+                                .keystore(new 
KeyStoreConfigurationImpl(serverKeyStorePath.toAbsolutePath().toString(), ""))
+                                .truststore(new 
KeyStoreConfigurationImpl(truststorePath.toAbsolutePath().toString(), ""))
+                                .build();
+
+            Function<SidecarConfigurationImpl.Builder, 
SidecarConfigurationImpl.Builder> configOverrides =
+            builder -> {
+                String type = "PKCS12";
+                SecondBoundConfiguration checkInterval = 
SecondBoundConfiguration.ONE;
+
+                SslConfiguration clientSslConfiguration =
+                SslConfigurationImpl.builder()
+                                    .enabled(true)
+                                    .useOpenSsl(true)
+                                    .keystore(new 
KeyStoreConfigurationImpl(clientCertPath.toAbsolutePath().toString(), "", type, 
checkInterval))
+                                    .truststore(new 
KeyStoreConfigurationImpl(truststorePath.toAbsolutePath().toString(), "", type, 
checkInterval))
+                                    .build();
+                SidecarClientConfiguration sidecarClientConfiguration = new 
SidecarClientConfigurationImpl(clientSslConfiguration);
+                return 
builder.sidecarClientConfiguration(sidecarClientConfiguration);
+            };
+            return super.abstractConfig(serverSslConfiguration, 
configOverrides);
+        }
+    }
+}


---------------------------------------------------------------------
To unsubscribe, e-mail: commits-unsubscr...@cassandra.apache.org
For additional commands, e-mail: commits-h...@cassandra.apache.org


Reply via email to