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 <[email protected]>
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: [email protected]
For additional commands, e-mail: [email protected]