This is an automated email from the ASF dual-hosted git repository.
chibenwa pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/james-project.git
The following commit(s) were added to refs/heads/master by this push:
new 0a26ed2f8d [PERF] Allow using tcnative with Cassandra driver (#3181)
0a26ed2f8d is described below
commit 0a26ed2f8d169ec0d9628035c1f05d8632d385f9
Author: Benoit TELLIER <[email protected]>
AuthorDate: Mon Sep 14 16:22:00 2026 +0200
[PERF] Allow using tcnative with Cassandra driver (#3181)
---
.../backends/cassandra/init/ClusterFactory.java | 22 +++
.../cassandra/init/TcNativeSslEngineFactory.java | 161 +++++++++++++++++++++
.../pages/distributed/configure/cassandra.adoc | 9 ++
3 files changed, 192 insertions(+)
diff --git
a/backends-common/cassandra/src/main/java/org/apache/james/backends/cassandra/init/ClusterFactory.java
b/backends-common/cassandra/src/main/java/org/apache/james/backends/cassandra/init/ClusterFactory.java
index a9b5a0223f..d3d25c1b04 100644
---
a/backends-common/cassandra/src/main/java/org/apache/james/backends/cassandra/init/ClusterFactory.java
+++
b/backends-common/cassandra/src/main/java/org/apache/james/backends/cassandra/init/ClusterFactory.java
@@ -23,12 +23,18 @@ import java.net.InetSocketAddress;
import
org.apache.james.backends.cassandra.init.configuration.ClusterConfiguration;
import
org.apache.james.backends.cassandra.init.configuration.KeyspaceConfiguration;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
import com.datastax.oss.driver.api.core.CqlSession;
import com.datastax.oss.driver.api.core.CqlSessionBuilder;
+import com.datastax.oss.driver.api.core.config.DriverConfigLoader;
+import com.datastax.oss.driver.api.core.config.DriverExecutionProfile;
import com.google.common.base.Preconditions;
public class ClusterFactory {
+ private static final Logger LOGGER =
LoggerFactory.getLogger(ClusterFactory.class);
+ private static final boolean TCNATIVE_ENABLED =
Boolean.parseBoolean(System.getProperty("james.cassandra.tcnative.enabled",
"false"));
public static CqlSession create(ClusterConfiguration configuration,
KeyspaceConfiguration keyspaceConfiguration) {
Preconditions.checkState(configuration.getUsername().isPresent() ==
configuration.getPassword().isPresent(), "If you specify username, you must
specify password");
@@ -44,6 +50,7 @@ public class ClusterFactory {
sessionBuilder.withAuthCredentials(username, password)));
sessionBuilder.withLocalDatacenter(configuration.getLocalDC().orElse("datacenter1"));
+ configureTcNative(sessionBuilder);
createKeyspace(keyspaceConfiguration, sessionBuilder);
@@ -73,6 +80,7 @@ public class ClusterFactory {
sessionBuilder.withAuthCredentials(username, password)));
sessionBuilder.withLocalDatacenter(configuration.getLocalDC().orElse("datacenter1"));
+ configureTcNative(sessionBuilder);
CqlSession session = sessionBuilder.build();
@@ -85,6 +93,20 @@ public class ClusterFactory {
}
}
+ private static void configureTcNative(CqlSessionBuilder sessionBuilder) {
+ if (!TCNATIVE_ENABLED) {
+ return;
+ }
+ try (DriverConfigLoader configLoader =
DriverConfigLoader.fromDefaults(ClusterFactory.class.getClassLoader())) {
+ DriverExecutionProfile profile =
configLoader.getInitialConfig().getDefaultProfile();
+ if (TcNativeSslEngineFactory.supports(profile)) {
+ sessionBuilder.withSslEngineFactory(new
TcNativeSslEngineFactory(profile));
+ } else {
+ LOGGER.warn("james.cassandra.tcnative.enabled ignored:
requires advanced.ssl-engine-factory.class = DefaultSslEngineFactory without
keystore-reload-interval");
+ }
+ }
+ }
+
private static void createKeyspace(KeyspaceConfiguration
keyspaceConfiguration, CqlSessionBuilder sessionBuilder) {
CqlSession cqlSession = sessionBuilder.build();
KeyspaceFactory.createKeyspace(keyspaceConfiguration,
cqlSession).block();
diff --git
a/backends-common/cassandra/src/main/java/org/apache/james/backends/cassandra/init/TcNativeSslEngineFactory.java
b/backends-common/cassandra/src/main/java/org/apache/james/backends/cassandra/init/TcNativeSslEngineFactory.java
new file mode 100644
index 0000000000..e78734cf37
--- /dev/null
+++
b/backends-common/cassandra/src/main/java/org/apache/james/backends/cassandra/init/TcNativeSslEngineFactory.java
@@ -0,0 +1,161 @@
+/****************************************************************
+ * 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.james.backends.cassandra.init;
+
+import java.io.InputStream;
+import java.net.InetSocketAddress;
+import java.net.SocketAddress;
+import java.nio.file.Files;
+import java.nio.file.Paths;
+import java.security.KeyStore;
+import java.util.Optional;
+
+import javax.net.ssl.KeyManagerFactory;
+import javax.net.ssl.SSLEngine;
+import javax.net.ssl.SSLParameters;
+import javax.net.ssl.TrustManagerFactory;
+
+import com.datastax.oss.driver.api.core.config.DefaultDriverOption;
+import com.datastax.oss.driver.api.core.config.DriverExecutionProfile;
+import com.datastax.oss.driver.api.core.context.DriverContext;
+import com.datastax.oss.driver.api.core.metadata.EndPoint;
+import com.datastax.oss.driver.api.core.ssl.SslEngineFactory;
+import com.datastax.oss.driver.internal.core.ssl.DefaultSslEngineFactory;
+
+import io.netty.buffer.ByteBufAllocator;
+import io.netty.handler.ssl.IdentityCipherSuiteFilter;
+import io.netty.handler.ssl.OpenSsl;
+import io.netty.handler.ssl.SslContext;
+import io.netty.handler.ssl.SslContextBuilder;
+import io.netty.handler.ssl.SslProvider;
+
+/**
+ * BoringSSL (Netty tcnative) counterpart of the driver's {@link
DefaultSslEngineFactory}, reading the same
+ * {@code advanced.ssl-engine-factory} options.
+ *
+ * <p>Keystore reloading ({@code keystore-reload-interval}) is not supported.
+ *
+ * <p>Requires {@code netty-tcnative-boringssl-static} on the runtime
classpath.
+ */
+public class TcNativeSslEngineFactory implements SslEngineFactory {
+ public static boolean supports(DriverExecutionProfile profile) {
+ return profile.isDefined(DefaultDriverOption.SSL_ENGINE_FACTORY_CLASS)
+ &&
isDefaultSslEngineFactory(profile.getString(DefaultDriverOption.SSL_ENGINE_FACTORY_CLASS))
+ &&
!profile.isDefined(DefaultDriverOption.SSL_KEYSTORE_RELOAD_INTERVAL);
+ }
+
+ private static boolean isDefaultSslEngineFactory(String className) {
+ return className.equals(DefaultSslEngineFactory.class.getSimpleName())
+ || className.equals(DefaultSslEngineFactory.class.getName());
+ }
+
+ private final SslContext sslContext;
+ private final boolean requireHostnameValidation;
+ private final boolean allowDnsReverseLookupSan;
+
+ public TcNativeSslEngineFactory(DriverContext driverContext) {
+ this(driverContext.getConfig().getDefaultProfile());
+ }
+
+ public TcNativeSslEngineFactory(DriverExecutionProfile profile) {
+ OpenSsl.ensureAvailability();
+ try {
+ this.sslContext = buildContext(profile);
+ } catch (Exception e) {
+ throw new IllegalStateException("Cannot initialize SSL Context",
e);
+ }
+ this.requireHostnameValidation =
profile.getBoolean(DefaultDriverOption.SSL_HOSTNAME_VALIDATION, true);
+ this.allowDnsReverseLookupSan =
profile.getBoolean(DefaultDriverOption.SSL_ALLOW_DNS_REVERSE_LOOKUP_SAN, true);
+ }
+
+ private static SslContext buildContext(DriverExecutionProfile profile)
throws Exception {
+ SslContextBuilder builder = SslContextBuilder.forClient()
+ .sslProvider(SslProvider.OPENSSL);
+
+ Optional<KeyStore> trustStore = loadStore(profile,
DefaultDriverOption.SSL_TRUSTSTORE_PATH,
DefaultDriverOption.SSL_TRUSTSTORE_PASSWORD);
+ if (trustStore.isPresent()) {
+ TrustManagerFactory trustManagerFactory =
TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm());
+ trustManagerFactory.init(trustStore.get());
+ builder.trustManager(trustManagerFactory);
+ }
+
+ Optional<KeyStore> keyStore = loadStore(profile,
DefaultDriverOption.SSL_KEYSTORE_PATH,
DefaultDriverOption.SSL_KEYSTORE_PASSWORD);
+ if (keyStore.isPresent()) {
+ KeyManagerFactory keyManagerFactory =
KeyManagerFactory.getInstance(KeyManagerFactory.getDefaultAlgorithm());
+ keyManagerFactory.init(keyStore.get(), password(profile,
DefaultDriverOption.SSL_KEYSTORE_PASSWORD));
+ builder.keyManager(keyManagerFactory);
+ }
+
+ if (profile.isDefined(DefaultDriverOption.SSL_CIPHER_SUITES)) {
+ // IdentityCipherSuiteFilter: see TCNativeEncryptionFactory, the
default filter strips TLS 1.3 suites
+
builder.ciphers(profile.getStringList(DefaultDriverOption.SSL_CIPHER_SUITES),
IdentityCipherSuiteFilter.INSTANCE);
+ }
+ return builder.build();
+ }
+
+ private static Optional<KeyStore> loadStore(DriverExecutionProfile
profile, DefaultDriverOption pathOption, DefaultDriverOption passwordOption)
throws Exception {
+ if (!profile.isDefined(pathOption)) {
+ return Optional.empty();
+ }
+ try (InputStream inputStream =
Files.newInputStream(Paths.get(profile.getString(pathOption)))) {
+ KeyStore keyStore = KeyStore.getInstance("JKS");
+ keyStore.load(inputStream, password(profile, passwordOption));
+ return Optional.of(keyStore);
+ }
+ }
+
+ private static char[] password(DriverExecutionProfile profile,
DefaultDriverOption passwordOption) {
+ if (profile.isDefined(passwordOption)) {
+ return profile.getString(passwordOption).toCharArray();
+ }
+ return null;
+ }
+
+ @Override
+ public SSLEngine newSslEngine(EndPoint remoteEndpoint) {
+ SSLEngine engine = createEngine(remoteEndpoint.resolve());
+ if (requireHostnameValidation) {
+ SSLParameters parameters = engine.getSSLParameters();
+ parameters.setEndpointIdentificationAlgorithm("HTTPS");
+ engine.setSSLParameters(parameters);
+ }
+ return engine;
+ }
+
+ private SSLEngine createEngine(SocketAddress remoteAddress) {
+ if (remoteAddress instanceof InetSocketAddress inetAddress) {
+ return sslContext.newEngine(ByteBufAllocator.DEFAULT,
hostname(inetAddress), inetAddress.getPort());
+ }
+ return sslContext.newEngine(ByteBufAllocator.DEFAULT);
+ }
+
+ private String hostname(InetSocketAddress address) {
+ if (allowDnsReverseLookupSan) {
+ return address.getHostName();
+ }
+ return address.getHostString();
+ }
+
+ @Override
+ public void close() {
+ // SslProvider.OPENSSL contexts and engines are released by the GC:
nothing to close, and the
+ // factory stays usable when ClusterFactory reuses its session builder
+ }
+}
diff --git a/docs/modules/servers/pages/distributed/configure/cassandra.adoc
b/docs/modules/servers/pages/distributed/configure/cassandra.adoc
index 9821b4d0cc..c7ce210d63 100644
--- a/docs/modules/servers/pages/distributed/configure/cassandra.adoc
+++ b/docs/modules/servers/pages/distributed/configure/cassandra.adoc
@@ -54,6 +54,15 @@ Falls back to default read consistency level if the blob is
missing. Defaults to
|===
+== Native TLS (tcnative)
+
+Set `james.cassandra.tcnative.enabled=true` in *jvm.properties* to encrypt
Cassandra client connections with BoringSSL
+(Netty tcnative) instead of the JDK. This requires
`netty-tcnative-boringssl-static` on the classpath.
+
+It only applies when *cassandra-driver.conf* uses
`advanced.ssl-engine-factory.class = DefaultSslEngineFactory` without
+`keystore-reload-interval`. The truststore, keystore, hostname validation and
cipher suite options stay the same.
+Otherwise, a warning is logged and the JDK implementation is kept.
+
== Cassandra Mailbox Configuration
*cassandra.properties* file furthermore expose some options to tune the
Cassandra Mailbox behaviour.
---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]