diff --git a/CHANGES.txt b/CHANGES.txt index 8aab7a4e8c..75dd48aa02 100644 --- a/CHANGES.txt +++ b/CHANGES.txt @@ -1,4 +1,5 @@ 5.1 + * Make JMX SSL configurable in cassandra.yaml (CASSANDRA-18508) * Fix cqlsh CAPTURE command to save query results without trace details when TRACING is ON (CASSANDRA-19105) * Optionally prevent tombstone purging during repair (CASSANDRA-20071) * Add post-filtering support for the IN operator in SAI queries (CASSANDRA-20025) diff --git a/NEWS.txt b/NEWS.txt index 8d9e620801..b4511c3bba 100644 --- a/NEWS.txt +++ b/NEWS.txt @@ -88,6 +88,7 @@ New features generate a password of configured password strength policy upon role creation or alteration when 'GENERATED PASSWORD' clause is used. Character sets supported are: English, Cyrillic, modern Cyrillic, German, Polish and Czech. + - JMX SSL configuration can be now done in cassandra.yaml via jmx_encryption_options section instead of cassandra-env.sh Upgrading diff --git a/conf/cassandra-env.sh b/conf/cassandra-env.sh index 4e28e687f9..98b7ed8ccd 100644 --- a/conf/cassandra-env.sh +++ b/conf/cassandra-env.sh @@ -236,6 +236,11 @@ else JVM_OPTS="$JVM_OPTS -Dcom.sun.management.jmxremote.authenticate=true" # jmx ssl options + # Consider using the jmx_encryption_options section of cassandra.yaml instead + # to prevent sensitive information being exposed. + # In case jmx ssl options are configured in both the places - this file and cassandra.yaml, and + # if com.sun.management.jmxremote.ssl is configured to be true here and encryption_options are marked enabled in + # cassandra.yaml, then we will get exception at the startup #JVM_OPTS="$JVM_OPTS -Dcom.sun.management.jmxremote.ssl=true" #JVM_OPTS="$JVM_OPTS -Dcom.sun.management.jmxremote.ssl.need.client.auth=true" #JVM_OPTS="$JVM_OPTS -Dcom.sun.management.jmxremote.ssl.enabled.protocols=" diff --git a/conf/cassandra.yaml b/conf/cassandra.yaml index 9c752b7d54..b93b41fa0b 100644 --- a/conf/cassandra.yaml +++ b/conf/cassandra.yaml @@ -1712,6 +1712,24 @@ client_encryption_options: # warnings will be reported during the session establishment. # certificate_validity_warn_threshold: 10d +# Configure SSL for JMX +# +# Using system properties via cassandra-env.sh to configure JMX SSL is +# considered legacy and only supported for backward compatibility. If SSL +# is enabled via both methods, a configuration error will occur at +# startup. Hot reloading of the `SSLContext` is not yet supported for the +# JMX SSL. +# Similar to `client/server_encryption_options`, you can specify PEM-based +# key material or customize the SSL configuration using `ssl_context_factory` in `jmx_encryption_options`. +#jmx_encryption_options: +# enabled: true +# cipher_suites: [TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384,TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256] +# accepted_protocols: [TLSv1.2,TLSv1.3,TLSv1.1] +# keystore: conf/cassandra_ssl.keystore +# keystore_password: cassandra +# truststore: conf/cassandra_ssl.truststore +# truststore_password: cassandra + # internode_compression controls whether traffic between nodes is # compressed. # Can be: diff --git a/conf/cassandra_latest.yaml b/conf/cassandra_latest.yaml index 39bce459cc..2ad1a25f07 100644 --- a/conf/cassandra_latest.yaml +++ b/conf/cassandra_latest.yaml @@ -1676,6 +1676,24 @@ client_encryption_options: # TLS_RSA_WITH_AES_256_CBC_SHA # ] +# Configure SSL for JMX +# +# Using system properties via cassandra-env.sh to configure JMX SSL is +# considered legacy and only supported for backward compatibility. If SSL +# is enabled via both methods, a configuration error will occur at +# startup. Hot reloading of the `SSLContext` is not yet supported for the +# JMX SSL. +# Similar to `client/server_encryption_options`, you can specify PEM-based +# key material or customize the SSL configuration using `ssl_context_factory` in `jmx_encryption_options`. +#jmx_encryption_options: +# enabled: true +# cipher_suites: [TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384,TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256] +# accepted_protocols: [TLSv1.2,TLSv1.3,TLSv1.1] +# keystore: conf/cassandra_ssl.keystore +# keystore_password: cassandra +# truststore: conf/cassandra_ssl.truststore +# truststore_password: cassandra + # internode_compression controls whether traffic between nodes is # compressed. # Can be: diff --git a/doc/modules/cassandra/pages/managing/operating/security.adoc b/doc/modules/cassandra/pages/managing/operating/security.adoc index a425cab8d0..e7748e93fb 100644 --- a/doc/modules/cassandra/pages/managing/operating/security.adoc +++ b/doc/modules/cassandra/pages/managing/operating/security.adoc @@ -78,7 +78,8 @@ files having the required PEM data as shown below, * Configuration: PEM keys/certs defined in-line (mind the spaces in the YAML!) -.... +[source,yaml] +---- client/server_encryption_options: ssl_context_factory: class_name: org.apache.cassandra.security.PEMBasedSslContextFactory @@ -97,17 +98,19 @@ YAML!) -----BEGIN CERTIFICATE----- -----END CERTIFICATE----- -.... +---- * Configuration: PEM keys/certs defined in files -.... + +[source,yaml] +---- client/server_encryption_options: ssl_context_factory: class_name: org.apache.cassandra.security.PEMBasedSslContextFactory keystore: keystore_password: "" truststore: -.... +---- == SSL Certificate Hot Reloading @@ -564,8 +567,61 @@ See also: xref:cassandra:developing/cql/security.adoc#permissions[`Permissions`] === JMX With SSL -JMX SSL configuration is controlled by a number of system properties, -some of which are optional. To turn on SSL, edit the relevant lines in +JMX SSL configuration should be specified in `cassandra.yaml` under +`encryption_options`, similar to how `client/server_encryption_options` +are configured. Using system properties to configure JMX SSL is +considered legacy and only supported for backward compatibility. If SSL +is enabled via both methods, a configuration error will occur at +startup. Hot reloading of the `SSLContext` is not yet supported for the +JMX SSL. + +==== JMX SSL configuration in cassandra.yaml +Below is an example of configuring JMX SSL in `cassandra.yaml` + +[source,yaml] +---- +jmx_encryption_options: + enabled: true + cipher_suites: [TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384,TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256] + accepted_protocols: [TLSv1.2,TLSv1.3,TLSv1.1] + keystore: test/conf/cassandra_ssl_test.keystore + keystore_password: cassandra + truststore: test/conf/cassandra_ssl_test.truststore + truststore_password: cassandra +---- + +Similar to `client/server_encryption_options`, you can specify PEM-based +key material or customize the SSL configuration using +`ssl_context_factory` in `jmx_encryption_options`. + +Below is an example of configuring PEM based key material, +[source,yaml] +---- +jmx_encryption_options: + enabled: true + cipher_suites: [TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384,TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256] + accepted_protocols: [TLSv1.2,TLSv1.3,TLSv1.1] + ssl_context_factory: + class_name: org.apache.cassandra.security.PEMBasedSslContextFactory + parameters: + private_key: | + -----BEGIN ENCRYPTED PRIVATE KEY----- OR -----BEGIN PRIVATE KEY----- + + -----END ENCRYPTED PRIVATE KEY----- OR -----END PRIVATE KEY----- + -----BEGIN CERTIFICATE----- + + -----END CERTIFICATE----- + + private_key_password: "" + + trusted_certificates: | + -----BEGIN CERTIFICATE----- + + -----END CERTIFICATE----- +---- + +==== (Legacy) JMX SSL configuration with system properties +To turn on SSL this way, edit the relevant lines in `cassandra-env.sh` to uncomment and set the values of these properties as required: diff --git a/doc/scripts/convert_yaml_to_adoc.py b/doc/scripts/convert_yaml_to_adoc.py index bf5e161488..061706f835 100644 --- a/doc/scripts/convert_yaml_to_adoc.py +++ b/doc/scripts/convert_yaml_to_adoc.py @@ -49,6 +49,7 @@ COMPLEX_OPTIONS = ( 'hints_compression', 'server_encryption_options', 'client_encryption_options', + 'jmx_encryption_options', 'transparent_data_encryption_options', 'hinted_handoff_disabled_datacenters', 'startup_checks', diff --git a/examples/ssl-factory/test/unit/org/apache/cassandra/security/KubernetesSecretsPEMSslContextFactoryTest.java b/examples/ssl-factory/test/unit/org/apache/cassandra/security/KubernetesSecretsPEMSslContextFactoryTest.java index 5ecec4ed26..ef20d425bb 100644 --- a/examples/ssl-factory/test/unit/org/apache/cassandra/security/KubernetesSecretsPEMSslContextFactoryTest.java +++ b/examples/ssl-factory/test/unit/org/apache/cassandra/security/KubernetesSecretsPEMSslContextFactoryTest.java @@ -114,7 +114,7 @@ public class KubernetesSecretsPEMSslContextFactoryTest * In order to test with real 'env' variables comment out this line and set appropriate env variable. This is * done to avoid having a dependency on env in the unit test. */ - commonConfig.put("require_client_auth", "false"; + commonConfig.put("require_client_auth", "false"); commonConfig.put("cipher_suites", Arrays.asList("TLS_RSA_WITH_AES_128_CBC_SHA")); } diff --git a/src/java/org/apache/cassandra/config/Config.java b/src/java/org/apache/cassandra/config/Config.java index 119cfbfb7d..ca4cf09be4 100644 --- a/src/java/org/apache/cassandra/config/Config.java +++ b/src/java/org/apache/cassandra/config/Config.java @@ -434,6 +434,7 @@ public class Config public EncryptionOptions.ServerEncryptionOptions server_encryption_options = new EncryptionOptions.ServerEncryptionOptions(); public EncryptionOptions client_encryption_options = new EncryptionOptions(); + public EncryptionOptions jmx_encryption_options = new EncryptionOptions(); public InternodeCompression internode_compression = InternodeCompression.none; @@ -1320,6 +1321,7 @@ public class Config private static final Set SENSITIVE_KEYS = new HashSet() {{ add("client_encryption_options"); add("server_encryption_options"); + add("jmx_encryption_options"); }}; public static void log(Config config) diff --git a/src/java/org/apache/cassandra/config/DatabaseDescriptor.java b/src/java/org/apache/cassandra/config/DatabaseDescriptor.java index 784a079320..3cd003b97b 100644 --- a/src/java/org/apache/cassandra/config/DatabaseDescriptor.java +++ b/src/java/org/apache/cassandra/config/DatabaseDescriptor.java @@ -960,6 +960,9 @@ public class DatabaseDescriptor if (conf.client_encryption_options != null) conf.client_encryption_options.applyConfig(); + if (conf.jmx_encryption_options != null) + conf.jmx_encryption_options.applyConfig(); + if (conf.snapshot_links_per_second < 0) throw new ConfigurationException("snapshot_links_per_second must be >= 0"); @@ -1306,7 +1309,14 @@ public class DatabaseDescriptor { SSLFactory.validateSslContext("Internode messaging", conf.server_encryption_options, REQUIRED, true); SSLFactory.validateSslContext("Native transport", conf.client_encryption_options, conf.client_encryption_options.getClientAuth(), true); + // For JMX SSL the validation is pretty much the same as the Native transport + SSLFactory.validateSslContext("JMX transport", conf.jmx_encryption_options, conf.jmx_encryption_options.getClientAuth(), true); SSLFactory.initHotReloading(conf.server_encryption_options, conf.client_encryption_options, false); + /* + For JMX SSL, the hot reloading of the SSLContext is out of scope for CASSANDRA-18508. + Since JMXServerUtil that initializes the JMX Server is used statically, it may require significant + effort to change that behavior unlike SSLFactory used for Native transport/Internode messaging. + */ } catch (IOException e) { @@ -3653,6 +3663,11 @@ public class DatabaseDescriptor return conf.client_encryption_options; } + public static EncryptionOptions getJmxEncryptionOptions() + { + return conf.jmx_encryption_options; + } + @VisibleForTesting public static void updateNativeProtocolEncryptionOptions(Function update) { diff --git a/src/java/org/apache/cassandra/config/EncryptionOptions.java b/src/java/org/apache/cassandra/config/EncryptionOptions.java index fdc5c0efc6..eeac859f31 100644 --- a/src/java/org/apache/cassandra/config/EncryptionOptions.java +++ b/src/java/org/apache/cassandra/config/EncryptionOptions.java @@ -172,10 +172,10 @@ public class EncryptionOptions ConfigKey(String keyName) { - this.keyName=keyName; + this.keyName = keyName; } - String getKeyName() + public String toString() { return keyName; } @@ -184,8 +184,9 @@ public class EncryptionOptions { Set valueSet = new HashSet<>(); ConfigKey[] values = values(); - for(ConfigKey key: values) { - valueSet.add(toLowerCaseLocalized(key.getKeyName())); + for (ConfigKey key : values) + { + valueSet.add(toLowerCaseLocalized(key.toString())); } return valueSet; } @@ -357,7 +358,7 @@ public class EncryptionOptions protected static void putSslContextFactoryParameter(Map existingParameters, ConfigKey configKey, Object value) { if (value != null) { - existingParameters.put(configKey.getKeyName(), value); + existingParameters.put(configKey.toString(), value); } } diff --git a/src/java/org/apache/cassandra/utils/JMXServerUtils.java b/src/java/org/apache/cassandra/utils/JMXServerUtils.java index 78c8ced8d2..43221f28bf 100644 --- a/src/java/org/apache/cassandra/utils/JMXServerUtils.java +++ b/src/java/org/apache/cassandra/utils/JMXServerUtils.java @@ -37,24 +37,25 @@ import java.rmi.registry.Registry; import java.rmi.server.RMIClientSocketFactory; import java.rmi.server.RMIServerSocketFactory; import java.rmi.server.UnicastRemoteObject; -import java.util.Arrays; import java.util.HashMap; import java.util.Map; -import java.util.stream.Collectors; -import javax.management.remote.*; +import javax.management.remote.JMXAuthenticator; +import javax.management.remote.JMXConnectorServer; +import javax.management.remote.JMXServiceURL; +import javax.management.remote.MBeanServerForwarder; import javax.management.remote.rmi.RMIConnectorServer; import javax.management.remote.rmi.RMIJRMPServerImpl; -import javax.rmi.ssl.SslRMIClientSocketFactory; -import javax.rmi.ssl.SslRMIServerSocketFactory; +import javax.net.ssl.SSLException; import javax.security.auth.Subject; import com.google.common.annotations.VisibleForTesting; import com.google.common.collect.ImmutableMap; -import org.apache.commons.lang3.StringUtils; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.apache.cassandra.auth.jmx.AuthenticationProxy; +import org.apache.cassandra.config.DatabaseDescriptor; +import org.apache.cassandra.utils.jmx.DefaultJmxSocketFactory; import static org.apache.cassandra.config.CassandraRelevantProperties.CASSANDRA_JMX_AUTHORIZER; import static org.apache.cassandra.config.CassandraRelevantProperties.CASSANDRA_JMX_REMOTE_LOGIN_CONFIG; @@ -62,12 +63,6 @@ import static org.apache.cassandra.config.CassandraRelevantProperties.COM_SUN_MA import static org.apache.cassandra.config.CassandraRelevantProperties.COM_SUN_MANAGEMENT_JMXREMOTE_AUTHENTICATE; import static org.apache.cassandra.config.CassandraRelevantProperties.COM_SUN_MANAGEMENT_JMXREMOTE_PASSWORD_FILE; import static org.apache.cassandra.config.CassandraRelevantProperties.COM_SUN_MANAGEMENT_JMXREMOTE_RMI_PORT; -import static org.apache.cassandra.config.CassandraRelevantProperties.COM_SUN_MANAGEMENT_JMXREMOTE_SSL; -import static org.apache.cassandra.config.CassandraRelevantProperties.COM_SUN_MANAGEMENT_JMXREMOTE_SSL_ENABLED_CIPHER_SUITES; -import static org.apache.cassandra.config.CassandraRelevantProperties.COM_SUN_MANAGEMENT_JMXREMOTE_SSL_ENABLED_PROTOCOLS; -import static org.apache.cassandra.config.CassandraRelevantProperties.COM_SUN_MANAGEMENT_JMXREMOTE_SSL_NEED_CLIENT_AUTH; -import static org.apache.cassandra.config.CassandraRelevantProperties.JAVAX_RMI_SSL_CLIENT_ENABLED_CIPHER_SUITES; -import static org.apache.cassandra.config.CassandraRelevantProperties.JAVAX_RMI_SSL_CLIENT_ENABLED_PROTOCOLS; import static org.apache.cassandra.config.CassandraRelevantProperties.JAVA_RMI_SERVER_HOSTNAME; public class JMXServerUtils @@ -92,6 +87,7 @@ public class JMXServerUtils } // Configure the RMI client & server socket factories, including SSL config. + // CASSANDRA-18508: Make JMX SSL to be configured in cassandra.yaml env.putAll(configureJmxSocketFactories(serverAddress, local)); // configure the RMI registry @@ -226,46 +222,33 @@ public class JMXServerUtils } } - private static Map configureJmxSocketFactories(InetAddress serverAddress, boolean localOnly) + /** + * Configures the client and server socket factories for the JMX connection. It uses {@link DefaultJmxSocketFactory} + * for configuring this. + * + * @param serverAddress the JMX server is bound to + * @param localOnly {@code true} if the JMX server only allows local connections; {@code false} if the JMX server + * allows the remote connections. + * @return Map<String, Object@gt; containing {@code jmx.remote.rmi.client.socket.factory}, {@code jmx.remote.rmi.server.socket.factory} + * and {@code com.sun.jndi.rmi.factory.socket} properties for the client and server socket factories. + * @throws SSLException if it fails to configure the socket factories with the given input + * + * @see DefaultJmxSocketFactory + */ + @VisibleForTesting + public static Map configureJmxSocketFactories(InetAddress serverAddress, boolean localOnly) throws SSLException { - Map env = new HashMap<>(); - if (COM_SUN_MANAGEMENT_JMXREMOTE_SSL.getBoolean()) - { - boolean requireClientAuth = COM_SUN_MANAGEMENT_JMXREMOTE_SSL_NEED_CLIENT_AUTH.getBoolean(); - String[] protocols = null; - String protocolList = COM_SUN_MANAGEMENT_JMXREMOTE_SSL_ENABLED_PROTOCOLS.getString(); - if (protocolList != null) - { - JAVAX_RMI_SSL_CLIENT_ENABLED_PROTOCOLS.setString(protocolList); - protocols = StringUtils.split(protocolList, ','); - } - - String[] ciphers = null; - String cipherList = COM_SUN_MANAGEMENT_JMXREMOTE_SSL_ENABLED_CIPHER_SUITES.getString(); - if (cipherList != null) - { - JAVAX_RMI_SSL_CLIENT_ENABLED_CIPHER_SUITES.setString(cipherList); - ciphers = StringUtils.split(cipherList, ','); - } - - SslRMIClientSocketFactory clientFactory = new SslRMIClientSocketFactory(); - SslRMIServerSocketFactory serverFactory = new SslRMIServerSocketFactory(ciphers, protocols, requireClientAuth); - env.put(RMIConnectorServer.RMI_SERVER_SOCKET_FACTORY_ATTRIBUTE, serverFactory); - env.put(RMIConnectorServer.RMI_CLIENT_SOCKET_FACTORY_ATTRIBUTE, clientFactory); - env.put("com.sun.jndi.rmi.factory.socket", clientFactory); - logJmxSslConfig(serverFactory); - } - else if (localOnly) - { - env.put(RMIConnectorServer.RMI_SERVER_SOCKET_FACTORY_ATTRIBUTE, - new RMIServerSocketFactoryImpl(serverAddress)); - } - - return env; + return new DefaultJmxSocketFactory().configure(serverAddress, localOnly, DatabaseDescriptor.getJmxEncryptionOptions()); } @VisibleForTesting public static void logJmxServiceUrl(InetAddress serverAddress, int port) + { + logger.info("Configured JMX server at: {}", getJmxServiceUrl(serverAddress, port)); + } + + @VisibleForTesting + private static String getJmxServiceUrl(InetAddress serverAddress, int port) { String urlTemplate = "service:jmx:rmi://%1$s/jndi/rmi://%1$s:%2$d/jmxrmi"; String hostName; @@ -280,23 +263,9 @@ public class JMXServerUtils ? '[' + serverAddress.getHostAddress() + ']' : serverAddress.getHostAddress(); } - String url = String.format(urlTemplate, hostName, port); - logger.info("Configured JMX server at: {}", url); + return String.format(urlTemplate, hostName, port); } - - private static void logJmxSslConfig(SslRMIServerSocketFactory serverFactory) - { - if (logger.isDebugEnabled()) - logger.debug("JMX SSL configuration. { protocols: [{}], cipher_suites: [{}], require_client_auth: {} }", - serverFactory.getEnabledProtocols() == null - ? "'JVM defaults'" - : Arrays.stream(serverFactory.getEnabledProtocols()).collect(Collectors.joining("','", "'", "'")), - serverFactory.getEnabledCipherSuites() == null - ? "'JVM defaults'" - : Arrays.stream(serverFactory.getEnabledCipherSuites()).collect(Collectors.joining("','", "'", "'")), - serverFactory.getNeedClientAuth()); - } - + private static class JMXPluggableAuthenticatorWrapper implements JMXAuthenticator { private static final MethodHandle ctorHandle; diff --git a/src/java/org/apache/cassandra/utils/RMIClientSocketFactoryImpl.java b/src/java/org/apache/cassandra/utils/RMIClientSocketFactoryImpl.java index 81e728eeb9..9be9e69f04 100644 --- a/src/java/org/apache/cassandra/utils/RMIClientSocketFactoryImpl.java +++ b/src/java/org/apache/cassandra/utils/RMIClientSocketFactoryImpl.java @@ -22,7 +22,6 @@ import java.io.IOException; import java.io.Serializable; import java.net.InetAddress; import java.net.Socket; -import java.rmi.server.RMIClientSocketFactory; import java.util.ArrayList; import java.util.List; import java.util.Objects; @@ -32,8 +31,9 @@ import java.util.Objects; * which can otherwise be influenced by the system property "java.rmi.server.hostname" in strange and * unpredictable ways. */ -public class RMIClientSocketFactoryImpl implements RMIClientSocketFactory, Serializable +public class RMIClientSocketFactoryImpl implements Serializable, RMICloseableClientSocketFactory { + private static final long serialVersionUID = 955153017775496366L; List sockets = new ArrayList<>(); private final InetAddress localAddress; @@ -50,6 +50,7 @@ public class RMIClientSocketFactoryImpl implements RMIClientSocketFactory, Seria return socket; } + @Override public void close() throws IOException { for (Socket socket: sockets) diff --git a/src/java/org/apache/cassandra/utils/RMICloseableClientSocketFactory.java b/src/java/org/apache/cassandra/utils/RMICloseableClientSocketFactory.java new file mode 100644 index 0000000000..1e2225772a --- /dev/null +++ b/src/java/org/apache/cassandra/utils/RMICloseableClientSocketFactory.java @@ -0,0 +1,29 @@ +/* + * 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.utils; + +import java.rmi.server.RMIClientSocketFactory; + +/** + * This represents closeable RMI Client Socket factory. It extends {@link AutoCloseable} and can be used with + * {@code try-with-resources}. + */ +public interface RMICloseableClientSocketFactory extends RMIClientSocketFactory, AutoCloseable +{ +} diff --git a/src/java/org/apache/cassandra/utils/RMICloseableServerSocketFactory.java b/src/java/org/apache/cassandra/utils/RMICloseableServerSocketFactory.java new file mode 100644 index 0000000000..b9d70f083a --- /dev/null +++ b/src/java/org/apache/cassandra/utils/RMICloseableServerSocketFactory.java @@ -0,0 +1,29 @@ +/* + * 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.utils; + +import java.rmi.server.RMIServerSocketFactory; + +/** + * This represents closeable RMI Server Socket factory. It extends {@link AutoCloseable} and can be used with + * {@code try-with-resources}. + */ +public interface RMICloseableServerSocketFactory extends RMIServerSocketFactory, AutoCloseable +{ +} diff --git a/src/java/org/apache/cassandra/utils/jmx/AbstractJmxSocketFactory.java b/src/java/org/apache/cassandra/utils/jmx/AbstractJmxSocketFactory.java new file mode 100644 index 0000000000..6ee390ee93 --- /dev/null +++ b/src/java/org/apache/cassandra/utils/jmx/AbstractJmxSocketFactory.java @@ -0,0 +1,208 @@ +/* + * 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.utils.jmx; + +import java.net.InetAddress; +import java.util.HashMap; +import java.util.Map; +import javax.net.ssl.SSLContext; +import javax.net.ssl.SSLException; + +import org.apache.commons.lang3.StringUtils; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import org.apache.cassandra.config.EncryptionOptions; +import org.apache.cassandra.exceptions.ConfigurationException; + +import static org.apache.cassandra.config.CassandraRelevantProperties.COM_SUN_MANAGEMENT_JMXREMOTE_SSL; +import static org.apache.cassandra.config.CassandraRelevantProperties.COM_SUN_MANAGEMENT_JMXREMOTE_SSL_ENABLED_CIPHER_SUITES; +import static org.apache.cassandra.config.CassandraRelevantProperties.COM_SUN_MANAGEMENT_JMXREMOTE_SSL_ENABLED_PROTOCOLS; +import static org.apache.cassandra.config.CassandraRelevantProperties.COM_SUN_MANAGEMENT_JMXREMOTE_SSL_NEED_CLIENT_AUTH; +import static org.apache.cassandra.config.CassandraRelevantProperties.JAVAX_RMI_SSL_CLIENT_ENABLED_CIPHER_SUITES; +import static org.apache.cassandra.config.CassandraRelevantProperties.JAVAX_RMI_SSL_CLIENT_ENABLED_PROTOCOLS; + +/** + * Abstracts out the most common workflow in setting up the SSL client and server socket factorires for JMX. + * First, it checks the system properties (see Java Documentation to read the SSL configuration. + * Next, it checks the provided {@code jmxEncryptionOptions} to read the SSL configuration. + * If none of them is enabled, it checks the provided {@code localOnly} flag to configure the JMX server socket + * factory for the local JMX connection. + */ +abstract public class AbstractJmxSocketFactory +{ + private static final Logger logger = LoggerFactory.getLogger(AbstractJmxSocketFactory.class); + + /** + * Configures the client and server socket factories for the JMX connection. + * Specifically it configures below properties as applicable, + *
+     *     jmx.remote.rmi.client.socket.factory
+     *     jmx.remote.rmi.server.socket.factory
+     *     com.sun.jndi.rmi.factory.socket
+     * 
+ *

+ * In case of remote connection enabled, this also sets the following system properties, + *

+     *     com.sun.management.jmxremote.ssl=true
+     *     javax.rmi.ssl.client.enabledCipherSuites=<applicable cipher suites provided in the configuration>
+     *     javax.rmi.ssl.client.enabledProtocols=<applicable protocols provided in the configuration>
+     * 
+ * + * @param serverAddress the JMX server is bound to + * @param localOnly {@code true} if the JMX server only allows local connections; {@code false} if the JMX server + * allows the remote connections. + * @param jmxEncryptionOptions {@link EncryptionOptions} used for the SSL configuration in case of the remote + * connections. Could be {@code null} if system properties are + * used instead as per Java Documentation + * @return Map<String, Object@gt; containing {@code jmx.remote.rmi.client.socket.factory}, {@code jmx.remote.rmi.server.socket.factory} + * and {@code com.sun.jndi.rmi.factory.socket} properties for the client and server socket factories. + * @throws SSLException if it fails to configure the socket factories with the given input + */ + public Map configure(InetAddress serverAddress, boolean localOnly, + EncryptionOptions jmxEncryptionOptions) throws SSLException + { + Map env = new HashMap<>(); + boolean jmxRemoteSslSystemConfigProvided = COM_SUN_MANAGEMENT_JMXREMOTE_SSL.getBoolean(); + // We check for the enabled jmx_encryption_options here because in case of no configuration provided in cassandra.yaml + // it will default to empty/non-null encryption options. Hence, we consider it set only if 'enabled' flag is set to true + boolean jmxEncryptionOptionsProvided = jmxEncryptionOptions != null + && jmxEncryptionOptions.getEnabled() != null + && jmxEncryptionOptions.getEnabled(); + + if (jmxRemoteSslSystemConfigProvided && jmxEncryptionOptionsProvided) + { + throw new ConfigurationException("Please specify JMX SSL configuration in either cassandra-env.sh or " + + "cassandra.yaml, not in both locations"); + } + + boolean requireClientAuth = false; + String[] ciphers = null; + String[] protocols = null; + SSLContext sslContext = null; + + if (jmxRemoteSslSystemConfigProvided) + { + logger.info("Enabling JMX SSL using environment file properties"); + logger.warn("Consider using the jmx_encryption_options section of cassandra.yaml instead to prevent " + + "sensitive information being exposed"); + requireClientAuth = COM_SUN_MANAGEMENT_JMXREMOTE_SSL_NEED_CLIENT_AUTH.getBoolean(); + String protocolList = COM_SUN_MANAGEMENT_JMXREMOTE_SSL_ENABLED_PROTOCOLS.getString(); + if (protocolList != null) + { + JAVAX_RMI_SSL_CLIENT_ENABLED_PROTOCOLS.setString(protocolList); + protocols = StringUtils.split(protocolList, ','); + } + + String cipherList = COM_SUN_MANAGEMENT_JMXREMOTE_SSL_ENABLED_CIPHER_SUITES.getString(); + if (cipherList != null) + { + JAVAX_RMI_SSL_CLIENT_ENABLED_CIPHER_SUITES.setString(cipherList); + ciphers = StringUtils.split(cipherList, ','); + } + configureSslClientSocketFactory(env, serverAddress); + configureSslServerSocketFactory(env, serverAddress, ciphers, protocols, requireClientAuth); + } + else if (jmxEncryptionOptionsProvided) + { + logger.info("Enabling JMX SSL using jmx_encryption_options from cassandra.yaml"); + // Here we can continue to use the SslRMIClientSocketFactory for client sockets. + // However, we should still set System properties for cipher_suites and enabled_protocols + // to have the same behavior as cassandra-env.sh based JMX SSL settings + setJmxSystemProperties(jmxEncryptionOptions); + + requireClientAuth = jmxEncryptionOptions.getClientAuth() == EncryptionOptions.ClientAuth.REQUIRED; + ciphers = jmxEncryptionOptions.cipherSuitesArray(); + protocols = jmxEncryptionOptions.acceptedProtocolsArray(); + sslContext = jmxEncryptionOptions.sslContextFactoryInstance + .createJSSESslContext(jmxEncryptionOptions.getClientAuth()); + configureSslClientSocketFactory(env, serverAddress); + configureSslServerSocketFactory(env, serverAddress, ciphers, protocols, requireClientAuth, sslContext); + } + else if (localOnly) + { + configureLocalSocketFactories(env, serverAddress); + } + + return env; + } + + /** + * Configures the non-SSL socket factories for the local JMX. + * + * @param env output param containing the configured socket factories + * @param serverAddress the JMX server is bound to + */ + abstract public void configureLocalSocketFactories(Map env, InetAddress serverAddress); + + /** + * Configures SSL based client socket factory. + * + * @param env output param containing the configured socket factories + * @param serverAddress the JMX server is bound to + */ + abstract public void configureSslClientSocketFactory(Map env, InetAddress serverAddress); + + /** + * Configures SSL based server socket factory based on system config for key/trust stores. + * + * @param env output param containing the configured socket factories + * @param serverAddress the JMX server is bound to + * @param enabledCipherSuites for the SSL communication + * @param enabledProtocols for the SSL communication + * @param needClientAuth {@code true} if it requires the client-auth; {@code false} otherwise + */ + abstract public void configureSslServerSocketFactory(Map env, InetAddress serverAddress, + String[] enabledCipherSuites, String[] enabledProtocols, + boolean needClientAuth); + + /** + * Configures SSL based server socket factory based on custom SSLContext. + * + * @param env output param containing the configured socket factories + * @param serverAddress the JMX server is bound to + * @param enabledCipherSuites for the SSL communication + * @param enabledProtocols for the SSL communication + * @param needClientAuth {@code true} if it requires the client-auth; {@code false} otherwise + * @param sslContext for the SSL communication + */ + abstract public void configureSslServerSocketFactory(Map env, InetAddress serverAddress, + String[] enabledCipherSuites, String[] enabledProtocols, + boolean needClientAuth, SSLContext sslContext); + + /** + * Sets the following JMX system properties. + *
+     *     com.sun.management.jmxremote.ssl=true
+     *     javax.rmi.ssl.client.enabledCipherSuites=<applicable cipher suites provided in the configuration>
+     *     javax.rmi.ssl.client.enabledProtocols=<applicable protocols provided in the configuration>
+     * 
+ * + * @param jmxEncryptionOptions for the SSL communication + */ + private void setJmxSystemProperties(EncryptionOptions jmxEncryptionOptions) + { + COM_SUN_MANAGEMENT_JMXREMOTE_SSL.setBoolean(true); + if (jmxEncryptionOptions.getAcceptedProtocols() != null) + JAVAX_RMI_SSL_CLIENT_ENABLED_PROTOCOLS.setString(StringUtils.join(jmxEncryptionOptions.getAcceptedProtocols(), ",")); + + if (jmxEncryptionOptions.cipherSuitesArray() != null) + JAVAX_RMI_SSL_CLIENT_ENABLED_CIPHER_SUITES.setString(StringUtils.join(jmxEncryptionOptions.cipherSuitesArray(), ",")); + } +} diff --git a/src/java/org/apache/cassandra/utils/jmx/DefaultJmxSocketFactory.java b/src/java/org/apache/cassandra/utils/jmx/DefaultJmxSocketFactory.java new file mode 100644 index 0000000000..35cc92b731 --- /dev/null +++ b/src/java/org/apache/cassandra/utils/jmx/DefaultJmxSocketFactory.java @@ -0,0 +1,82 @@ +/* + * 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.utils.jmx; + +import java.net.InetAddress; +import java.util.Arrays; +import java.util.Map; +import java.util.stream.Collectors; +import javax.management.remote.rmi.RMIConnectorServer; +import javax.net.ssl.SSLContext; +import javax.rmi.ssl.SslRMIClientSocketFactory; +import javax.rmi.ssl.SslRMIServerSocketFactory; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import org.apache.cassandra.utils.RMIServerSocketFactoryImpl; + +/** + * Default implementation of the JMX Socket factory. + */ +public final class DefaultJmxSocketFactory extends AbstractJmxSocketFactory +{ + private static final Logger logger = LoggerFactory.getLogger(DefaultJmxSocketFactory.class); + + @Override + public void configureLocalSocketFactories(Map env, InetAddress serverAddress) + { + env.put(RMIConnectorServer.RMI_SERVER_SOCKET_FACTORY_ATTRIBUTE, new RMIServerSocketFactoryImpl(serverAddress)); + } + + @Override + public void configureSslClientSocketFactory(Map env, InetAddress serverAddress) + { + SslRMIClientSocketFactory clientFactory = new SslRMIClientSocketFactory(); + env.put(RMIConnectorServer.RMI_CLIENT_SOCKET_FACTORY_ATTRIBUTE, clientFactory); + env.put("com.sun.jndi.rmi.factory.socket", clientFactory); + } + + @Override + public void configureSslServerSocketFactory(Map env, InetAddress serverAddress, String[] enabledCipherSuites, + String[] enabledProtocols, boolean needClientAuth) + { + SslRMIServerSocketFactory serverFactory = new SslRMIServerSocketFactory(enabledCipherSuites, enabledProtocols, needClientAuth); + env.put(RMIConnectorServer.RMI_SERVER_SOCKET_FACTORY_ATTRIBUTE, serverFactory); + logJmxSslConfig(serverFactory); + } + + @Override + public void configureSslServerSocketFactory(Map env, InetAddress serverAddress, String[] enabledCipherSuites, + String[] enabledProtocols, boolean needClientAuth, SSLContext sslContext) + { + SslRMIServerSocketFactory serverFactory = new SslRMIServerSocketFactory(sslContext, enabledCipherSuites, enabledProtocols, needClientAuth); + env.put(RMIConnectorServer.RMI_SERVER_SOCKET_FACTORY_ATTRIBUTE, serverFactory); + logJmxSslConfig(serverFactory); + } + + private void logJmxSslConfig(SslRMIServerSocketFactory serverFactory) + { + if (logger.isDebugEnabled()) + logger.debug("JMX SSL configuration. { protocols: [{}], cipher_suites: [{}], require_client_auth: {} }", + serverFactory.getEnabledProtocols() == null ? "'JVM defaults'" : Arrays.stream(serverFactory.getEnabledProtocols()).collect(Collectors.joining("','", "'", "'")), + serverFactory.getEnabledCipherSuites() == null ? "'JVM defaults'" : Arrays.stream(serverFactory.getEnabledCipherSuites()).collect(Collectors.joining("','", "'", "'")), + serverFactory.getNeedClientAuth()); + } +} diff --git a/test/conf/cassandra-jmx-disabled-sslconfig.yaml b/test/conf/cassandra-jmx-disabled-sslconfig.yaml new file mode 100644 index 0000000000..2b63389926 --- /dev/null +++ b/test/conf/cassandra-jmx-disabled-sslconfig.yaml @@ -0,0 +1,147 @@ +# +# 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. +# + +# +# Testing for pluggable ssl_context_factory option for client and server encryption options with a valid and a missing +# implementation classes. +# +cluster_name: Test Cluster +# memtable_allocation_type: heap_buffers +memtable_allocation_type: offheap_objects +commitlog_sync: batch +commitlog_segment_size: 5MiB +commitlog_directory: build/test/cassandra/commitlog +# commitlog_compression: +# - class_name: LZ4Compressor +cdc_raw_directory: build/test/cassandra/cdc_raw +cdc_enabled: false +hints_directory: build/test/cassandra/hints +partitioner: org.apache.cassandra.dht.ByteOrderedPartitioner +listen_address: 127.0.0.1 +storage_port: 7012 +ssl_storage_port: 17012 +start_native_transport: true +native_transport_port: 9042 +column_index_size: 4KiB +saved_caches_directory: build/test/cassandra/saved_caches +data_file_directories: + - build/test/cassandra/data +disk_access_mode: mmap_index_only +seed_provider: + - class_name: org.apache.cassandra.locator.SimpleSeedProvider + parameters: + - seeds: "127.0.0.1:7012" +endpoint_snitch: org.apache.cassandra.locator.SimpleSnitch +dynamic_snitch: true +jmx_encryption_options: + enabled: false + cipher_suites: [TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384,TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256] + accepted_protocols: [TLSv1.2,TLSv1.3,TLSv1.1] + ssl_context_factory: + class_name: org.apache.cassandra.security.PEMBasedSslContextFactory + parameters: + private_key: | + -----BEGIN ENCRYPTED PRIVATE KEY----- + MIIE6jAcBgoqhkiG9w0BDAEDMA4ECOWqSzq5PBIdAgIFxQSCBMjXsCK30J0aT3J/ + g5kcbmevTOY1pIhJGbf5QYYrMUPiuDK2ydxIbiPzoTE4/S+OkCeHhlqwn/YydpBl + xgjZZ1Z5rLJHO27d2biuESqanDiBVXYuVmHmaifRnFy0uUTFkStB5mjVZEiJgO29 + L83hL60uWru71EVuVriC2WCfmZ/EXp6wyYszOqCFQ8Quk/rDO6XuaBl467MJbx5V + sucGT6E9XKNd9hB14/Izb2jtVM5kqKxoiHpz1na6yhEYJiE5D1uOonznWjBnjwB/ + f0x+acpDfVDoJKTlRdz+DEcbOF7mb9lBVVjP6P/AAsmQzz6JKwHjvCrjYfQmyyN8 + RI4KRQnWgm4L3dtByLqY8HFU4ogisCMCgI+hZQ+OKMz/hoRO540YGiPcTRY3EOUR + 0bd5JxU6tCJDMTqKP9aSL2KmLoiLowdMkSPz7TCzLsZ2bGJemuCfpAs4XT1vXCHs + evrUbOnh8et1IA8mZ9auThfqsZtNagJLEXA6hWIKp1FfVL3Q49wvMKZt4eTn/zwU + tLL0m5yPo6/HAaOA3hbm/oghZS0dseshXl7PZrmZQtvYnIvjyoxEL7ducYDQCDP6 + wZ7Nzyh1QZAauSS15hl3vLFRZCA9hWAVgwQAviTvhB342O0i9qI7TQkcHk+qcTPN + K+iGNbFZ8ma1izXNKSJ2PgI/QqFNIeJWvZrb9PhJRmaZVsTJ9fERm1ewpebZqkVv + zMqMhlKgx9ggAaSKgnGZkwXwB6GrSbbzUrwRCKm3FieD1QE4VVYevaadVUU75GG5 + mrFKorJEH7kFZlic8OTjDksYnHbcgU36XZrGEXa2+ldVeGKL3CsXWciaQRcJg8yo + WQDjZpcutGI0eMJWCqUkv8pYZC2/wZU4htCve5nVJUU4t9uuo9ex7lnwlLWPvheQ + jUBMgzSRsZ+zwaIusvufAAxiKK/cJm4ubZSZPIjBbfd4U7VPxtirP4Accydu7EK6 + eG/MZwtAMFNJxfxUR+/aYzJU/q1ePw7fWVHrpt58t/22CX2SJBEiUGmSmuyER4Ny + DPw6d6mhvPUS1jRhIZ9A81ht8MOX7VL5uVp307rt7o5vRpV1mo0iPiRHzGscMpJn + AP36klEAUNTf0uLTKZa7KHiwhn5iPmsCrENHkOKJjxhRrqHjD2wy3YHs3ow2voyY + Ua4Cids+c1hvRkNEDGNHm4+rKGFOGOsG/ZU7uj/6gflO4JXxNGiyTLflqMdWBvow + Zd7hk1zCaGAAn8nZ0hPweGxQ4Q30I9IBZrimGxB0vjiUqNio9+qMf33dCHFJEuut + ZGJMaUGVaPhXQcTy4uD5hzsPZV5xcsU4H3vBYyBcZgrusJ6OOgkuZQaU7p8rWQWr + bUEVbXuZdwEmxsCe7H/vEVv5+aA4sF4kWnMMFL7/LIYaiEzkTqdJlRv/KyJJgcAH + hg2BvR3XTAq8wiX0C98CdmTbsx2eyQdj5tCU606rEohFLKUxWkJYAKxCiUbxGGpI + RheVmxkef9ErxJiq7hsAsGrSJvMtJuDKIasnD14SOEwD/7jRAq6WdL9VLpxtzlOw + pWnIl8kUCO3WoaG9Jf+ZTIv2hnxJhaSzYrdXzGPNnaWKhBlwnXJRvQEdrIxZOimP + FujZhqbKUDbYAcqTkoQ= + -----END ENCRYPTED PRIVATE KEY----- + -----BEGIN CERTIFICATE----- + MIIDkTCCAnmgAwIBAgIETxH5JDANBgkqhkiG9w0BAQsFADB5MRAwDgYDVQQGEwdV + bmtub3duMRAwDgYDVQQIEwdVbmtub3duMRAwDgYDVQQHEwdVbmtub3duMRAwDgYD + VQQKEwdVbmtub3duMRQwEgYDVQQLDAtzc2xfdGVzdGluZzEZMBcGA1UEAxMQQXBh + Y2hlIENhc3NhbmRyYTAeFw0xNjAzMTgyMTI4MDJaFw0xNjA2MTYyMTI4MDJaMHkx + EDAOBgNVBAYTB1Vua25vd24xEDAOBgNVBAgTB1Vua25vd24xEDAOBgNVBAcTB1Vu + a25vd24xEDAOBgNVBAoTB1Vua25vd24xFDASBgNVBAsMC3NzbF90ZXN0aW5nMRkw + FwYDVQQDExBBcGFjaGUgQ2Fzc2FuZHJhMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8A + MIIBCgKCAQEAjkmVX/HS49cS8Hn6o26IGwMIcEV3d7ZhH0GNcx8rnSRd10dU9F6d + ugSjbwGFMcWUQzYNejN6az0Wb8JIQyXRPTWjfgaWTyVGr0bGTnxg6vwhzfI/9jzy + q59xv29OuSY1dxmY31f0pZ9OOw3mabWksjoO2TexfKoxqsRHJ8PrM1f8E84Z4xo2 + TJXGzpuIxRkAJ+sVDqKEAhrKAfRYMSgdJ7zRt8VXv9ngjX20uA2m092NcH0Kmeto + TmuWUtK8E/qcN7ULN8xRWNUn4hu6mG6mayk4XliGRqI1VZupqh+MgNqHznuTd0bA + YrQsFPw9HaZ2hvVnJffJ5l7njAekZNOL+wIDAQABoyEwHzAdBgNVHQ4EFgQUcdiD + N6aylI91kAd34Hl2AzWY51QwDQYJKoZIhvcNAQELBQADggEBAG9q29ilUgCWQP5v + iHkZHj10gXGEoMkdfrPBf8grC7dpUcaw1Qfku/DJ7kPvMALeEsmFDk/t78roeNbh + IYBLJlzI1HZN6VPtpWQGsqxltAy5XN9Xw9mQM/tu70ShgsodGmE1UoW6eE5+/GMv + 6Fg+zLuICPvs2cFNmWUvukN5LW146tJSYCv0Q/rCPB3m9dNQ9pBxrzPUHXw4glwG + qGnGddXmOC+tSW5lDLLG1BRbKv4zxv3UlrtIjqlJtZb/sQMT6WtG2ihAz7SKOBHa + HOWUwuPTetWIuJCKP7P4mWWtmSmjLy+BFX5seNEngn3RzJ2L8uuTJQ/88OsqgGru + n3MVF9w= + -----END CERTIFICATE----- + private_key_password: "cassandra" + trusted_certificates: | + -----BEGIN CERTIFICATE----- + MIIDkTCCAnmgAwIBAgIETxH5JDANBgkqhkiG9w0BAQsFADB5MRAwDgYDVQQGEwdV + bmtub3duMRAwDgYDVQQIEwdVbmtub3duMRAwDgYDVQQHEwdVbmtub3duMRAwDgYD + VQQKEwdVbmtub3duMRQwEgYDVQQLDAtzc2xfdGVzdGluZzEZMBcGA1UEAxMQQXBh + Y2hlIENhc3NhbmRyYTAeFw0xNjAzMTgyMTI4MDJaFw0xNjA2MTYyMTI4MDJaMHkx + EDAOBgNVBAYTB1Vua25vd24xEDAOBgNVBAgTB1Vua25vd24xEDAOBgNVBAcTB1Vu + a25vd24xEDAOBgNVBAoTB1Vua25vd24xFDASBgNVBAsMC3NzbF90ZXN0aW5nMRkw + FwYDVQQDExBBcGFjaGUgQ2Fzc2FuZHJhMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8A + MIIBCgKCAQEAjkmVX/HS49cS8Hn6o26IGwMIcEV3d7ZhH0GNcx8rnSRd10dU9F6d + ugSjbwGFMcWUQzYNejN6az0Wb8JIQyXRPTWjfgaWTyVGr0bGTnxg6vwhzfI/9jzy + q59xv29OuSY1dxmY31f0pZ9OOw3mabWksjoO2TexfKoxqsRHJ8PrM1f8E84Z4xo2 + TJXGzpuIxRkAJ+sVDqKEAhrKAfRYMSgdJ7zRt8VXv9ngjX20uA2m092NcH0Kmeto + TmuWUtK8E/qcN7ULN8xRWNUn4hu6mG6mayk4XliGRqI1VZupqh+MgNqHznuTd0bA + YrQsFPw9HaZ2hvVnJffJ5l7njAekZNOL+wIDAQABoyEwHzAdBgNVHQ4EFgQUcdiD + N6aylI91kAd34Hl2AzWY51QwDQYJKoZIhvcNAQELBQADggEBAG9q29ilUgCWQP5v + iHkZHj10gXGEoMkdfrPBf8grC7dpUcaw1Qfku/DJ7kPvMALeEsmFDk/t78roeNbh + IYBLJlzI1HZN6VPtpWQGsqxltAy5XN9Xw9mQM/tu70ShgsodGmE1UoW6eE5+/GMv + 6Fg+zLuICPvs2cFNmWUvukN5LW146tJSYCv0Q/rCPB3m9dNQ9pBxrzPUHXw4glwG + qGnGddXmOC+tSW5lDLLG1BRbKv4zxv3UlrtIjqlJtZb/sQMT6WtG2ihAz7SKOBHa + HOWUwuPTetWIuJCKP7P4mWWtmSmjLy+BFX5seNEngn3RzJ2L8uuTJQ/88OsqgGru + n3MVF9w= + -----END CERTIFICATE----- +incremental_backups: true +concurrent_compactors: 4 +compaction_throughput: 0MiB/s +row_cache_class_name: org.apache.cassandra.cache.OHCProvider +row_cache_size: 16MiB +user_defined_functions_enabled: true +scripted_user_defined_functions_enabled: false +prepared_statements_cache_size: 1MiB +corrupted_tombstone_strategy: exception +stream_entire_sstables: true +stream_throughput_outbound: 24MiB/s +sasi_indexes_enabled: true +materialized_views_enabled: true +file_cache_enabled: true diff --git a/test/conf/cassandra-jmx-pem-sslconfig.yaml b/test/conf/cassandra-jmx-pem-sslconfig.yaml new file mode 100644 index 0000000000..45f58ffda2 --- /dev/null +++ b/test/conf/cassandra-jmx-pem-sslconfig.yaml @@ -0,0 +1,147 @@ +# +# 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. +# + +# +# Testing for pluggable ssl_context_factory option for client and server encryption options with a valid and a missing +# implementation classes. +# +cluster_name: Test Cluster +# memtable_allocation_type: heap_buffers +memtable_allocation_type: offheap_objects +commitlog_sync: batch +commitlog_segment_size: 5MiB +commitlog_directory: build/test/cassandra/commitlog +# commitlog_compression: +# - class_name: LZ4Compressor +cdc_raw_directory: build/test/cassandra/cdc_raw +cdc_enabled: false +hints_directory: build/test/cassandra/hints +partitioner: org.apache.cassandra.dht.ByteOrderedPartitioner +listen_address: 127.0.0.1 +storage_port: 7012 +ssl_storage_port: 17012 +start_native_transport: true +native_transport_port: 9042 +column_index_size: 4KiB +saved_caches_directory: build/test/cassandra/saved_caches +data_file_directories: + - build/test/cassandra/data +disk_access_mode: mmap_index_only +seed_provider: + - class_name: org.apache.cassandra.locator.SimpleSeedProvider + parameters: + - seeds: "127.0.0.1:7012" +endpoint_snitch: org.apache.cassandra.locator.SimpleSnitch +dynamic_snitch: true +jmx_encryption_options: + enabled: true + cipher_suites: [TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384,TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256] + accepted_protocols: [TLSv1.2,TLSv1.3,TLSv1.1] + ssl_context_factory: + class_name: org.apache.cassandra.security.PEMBasedSslContextFactory + parameters: + private_key: | + -----BEGIN ENCRYPTED PRIVATE KEY----- + MIIE6jAcBgoqhkiG9w0BDAEDMA4ECOWqSzq5PBIdAgIFxQSCBMjXsCK30J0aT3J/ + g5kcbmevTOY1pIhJGbf5QYYrMUPiuDK2ydxIbiPzoTE4/S+OkCeHhlqwn/YydpBl + xgjZZ1Z5rLJHO27d2biuESqanDiBVXYuVmHmaifRnFy0uUTFkStB5mjVZEiJgO29 + L83hL60uWru71EVuVriC2WCfmZ/EXp6wyYszOqCFQ8Quk/rDO6XuaBl467MJbx5V + sucGT6E9XKNd9hB14/Izb2jtVM5kqKxoiHpz1na6yhEYJiE5D1uOonznWjBnjwB/ + f0x+acpDfVDoJKTlRdz+DEcbOF7mb9lBVVjP6P/AAsmQzz6JKwHjvCrjYfQmyyN8 + RI4KRQnWgm4L3dtByLqY8HFU4ogisCMCgI+hZQ+OKMz/hoRO540YGiPcTRY3EOUR + 0bd5JxU6tCJDMTqKP9aSL2KmLoiLowdMkSPz7TCzLsZ2bGJemuCfpAs4XT1vXCHs + evrUbOnh8et1IA8mZ9auThfqsZtNagJLEXA6hWIKp1FfVL3Q49wvMKZt4eTn/zwU + tLL0m5yPo6/HAaOA3hbm/oghZS0dseshXl7PZrmZQtvYnIvjyoxEL7ducYDQCDP6 + wZ7Nzyh1QZAauSS15hl3vLFRZCA9hWAVgwQAviTvhB342O0i9qI7TQkcHk+qcTPN + K+iGNbFZ8ma1izXNKSJ2PgI/QqFNIeJWvZrb9PhJRmaZVsTJ9fERm1ewpebZqkVv + zMqMhlKgx9ggAaSKgnGZkwXwB6GrSbbzUrwRCKm3FieD1QE4VVYevaadVUU75GG5 + mrFKorJEH7kFZlic8OTjDksYnHbcgU36XZrGEXa2+ldVeGKL3CsXWciaQRcJg8yo + WQDjZpcutGI0eMJWCqUkv8pYZC2/wZU4htCve5nVJUU4t9uuo9ex7lnwlLWPvheQ + jUBMgzSRsZ+zwaIusvufAAxiKK/cJm4ubZSZPIjBbfd4U7VPxtirP4Accydu7EK6 + eG/MZwtAMFNJxfxUR+/aYzJU/q1ePw7fWVHrpt58t/22CX2SJBEiUGmSmuyER4Ny + DPw6d6mhvPUS1jRhIZ9A81ht8MOX7VL5uVp307rt7o5vRpV1mo0iPiRHzGscMpJn + AP36klEAUNTf0uLTKZa7KHiwhn5iPmsCrENHkOKJjxhRrqHjD2wy3YHs3ow2voyY + Ua4Cids+c1hvRkNEDGNHm4+rKGFOGOsG/ZU7uj/6gflO4JXxNGiyTLflqMdWBvow + Zd7hk1zCaGAAn8nZ0hPweGxQ4Q30I9IBZrimGxB0vjiUqNio9+qMf33dCHFJEuut + ZGJMaUGVaPhXQcTy4uD5hzsPZV5xcsU4H3vBYyBcZgrusJ6OOgkuZQaU7p8rWQWr + bUEVbXuZdwEmxsCe7H/vEVv5+aA4sF4kWnMMFL7/LIYaiEzkTqdJlRv/KyJJgcAH + hg2BvR3XTAq8wiX0C98CdmTbsx2eyQdj5tCU606rEohFLKUxWkJYAKxCiUbxGGpI + RheVmxkef9ErxJiq7hsAsGrSJvMtJuDKIasnD14SOEwD/7jRAq6WdL9VLpxtzlOw + pWnIl8kUCO3WoaG9Jf+ZTIv2hnxJhaSzYrdXzGPNnaWKhBlwnXJRvQEdrIxZOimP + FujZhqbKUDbYAcqTkoQ= + -----END ENCRYPTED PRIVATE KEY----- + -----BEGIN CERTIFICATE----- + MIIDkTCCAnmgAwIBAgIETxH5JDANBgkqhkiG9w0BAQsFADB5MRAwDgYDVQQGEwdV + bmtub3duMRAwDgYDVQQIEwdVbmtub3duMRAwDgYDVQQHEwdVbmtub3duMRAwDgYD + VQQKEwdVbmtub3duMRQwEgYDVQQLDAtzc2xfdGVzdGluZzEZMBcGA1UEAxMQQXBh + Y2hlIENhc3NhbmRyYTAeFw0xNjAzMTgyMTI4MDJaFw0xNjA2MTYyMTI4MDJaMHkx + EDAOBgNVBAYTB1Vua25vd24xEDAOBgNVBAgTB1Vua25vd24xEDAOBgNVBAcTB1Vu + a25vd24xEDAOBgNVBAoTB1Vua25vd24xFDASBgNVBAsMC3NzbF90ZXN0aW5nMRkw + FwYDVQQDExBBcGFjaGUgQ2Fzc2FuZHJhMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8A + MIIBCgKCAQEAjkmVX/HS49cS8Hn6o26IGwMIcEV3d7ZhH0GNcx8rnSRd10dU9F6d + ugSjbwGFMcWUQzYNejN6az0Wb8JIQyXRPTWjfgaWTyVGr0bGTnxg6vwhzfI/9jzy + q59xv29OuSY1dxmY31f0pZ9OOw3mabWksjoO2TexfKoxqsRHJ8PrM1f8E84Z4xo2 + TJXGzpuIxRkAJ+sVDqKEAhrKAfRYMSgdJ7zRt8VXv9ngjX20uA2m092NcH0Kmeto + TmuWUtK8E/qcN7ULN8xRWNUn4hu6mG6mayk4XliGRqI1VZupqh+MgNqHznuTd0bA + YrQsFPw9HaZ2hvVnJffJ5l7njAekZNOL+wIDAQABoyEwHzAdBgNVHQ4EFgQUcdiD + N6aylI91kAd34Hl2AzWY51QwDQYJKoZIhvcNAQELBQADggEBAG9q29ilUgCWQP5v + iHkZHj10gXGEoMkdfrPBf8grC7dpUcaw1Qfku/DJ7kPvMALeEsmFDk/t78roeNbh + IYBLJlzI1HZN6VPtpWQGsqxltAy5XN9Xw9mQM/tu70ShgsodGmE1UoW6eE5+/GMv + 6Fg+zLuICPvs2cFNmWUvukN5LW146tJSYCv0Q/rCPB3m9dNQ9pBxrzPUHXw4glwG + qGnGddXmOC+tSW5lDLLG1BRbKv4zxv3UlrtIjqlJtZb/sQMT6WtG2ihAz7SKOBHa + HOWUwuPTetWIuJCKP7P4mWWtmSmjLy+BFX5seNEngn3RzJ2L8uuTJQ/88OsqgGru + n3MVF9w= + -----END CERTIFICATE----- + private_key_password: "cassandra" + trusted_certificates: | + -----BEGIN CERTIFICATE----- + MIIDkTCCAnmgAwIBAgIETxH5JDANBgkqhkiG9w0BAQsFADB5MRAwDgYDVQQGEwdV + bmtub3duMRAwDgYDVQQIEwdVbmtub3duMRAwDgYDVQQHEwdVbmtub3duMRAwDgYD + VQQKEwdVbmtub3duMRQwEgYDVQQLDAtzc2xfdGVzdGluZzEZMBcGA1UEAxMQQXBh + Y2hlIENhc3NhbmRyYTAeFw0xNjAzMTgyMTI4MDJaFw0xNjA2MTYyMTI4MDJaMHkx + EDAOBgNVBAYTB1Vua25vd24xEDAOBgNVBAgTB1Vua25vd24xEDAOBgNVBAcTB1Vu + a25vd24xEDAOBgNVBAoTB1Vua25vd24xFDASBgNVBAsMC3NzbF90ZXN0aW5nMRkw + FwYDVQQDExBBcGFjaGUgQ2Fzc2FuZHJhMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8A + MIIBCgKCAQEAjkmVX/HS49cS8Hn6o26IGwMIcEV3d7ZhH0GNcx8rnSRd10dU9F6d + ugSjbwGFMcWUQzYNejN6az0Wb8JIQyXRPTWjfgaWTyVGr0bGTnxg6vwhzfI/9jzy + q59xv29OuSY1dxmY31f0pZ9OOw3mabWksjoO2TexfKoxqsRHJ8PrM1f8E84Z4xo2 + TJXGzpuIxRkAJ+sVDqKEAhrKAfRYMSgdJ7zRt8VXv9ngjX20uA2m092NcH0Kmeto + TmuWUtK8E/qcN7ULN8xRWNUn4hu6mG6mayk4XliGRqI1VZupqh+MgNqHznuTd0bA + YrQsFPw9HaZ2hvVnJffJ5l7njAekZNOL+wIDAQABoyEwHzAdBgNVHQ4EFgQUcdiD + N6aylI91kAd34Hl2AzWY51QwDQYJKoZIhvcNAQELBQADggEBAG9q29ilUgCWQP5v + iHkZHj10gXGEoMkdfrPBf8grC7dpUcaw1Qfku/DJ7kPvMALeEsmFDk/t78roeNbh + IYBLJlzI1HZN6VPtpWQGsqxltAy5XN9Xw9mQM/tu70ShgsodGmE1UoW6eE5+/GMv + 6Fg+zLuICPvs2cFNmWUvukN5LW146tJSYCv0Q/rCPB3m9dNQ9pBxrzPUHXw4glwG + qGnGddXmOC+tSW5lDLLG1BRbKv4zxv3UlrtIjqlJtZb/sQMT6WtG2ihAz7SKOBHa + HOWUwuPTetWIuJCKP7P4mWWtmSmjLy+BFX5seNEngn3RzJ2L8uuTJQ/88OsqgGru + n3MVF9w= + -----END CERTIFICATE----- +incremental_backups: true +concurrent_compactors: 4 +compaction_throughput: 0MiB/s +row_cache_class_name: org.apache.cassandra.cache.OHCProvider +row_cache_size: 16MiB +user_defined_functions_enabled: true +scripted_user_defined_functions_enabled: false +prepared_statements_cache_size: 1MiB +corrupted_tombstone_strategy: exception +stream_entire_sstables: true +stream_throughput_outbound: 24MiB/s +sasi_indexes_enabled: true +materialized_views_enabled: true +file_cache_enabled: true diff --git a/test/conf/cassandra-jmx-sslconfig.yaml b/test/conf/cassandra-jmx-sslconfig.yaml new file mode 100644 index 0000000000..317d5236b6 --- /dev/null +++ b/test/conf/cassandra-jmx-sslconfig.yaml @@ -0,0 +1,72 @@ +# +# 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. +# + +# +# Testing for pluggable ssl_context_factory option for client and server encryption options with a valid and a missing +# implementation classes. +# +cluster_name: Test Cluster +# memtable_allocation_type: heap_buffers +memtable_allocation_type: offheap_objects +commitlog_sync: batch +commitlog_segment_size: 5MiB +commitlog_directory: build/test/cassandra/commitlog +# commitlog_compression: +# - class_name: LZ4Compressor +cdc_raw_directory: build/test/cassandra/cdc_raw +cdc_enabled: false +hints_directory: build/test/cassandra/hints +partitioner: org.apache.cassandra.dht.ByteOrderedPartitioner +listen_address: 127.0.0.1 +storage_port: 7012 +ssl_storage_port: 17012 +start_native_transport: true +native_transport_port: 9042 +column_index_size: 4KiB +saved_caches_directory: build/test/cassandra/saved_caches +data_file_directories: + - build/test/cassandra/data +disk_access_mode: mmap_index_only +seed_provider: + - class_name: org.apache.cassandra.locator.SimpleSeedProvider + parameters: + - seeds: "127.0.0.1:7012" +endpoint_snitch: org.apache.cassandra.locator.SimpleSnitch +dynamic_snitch: true +jmx_encryption_options: + enabled: true + cipher_suites: [TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384,TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256] + accepted_protocols: [TLSv1.2,TLSv1.3,TLSv1.1] + keystore: test/conf/cassandra_ssl_test.keystore + keystore_password: cassandra + truststore: test/conf/cassandra_ssl_test.truststore + truststore_password: cassandra +incremental_backups: true +concurrent_compactors: 4 +compaction_throughput: 0MiB/s +row_cache_class_name: org.apache.cassandra.cache.OHCProvider +row_cache_size: 16MiB +user_defined_functions_enabled: true +scripted_user_defined_functions_enabled: false +prepared_statements_cache_size: 1MiB +corrupted_tombstone_strategy: exception +stream_entire_sstables: true +stream_throughput_outbound: 24MiB/s +sasi_indexes_enabled: true +materialized_views_enabled: true +file_cache_enabled: true diff --git a/test/distributed/org/apache/cassandra/distributed/impl/CollectingRMIServerSocketFactoryImpl.java b/test/distributed/org/apache/cassandra/distributed/impl/CollectingRMIServerSocketFactoryImpl.java index 0fc742513f..bba536f67f 100644 --- a/test/distributed/org/apache/cassandra/distributed/impl/CollectingRMIServerSocketFactoryImpl.java +++ b/test/distributed/org/apache/cassandra/distributed/impl/CollectingRMIServerSocketFactoryImpl.java @@ -22,20 +22,21 @@ import java.io.IOException; import java.net.InetAddress; import java.net.ServerSocket; import java.net.SocketException; -import java.rmi.server.RMIServerSocketFactory; import java.util.ArrayList; import java.util.List; import java.util.Objects; import javax.net.ServerSocketFactory; +import org.apache.cassandra.utils.RMICloseableServerSocketFactory; + /** * This class is used to keep track of RMI servers created during a cluster creation so we can * later close the sockets, which would otherwise be left with a thread running waiting for * connections that would never show up as the server was otherwise closed. */ -class CollectingRMIServerSocketFactoryImpl implements RMIServerSocketFactory +class CollectingRMIServerSocketFactoryImpl implements RMICloseableServerSocketFactory { private final InetAddress bindAddress; List sockets = new ArrayList<>(); @@ -62,7 +63,7 @@ class CollectingRMIServerSocketFactoryImpl implements RMIServerSocketFactory return result; } - + @Override public void close() throws IOException { for (ServerSocket socket : sockets) diff --git a/test/distributed/org/apache/cassandra/distributed/impl/CollectingSslRMIServerSocketFactoryImpl.java b/test/distributed/org/apache/cassandra/distributed/impl/CollectingSslRMIServerSocketFactoryImpl.java new file mode 100644 index 0000000000..01d051e4cf --- /dev/null +++ b/test/distributed/org/apache/cassandra/distributed/impl/CollectingSslRMIServerSocketFactoryImpl.java @@ -0,0 +1,160 @@ +/* + * 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.distributed.impl; + +import java.io.IOException; +import java.net.InetAddress; +import java.net.ServerSocket; +import java.net.Socket; +import java.net.SocketException; +import java.util.ArrayList; +import java.util.List; +import java.util.Objects; +import javax.net.ssl.SSLContext; +import javax.net.ssl.SSLSocket; +import javax.net.ssl.SSLSocketFactory; + +import org.apache.cassandra.utils.RMICloseableServerSocketFactory; + + +/** + * This class is used to keep track of SSL based RMI servers created during a cluster creation to + * later close the sockets, which would otherwise be left with a thread running waiting for + * connections that would never show up as the server was otherwise closed. + */ +class CollectingSslRMIServerSocketFactoryImpl implements RMICloseableServerSocketFactory +{ + private final InetAddress bindAddress; + private final String[] enabledCipherSuites; + private final String[] enabledProtocols; + private final boolean needClientAuth; + private final SSLSocketFactory sslSocketFactory; + List sockets = new ArrayList<>(); + + public CollectingSslRMIServerSocketFactoryImpl(InetAddress bindAddress, String[] enabledCipherSuites, + String[] enabledProtocols, boolean needClientAuth, SSLContext sslContext) + { + this.bindAddress = bindAddress; + this.enabledCipherSuites = enabledCipherSuites; + this.enabledProtocols = enabledProtocols; + this.needClientAuth = needClientAuth; + this.sslSocketFactory = sslContext.getSocketFactory(); + } + + public CollectingSslRMIServerSocketFactoryImpl(InetAddress bindAddress, String[] enabledCipherSuites, + String[] enabledProtocols, boolean needClientAuth) + { + this.bindAddress = bindAddress; + this.enabledCipherSuites = enabledCipherSuites; + this.enabledProtocols = enabledProtocols; + this.needClientAuth = needClientAuth; + this.sslSocketFactory = getDefaultSSLSocketFactory(); + } + + public String[] getEnabledCipherSuites() + { + return enabledCipherSuites; + } + + public String[] getEnabledProtocols() + { + return enabledProtocols; + } + + public boolean isNeedClientAuth() + { + return needClientAuth; + } + + @Override + public ServerSocket createServerSocket(int pPort) throws IOException + { + ServerSocket result = createSslServerSocket(pPort); + try + { + result.setReuseAddress(true); + } + catch (SocketException e) + { + result.close(); + throw e; + } + sockets.add(result); + return result; + } + + private ServerSocket createSslServerSocket(int pPort) throws IOException + { + return new ServerSocket(pPort, 0, bindAddress) + { + public Socket accept() throws IOException + { + Socket socket = super.accept(); + SSLSocket sslSocket = (SSLSocket) sslSocketFactory.createSocket( + socket, socket.getInetAddress().getHostName(), + socket.getPort(), true); + sslSocket.setUseClientMode(false); + if (enabledCipherSuites != null) + { + sslSocket.setEnabledCipherSuites(enabledCipherSuites); + } + if (enabledProtocols != null) + { + sslSocket.setEnabledProtocols(enabledProtocols); + } + sslSocket.setNeedClientAuth(needClientAuth); + return sslSocket; + } + }; + } + + @Override + public void close() throws IOException + { + for (ServerSocket socket : sockets) + { + socket.close(); + } + } + + @Override + public boolean equals(Object o) + { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + CollectingSslRMIServerSocketFactoryImpl that = (CollectingSslRMIServerSocketFactoryImpl) o; + return Objects.equals(bindAddress, that.bindAddress); + } + + @Override + public int hashCode() + { + return Objects.hash(bindAddress); + } + + private static SSLSocketFactory defaultSSLSocketFactory = null; + + private static synchronized SSLSocketFactory getDefaultSSLSocketFactory() + { + if (defaultSSLSocketFactory == null) + defaultSSLSocketFactory = + (SSLSocketFactory) SSLSocketFactory.getDefault(); + return defaultSSLSocketFactory; + } +} diff --git a/test/distributed/org/apache/cassandra/distributed/impl/Instance.java b/test/distributed/org/apache/cassandra/distributed/impl/Instance.java index fe4a25be43..7b5501fbad 100644 --- a/test/distributed/org/apache/cassandra/distributed/impl/Instance.java +++ b/test/distributed/org/apache/cassandra/distributed/impl/Instance.java @@ -708,9 +708,9 @@ public class Instance extends IsolatedExecutor implements IInvokableInstance assert config.networkTopology().contains(config.broadcastAddress()) : String.format("Network topology %s doesn't contain the address %s", config.networkTopology(), config.broadcastAddress()); DistributedTestSnitch.assign(config.networkTopology()); + DatabaseDescriptor.daemonInitialization(); if (config.has(JMX)) startJmx(); - DatabaseDescriptor.daemonInitialization(); LoggingSupportFactory.getLoggingSupport().onStartup(); logSystemInfo(inInstancelogger); diff --git a/test/distributed/org/apache/cassandra/distributed/impl/IsolatedJmx.java b/test/distributed/org/apache/cassandra/distributed/impl/IsolatedJmx.java index 7812ec89f8..6a575972eb 100644 --- a/test/distributed/org/apache/cassandra/distributed/impl/IsolatedJmx.java +++ b/test/distributed/org/apache/cassandra/distributed/impl/IsolatedJmx.java @@ -22,9 +22,9 @@ import java.io.IOException; import java.net.InetAddress; import java.util.HashMap; import java.util.LinkedList; +import java.util.List; import java.util.Map; import java.util.concurrent.TimeUnit; - import javax.management.remote.JMXConnector; import javax.management.remote.JMXConnectorServer; import javax.management.remote.JMXServiceURL; @@ -34,12 +34,14 @@ import javax.management.remote.rmi.RMIJRMPServerImpl; import com.google.common.util.concurrent.Uninterruptibles; import org.slf4j.Logger; +import org.apache.cassandra.config.EncryptionOptions; import org.apache.cassandra.distributed.api.IInstance; import org.apache.cassandra.distributed.api.IInstanceConfig; import org.apache.cassandra.distributed.shared.JMXUtil; import org.apache.cassandra.utils.JMXServerUtils; import org.apache.cassandra.utils.MBeanWrapper; -import org.apache.cassandra.utils.RMIClientSocketFactoryImpl; +import org.apache.cassandra.utils.RMICloseableClientSocketFactory; +import org.apache.cassandra.utils.RMICloseableServerSocketFactory; import sun.rmi.transport.tcp.TCPEndpoint; import static org.apache.cassandra.config.CassandraRelevantProperties.JAVA_RMI_DGC_LEASE_VALUE_IN_JVM_DTEST; @@ -57,17 +59,20 @@ public class IsolatedJmx private JMXServerUtils.JmxRegistry registry; private RMIJRMPServerImpl jmxRmiServer; private MBeanWrapper.InstanceMBeanWrapper wrapper; - private RMIClientSocketFactoryImpl clientSocketFactory; - private CollectingRMIServerSocketFactoryImpl serverSocketFactory; + private RMICloseableClientSocketFactory clientSocketFactory; + private RMICloseableServerSocketFactory serverSocketFactory; private Logger inInstancelogger; private IInstanceConfig config; - public IsolatedJmx(IInstance instance, Logger inInstanceLogger) { + public IsolatedJmx(IInstance instance, Logger inInstanceLogger) + { this.config = instance.config(); this.inInstancelogger = inInstanceLogger; } - public void startJmx() { + @SuppressWarnings("unchecked") + public void startJmx() + { try { // Several RMI threads hold references to in-jvm dtest objects, and are, by default, kept @@ -84,14 +89,17 @@ public class IsolatedJmx String hostname = addr.getHostAddress(); wrapper = new MBeanWrapper.InstanceMBeanWrapper(hostname + ":" + jmxPort); ((MBeanWrapper.DelegatingMbeanWrapper) MBeanWrapper.instance).setDelegate(wrapper); - Map env = new HashMap<>(); - serverSocketFactory = new CollectingRMIServerSocketFactoryImpl(addr); - env.put(RMIConnectorServer.RMI_SERVER_SOCKET_FACTORY_ATTRIBUTE, - serverSocketFactory); - clientSocketFactory = new RMIClientSocketFactoryImpl(addr); - env.put(RMIConnectorServer.RMI_CLIENT_SOCKET_FACTORY_ATTRIBUTE, - clientSocketFactory); + // CASSANDRA-18508: Sensitive JMX SSL configuration options can be easily exposed + Map encryptionOptionsMap = (Map) config.getParams().get("jmx_encryption_options"); + EncryptionOptions jmxEncryptionOptions = getJmxEncryptionOptions(encryptionOptionsMap); + // Here the `localOnly` is always passed as true as it is for the local isolated JMX testing + // However if the `jmxEncryptionOptions` are provided or JMX SSL configuration is set it will configure + // the socket factories appropriately. + Map socketFactories = new IsolatedJmxSocketFactory().configure(addr, true, jmxEncryptionOptions); + serverSocketFactory = (RMICloseableServerSocketFactory) socketFactories.get(RMIConnectorServer.RMI_SERVER_SOCKET_FACTORY_ATTRIBUTE); + clientSocketFactory = (RMICloseableClientSocketFactory) socketFactories.get(RMIConnectorServer.RMI_CLIENT_SOCKET_FACTORY_ATTRIBUTE); + Map env = new HashMap<>(socketFactories); // configure the RMI registry registry = new JMXServerUtils.JmxRegistry(jmxPort, @@ -137,6 +145,49 @@ public class IsolatedJmx } } + /** + * Builds {@code EncryptionOptions} from the map based SSL configuration properties. + * + * @param encryptionOptionsMap of SSL configuration properties + * @return EncryptionOptions built object + */ + @SuppressWarnings("unchecked") + private EncryptionOptions getJmxEncryptionOptions(Map encryptionOptionsMap) + { + if (encryptionOptionsMap == null) + { + return null; + } + EncryptionOptions jmxEncryptionOptions = new EncryptionOptions(); + String[] cipherSuitesArray = (String[]) encryptionOptionsMap.get(EncryptionOptions.ConfigKey.CIPHER_SUITES.toString()); + if (cipherSuitesArray != null) + { + jmxEncryptionOptions = jmxEncryptionOptions.withCipherSuites(cipherSuitesArray); + } + List acceptedProtocols = (List) encryptionOptionsMap.get(EncryptionOptions.ConfigKey.ACCEPTED_PROTOCOLS.toString()); + if (acceptedProtocols != null) + { + jmxEncryptionOptions = jmxEncryptionOptions.withAcceptedProtocols(acceptedProtocols); + } + + Boolean requireClientAuthValue = (Boolean) encryptionOptionsMap.get(EncryptionOptions.ConfigKey.REQUIRE_CLIENT_AUTH.toString()); + EncryptionOptions.ClientAuth requireClientAuth = requireClientAuthValue == null ? + EncryptionOptions.ClientAuth.NOT_REQUIRED : + EncryptionOptions.ClientAuth.from(String.valueOf(requireClientAuthValue)); + Object enabledOption = encryptionOptionsMap.get(EncryptionOptions.ConfigKey.ENABLED.toString()); + boolean enabled = enabledOption != null ? (Boolean) encryptionOptionsMap.get(EncryptionOptions.ConfigKey.ENABLED.toString()) : false; + + //CASSANDRA-18508 NOTE - We do not populate sslContextFactory configuration here for tests, it could be enhanced + jmxEncryptionOptions = jmxEncryptionOptions + .withKeyStore((String) encryptionOptionsMap.get(EncryptionOptions.ConfigKey.KEYSTORE.toString())) + .withKeyStorePassword((String) encryptionOptionsMap.get(EncryptionOptions.ConfigKey.KEYSTORE_PASSWORD.toString())) + .withTrustStore((String) encryptionOptionsMap.get(EncryptionOptions.ConfigKey.TRUSTSTORE.toString())) + .withTrustStorePassword((String) encryptionOptionsMap.get(EncryptionOptions.ConfigKey.TRUSTSTORE_PASSWORD.toString())) + .withRequireClientAuth(requireClientAuth) + .withEnabled(enabled); + return jmxEncryptionOptions; + } + private void waitForJmxAvailability(Map env) { try (JMXConnector ignored = JMXUtil.getJmxConnector(config, 20, env)) diff --git a/test/distributed/org/apache/cassandra/distributed/impl/IsolatedJmxSocketFactory.java b/test/distributed/org/apache/cassandra/distributed/impl/IsolatedJmxSocketFactory.java new file mode 100644 index 0000000000..868cb8ae4b --- /dev/null +++ b/test/distributed/org/apache/cassandra/distributed/impl/IsolatedJmxSocketFactory.java @@ -0,0 +1,96 @@ +/* + * 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.distributed.impl; + +import java.net.InetAddress; +import java.util.Arrays; +import java.util.Map; +import java.util.stream.Collectors; +import javax.management.remote.rmi.RMIConnectorServer; +import javax.net.ssl.SSLContext; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import org.apache.cassandra.config.CassandraRelevantProperties; +import org.apache.cassandra.utils.RMIClientSocketFactoryImpl; +import org.apache.cassandra.utils.jmx.AbstractJmxSocketFactory; + +/** + * JMX Socket factory used for the isolated JMX testing. + */ +public class IsolatedJmxSocketFactory extends AbstractJmxSocketFactory +{ + private static final Logger logger = LoggerFactory.getLogger(IsolatedJmxSocketFactory.class); + + @Override + public void configureLocalSocketFactories(Map env, InetAddress serverAddress) + { + CollectingRMIServerSocketFactoryImpl serverSocketFactory = new CollectingRMIServerSocketFactoryImpl(serverAddress); + env.put(RMIConnectorServer.RMI_SERVER_SOCKET_FACTORY_ATTRIBUTE, + serverSocketFactory); + RMIClientSocketFactoryImpl clientSocketFactory = new RMIClientSocketFactoryImpl(serverAddress); + env.put(RMIConnectorServer.RMI_CLIENT_SOCKET_FACTORY_ATTRIBUTE, + clientSocketFactory); + } + + @Override + public void configureSslClientSocketFactory(Map env, InetAddress serverAddress) + { + RMISslClientSocketFactoryImpl clientFactory = new RMISslClientSocketFactoryImpl(serverAddress, + CassandraRelevantProperties.JAVAX_RMI_SSL_CLIENT_ENABLED_CIPHER_SUITES.getString(), + CassandraRelevantProperties.JAVAX_RMI_SSL_CLIENT_ENABLED_PROTOCOLS.getString()); + env.put(RMIConnectorServer.RMI_CLIENT_SOCKET_FACTORY_ATTRIBUTE, clientFactory); + env.put("com.sun.jndi.rmi.factory.socket", clientFactory); + } + + @Override + public void configureSslServerSocketFactory(Map env, InetAddress serverAddress, String[] enabledCipherSuites, + String[] enabledProtocols, boolean needClientAuth) + { + CollectingSslRMIServerSocketFactoryImpl serverFactory = new CollectingSslRMIServerSocketFactoryImpl(serverAddress, + enabledCipherSuites, + enabledProtocols, + needClientAuth); + env.put(RMIConnectorServer.RMI_SERVER_SOCKET_FACTORY_ATTRIBUTE, serverFactory); + logJmxSslConfig(serverFactory); + } + + @Override + public void configureSslServerSocketFactory(Map env, InetAddress serverAddress, String[] enabledCipherSuites, + String[] enabledProtocols, boolean needClientAuth, SSLContext sslContext) + { + CollectingSslRMIServerSocketFactoryImpl serverFactory = new CollectingSslRMIServerSocketFactoryImpl(serverAddress, + enabledCipherSuites, + enabledProtocols, + needClientAuth, + sslContext); + env.put(RMIConnectorServer.RMI_SERVER_SOCKET_FACTORY_ATTRIBUTE, serverFactory); + logJmxSslConfig(serverFactory); + } + + private void logJmxSslConfig(CollectingSslRMIServerSocketFactoryImpl serverFactory) + { + if (logger.isDebugEnabled()) + logger.debug("JMX SSL configuration. { protocols: [{}], cipher_suites: [{}], require_client_auth: {} }", + serverFactory.getEnabledProtocols() == null ? "'JVM defaults'" : Arrays.stream(serverFactory.getEnabledProtocols()).collect(Collectors.joining("','", "'", "'")), + serverFactory.getEnabledCipherSuites() == null ? "'JVM defaults'" : Arrays.stream(serverFactory.getEnabledCipherSuites()).collect(Collectors.joining("','", "'", "'")), + serverFactory.isNeedClientAuth()); + } +} diff --git a/test/distributed/org/apache/cassandra/distributed/impl/JmxTestClientSslContextFactory.java b/test/distributed/org/apache/cassandra/distributed/impl/JmxTestClientSslContextFactory.java new file mode 100644 index 0000000000..8247d8e9d4 --- /dev/null +++ b/test/distributed/org/apache/cassandra/distributed/impl/JmxTestClientSslContextFactory.java @@ -0,0 +1,135 @@ +/* + * 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.distributed.impl; + +import java.io.InputStream; +import java.nio.file.Files; +import java.security.KeyStore; +import java.util.Map; +import javax.net.ssl.KeyManagerFactory; +import javax.net.ssl.SSLContext; +import javax.net.ssl.SSLException; +import javax.net.ssl.TrustManager; +import javax.net.ssl.TrustManagerFactory; + +import org.apache.commons.lang3.StringUtils; + +import org.apache.cassandra.config.EncryptionOptions; +import org.apache.cassandra.io.util.File; + +/** + * Simplified and independent version of {@link org.apache.cassandra.security.FileBasedSslContextFactory} for + * testing SSL based JMX clients that require configuring keystore and/or truststore. + */ +public class JmxTestClientSslContextFactory +{ + private final Map parameters; + // keystore is not needed when the JMX server does not require client-auth + private final String keystore; + // keystore could be null in case JMX server does not require client-auth + private final String keystore_password; + private final String truststore; + private final String truststore_password; + private final String protocol; + private final String algorithm; + private final String store_type; + + public JmxTestClientSslContextFactory(Map parameters) + { + this.parameters = parameters; + keystore = getString(EncryptionOptions.ConfigKey.KEYSTORE.toString()); + keystore_password = getString(EncryptionOptions.ConfigKey.KEYSTORE_PASSWORD.toString()); + truststore = getString(EncryptionOptions.ConfigKey.TRUSTSTORE.toString()); + truststore_password = getString(EncryptionOptions.ConfigKey.TRUSTSTORE_PASSWORD.toString()); + protocol = getString(EncryptionOptions.ConfigKey.PROTOCOL.toString(), "TLS"); + algorithm = getString(EncryptionOptions.ConfigKey.ALGORITHM.toString()); + store_type = getString(EncryptionOptions.ConfigKey.STORE_TYPE.toString(), "JKS"); + } + + private String getString(String key, String defaultValue) + { + return parameters.get(key) == null ? defaultValue : (String) parameters.get(key); + } + + private String getString(String key) + { + return (String) parameters.get(key); + } + + private TrustManagerFactory buildTrustManagerFactory() throws SSLException + { + try (InputStream tsf = Files.newInputStream(File.getPath(truststore))) + { + final String algorithm = this.algorithm == null ? TrustManagerFactory.getDefaultAlgorithm() : this.algorithm; + TrustManagerFactory tmf = TrustManagerFactory.getInstance(algorithm); + KeyStore ts = KeyStore.getInstance(store_type); + + final char[] truststorePassword = StringUtils.isEmpty(truststore_password) ? null : truststore_password.toCharArray(); + ts.load(tsf, truststorePassword); + tmf.init(ts); + return tmf; + } + catch (Exception e) + { + throw new SSLException("failed to build trust manager store for secure connections", e); + } + } + + private KeyManagerFactory buildKeyManagerFactory() throws SSLException + { + final String algorithm = this.algorithm == null ? KeyManagerFactory.getDefaultAlgorithm() : this.algorithm; + + if (keystore != null) + { + try (InputStream ksf = Files.newInputStream(File.getPath(keystore))) + { + KeyManagerFactory kmf = KeyManagerFactory.getInstance(algorithm); + KeyStore ks = KeyStore.getInstance(store_type); + final char[] password = keystore_password.toCharArray(); + ks.load(ksf, password); + kmf.init(ks, password); + return kmf; + } + catch (Exception e) + { + throw new SSLException("failed to build key manager store for secure connections", e); + } + } + else + { + return null; + } + } + + public SSLContext createSSLContext() throws SSLException + { + TrustManager[] trustManagers = buildTrustManagerFactory().getTrustManagers(); + KeyManagerFactory kmf = buildKeyManagerFactory(); + try + { + SSLContext ctx = SSLContext.getInstance(protocol); + ctx.init(kmf != null ? kmf.getKeyManagers() : null, trustManagers, null); + return ctx; + } + catch (Exception e) + { + throw new SSLException("Error creating/initializing the SSL Context", e); + } + } +} diff --git a/test/distributed/org/apache/cassandra/distributed/impl/JmxTestClientSslSocketFactory.java b/test/distributed/org/apache/cassandra/distributed/impl/JmxTestClientSslSocketFactory.java new file mode 100644 index 0000000000..db802c031e --- /dev/null +++ b/test/distributed/org/apache/cassandra/distributed/impl/JmxTestClientSslSocketFactory.java @@ -0,0 +1,60 @@ +/* + * 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.distributed.impl; + +import java.io.IOException; +import java.io.Serializable; +import java.net.Socket; +import javax.net.SocketFactory; +import javax.net.ssl.SSLContext; +import javax.net.ssl.SSLSocket; +import javax.net.ssl.SSLSocketFactory; +import javax.rmi.ssl.SslRMIClientSocketFactory; + +/** + * {@code RMIClientSocketFactory} for testing SSL based JMX clients. + */ +public class JmxTestClientSslSocketFactory extends SslRMIClientSocketFactory implements Serializable +{ + private static final long serialVersionUID = 818579127759449333L; + private final SocketFactory defaultSocketFactory; + private final String[] cipherSuites; + private final String[] acceptedProtocols; + + public JmxTestClientSslSocketFactory(SSLContext sslContext, String[] cipherSuites, String[] acceptedProtocols) + { + this.cipherSuites = cipherSuites; + this.acceptedProtocols = acceptedProtocols; + defaultSocketFactory = sslContext == null ? SSLSocketFactory.getDefault() : sslContext.getSocketFactory(); + } + + @Override + public Socket createSocket(String host, int port) throws IOException + { + final SSLSocket sslSocket = (SSLSocket) defaultSocketFactory.createSocket(host, port); + + if (cipherSuites != null) + sslSocket.setEnabledCipherSuites(cipherSuites); + + if (acceptedProtocols != null) + sslSocket.setEnabledProtocols(acceptedProtocols); + + return sslSocket; + } +} diff --git a/test/distributed/org/apache/cassandra/distributed/impl/RMISslClientSocketFactoryImpl.java b/test/distributed/org/apache/cassandra/distributed/impl/RMISslClientSocketFactoryImpl.java new file mode 100644 index 0000000000..bca6b1fb6a --- /dev/null +++ b/test/distributed/org/apache/cassandra/distributed/impl/RMISslClientSocketFactoryImpl.java @@ -0,0 +1,131 @@ +/* + * 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.distributed.impl; + +import java.io.IOException; +import java.io.Serializable; +import java.net.InetAddress; +import java.net.Socket; +import java.util.ArrayList; +import java.util.List; +import java.util.Objects; +import java.util.regex.Pattern; +import javax.net.SocketFactory; +import javax.net.ssl.SSLSocket; +import javax.net.ssl.SSLSocketFactory; + +import org.apache.cassandra.utils.RMICloseableClientSocketFactory; + +/** + * {@code RMIClientSocketFactory} for testing SSL based JMX clients. + * This class is used to override the local address the JMX client calculates when trying to connect, + * which can otherwise be influenced by the system property "java.rmi.server.hostname" in strange and + * unpredictable ways. + */ +public class RMISslClientSocketFactoryImpl implements Serializable, RMICloseableClientSocketFactory +{ + private static final long serialVersionUID = 9054380061905145241L; + private static final Pattern COMMA_SPLITTER = Pattern.compile(","); + private static final List sockets = new ArrayList<>(); + private final InetAddress localAddress; + private final String[] enabledCipherSuites; + private final String[] enabledProtocols; + + public RMISslClientSocketFactoryImpl(InetAddress localAddress, String enabledCipherSuites, String enabledProtocls) + { + this.localAddress = localAddress; + this.enabledCipherSuites = splitCommaSeparatedString(enabledCipherSuites); + this.enabledProtocols = splitCommaSeparatedString(enabledProtocls); + } + + @Override + public Socket createSocket(String host, int port) throws IOException + { + Socket socket = createSslSocket(port); + sockets.add(socket); + return socket; + } + + private Socket createSslSocket(int port) throws IOException + { + final SocketFactory sslSocketFactory = SSLSocketFactory.getDefault(); + final SSLSocket sslSocket = (SSLSocket) sslSocketFactory.createSocket(localAddress, port); + if (enabledCipherSuites != null) + { + try + { + sslSocket.setEnabledCipherSuites(enabledCipherSuites); + } + catch (IllegalArgumentException e) + { + throw new IOException(e.getMessage(), e); + } + } + if (enabledProtocols != null) + { + try + { + sslSocket.setEnabledProtocols(enabledProtocols); + } + catch (IllegalArgumentException e) + { + throw new IOException(e.getMessage(), e); + } + } + return sslSocket; + } + + @Override + public void close() throws IOException + { + for (Socket socket : sockets) + { + try + { + socket.close(); + } + catch (IOException ignored) + { + // intentionally ignored + } + } + } + + @Override + public boolean equals(Object o) + { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + RMISslClientSocketFactoryImpl that = (RMISslClientSocketFactoryImpl) o; + return Objects.equals(localAddress, that.localAddress); + } + + @Override + public int hashCode() + { + return Objects.hash(localAddress); + } + + private String[] splitCommaSeparatedString(String stringToSplit) + { + if (stringToSplit == null) + return null; + return COMMA_SPLITTER.split(stringToSplit); + } +} diff --git a/test/distributed/org/apache/cassandra/distributed/shared/WithProperties.java b/test/distributed/org/apache/cassandra/distributed/shared/WithProperties.java index d17d3e6f3a..9c476525f8 100644 --- a/test/distributed/org/apache/cassandra/distributed/shared/WithProperties.java +++ b/test/distributed/org/apache/cassandra/distributed/shared/WithProperties.java @@ -72,6 +72,13 @@ public final class WithProperties implements AutoCloseable return set(prop, () -> prop.setLong(value)); } + public WithProperties preserve(CassandraRelevantProperties prop) + { + String previous = prop.getString(); // because all properties are strings + rollback.add(previous == null ? prop::clearValue : () -> prop.setString(previous)); + return this; + } + private void with(String key, String value) { String previous = System.setProperty(key, value); // checkstyle: suppress nearby 'blockSystemPropertyUsage' diff --git a/test/distributed/org/apache/cassandra/distributed/test/AbstractEncryptionOptionsImpl.java b/test/distributed/org/apache/cassandra/distributed/test/AbstractEncryptionOptionsImpl.java index 51af668c29..25e9e64b63 100644 --- a/test/distributed/org/apache/cassandra/distributed/test/AbstractEncryptionOptionsImpl.java +++ b/test/distributed/org/apache/cassandra/distributed/test/AbstractEncryptionOptionsImpl.java @@ -70,19 +70,19 @@ public class AbstractEncryptionOptionsImpl extends TestBaseImpl final static String validTrustStorePassword = TlsTestUtils.SERVER_TRUSTSTORE_PASSWORD; // Base configuration map for a valid keystore that can be opened - final static Map validKeystore = ImmutableMap.of("keystore", validKeyStorePath, - "keystore_password", validKeyStorePassword, - "truststore", validTrustStorePath, - "truststore_password", validTrustStorePassword); + protected final static Map validFileBasedKeystores = ImmutableMap.of("keystore", validKeyStorePath, + "keystore_password", validKeyStorePassword, + "truststore", validTrustStorePath, + "truststore_password", validTrustStorePassword); // Configuration with a valid keystore, but an unknown protocol final static Map nonExistantProtocol = ImmutableMap.builder() - .putAll(validKeystore) + .putAll(validFileBasedKeystores) .put("accepted_protocols", Collections.singletonList("NoProtocolIKnow")) .build(); // Configuration with a valid keystore, but an unknown cipher suite final static Map nonExistantCipher = ImmutableMap.builder() - .putAll(validKeystore) + .putAll(validFileBasedKeystores) .put("cipher_suites", Collections.singletonList("NoCipherIKnow")) .build(); @@ -340,7 +340,7 @@ public class AbstractEncryptionOptionsImpl extends TestBaseImpl } /* Provde the cluster cannot start with the configured options */ - void assertCannotStartDueToConfigurationException(Cluster cluster) + protected void assertCannotStartDueToConfigurationException(Cluster cluster) { Throwable tr = null; try diff --git a/test/distributed/org/apache/cassandra/distributed/test/InternodeEncryptionOptionsTest.java b/test/distributed/org/apache/cassandra/distributed/test/InternodeEncryptionOptionsTest.java index 83bcaaad3c..a955757c9b 100644 --- a/test/distributed/org/apache/cassandra/distributed/test/InternodeEncryptionOptionsTest.java +++ b/test/distributed/org/apache/cassandra/distributed/test/InternodeEncryptionOptionsTest.java @@ -53,7 +53,7 @@ public class InternodeEncryptionOptionsTest extends AbstractEncryptionOptionsImp c.with(Feature.NETWORK); c.set("ssl_storage_port", 7013); c.set("server_encryption_options", - ImmutableMap.builder().putAll(validKeystore) + ImmutableMap.builder().putAll(validFileBasedKeystores) .put("internode_encryption", "none") .put("optional", false) .put("legacy_ssl_storage_port_enabled", "true") @@ -87,7 +87,7 @@ public class InternodeEncryptionOptionsTest extends AbstractEncryptionOptionsImp { try (Cluster cluster = builder().withNodes(1).withConfig(c -> { c.with(Feature.NETWORK); - c.set("server_encryption_options", validKeystore); + c.set("server_encryption_options", validFileBasedKeystores); }).createWithoutStarting()) { InetAddress address = cluster.get(1).config().broadcastAddress().getAddress(); @@ -111,7 +111,7 @@ public class InternodeEncryptionOptionsTest extends AbstractEncryptionOptionsImp c.set("storage_port", 7012); c.set("ssl_storage_port", 7013); c.set("server_encryption_options", - ImmutableMap.builder().putAll(validKeystore) + ImmutableMap.builder().putAll(validFileBasedKeystores) .put("internode_encryption", "none") .put("optional", true) .put("legacy_ssl_storage_port_enabled", "true") @@ -146,7 +146,7 @@ public class InternodeEncryptionOptionsTest extends AbstractEncryptionOptionsImp c.set("storage_port", 7012); // must match in-jvm dtest assigned ports c.set("ssl_storage_port", 7012); c.set("server_encryption_options", - ImmutableMap.builder().putAll(validKeystore) + ImmutableMap.builder().putAll(validFileBasedKeystores) .put("internode_encryption", "none") .put("optional", true) .put("legacy_ssl_storage_port_enabled", "true") @@ -174,7 +174,7 @@ public class InternodeEncryptionOptionsTest extends AbstractEncryptionOptionsImp try (Cluster cluster = builder().withNodes(1).withConfig(c -> { c.with(Feature.NETWORK); c.set("server_encryption_options", - ImmutableMap.builder().putAll(validKeystore) + ImmutableMap.builder().putAll(validFileBasedKeystores) .put("internode_encryption", "none") .put("optional", false) .build()); @@ -202,7 +202,7 @@ public class InternodeEncryptionOptionsTest extends AbstractEncryptionOptionsImp .with(Feature.GOSSIP) // To make sure AllMembersAliveMonitor checks gossip (which uses internode conns) .with(Feature.NATIVE_PROTOCOL); // For virtual tables c.set("server_encryption_options", - ImmutableMap.builder().putAll(validKeystore) + ImmutableMap.builder().putAll(validFileBasedKeystores) .put("internode_encryption", "all") .build()); }).start()) @@ -234,7 +234,7 @@ public class InternodeEncryptionOptionsTest extends AbstractEncryptionOptionsImp try (Cluster cluster = builder().withNodes(1).withConfig(c -> { c.with(Feature.NETWORK); c.set("server_encryption_options", - ImmutableMap.builder().putAll(validKeystore) + ImmutableMap.builder().putAll(validFileBasedKeystores) .put("internode_encryption", "all") .put("accepted_protocols", ImmutableList.of("TLSv1.1", "TLSv1.2", "TLSv1.3")) .build()); @@ -272,7 +272,7 @@ public class InternodeEncryptionOptionsTest extends AbstractEncryptionOptionsImp try (Cluster cluster = builder().withNodes(1).withConfig(c -> { c.with(Feature.NETWORK); c.set("server_encryption_options", - ImmutableMap.builder().putAll(validKeystore) + ImmutableMap.builder().putAll(validFileBasedKeystores) .put("internode_encryption", "all") .put("accepted_protocols", Collections.singletonList("TLSv1.2")) .put("cipher_suites", Collections.singletonList("TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384")) diff --git a/test/distributed/org/apache/cassandra/distributed/test/NativeTransportEncryptionOptionsTest.java b/test/distributed/org/apache/cassandra/distributed/test/NativeTransportEncryptionOptionsTest.java index 9eb7c7afeb..b23f501e69 100644 --- a/test/distributed/org/apache/cassandra/distributed/test/NativeTransportEncryptionOptionsTest.java +++ b/test/distributed/org/apache/cassandra/distributed/test/NativeTransportEncryptionOptionsTest.java @@ -87,7 +87,7 @@ public class NativeTransportEncryptionOptionsTest extends AbstractEncryptionOpti { try (Cluster cluster = builder().withNodes(1).withConfig(c -> { c.with(Feature.NATIVE_PROTOCOL); - c.set("client_encryption_options", validKeystore); + c.set("client_encryption_options", validFileBasedKeystores); }).createWithoutStarting()) { InetAddress address = cluster.get(1).config().broadcastAddress().getAddress(); @@ -119,7 +119,7 @@ public class NativeTransportEncryptionOptionsTest extends AbstractEncryptionOpti try (Cluster cluster = builder().withNodes(1).withConfig(c -> { c.with(Feature.NATIVE_PROTOCOL); c.set("client_encryption_options", - ImmutableMap.builder().putAll(validKeystore) + ImmutableMap.builder().putAll(validFileBasedKeystores) .put("enabled", true) .put("accepted_protocols", ImmutableList.of("TLSv1.1", "TLSv1.2", "TLSv1.3")) .build()); @@ -156,7 +156,7 @@ public class NativeTransportEncryptionOptionsTest extends AbstractEncryptionOpti try (Cluster cluster = builder().withNodes(1).withConfig(c -> { c.with(Feature.NATIVE_PROTOCOL); c.set("client_encryption_options", - ImmutableMap.builder().putAll(validKeystore) + ImmutableMap.builder().putAll(validFileBasedKeystores) .put("enabled", true) .put("accepted_protocols", Collections.singletonList("TLSv1.2")) .put("cipher_suites", Collections.singletonList("TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384")) @@ -237,7 +237,7 @@ public class NativeTransportEncryptionOptionsTest extends AbstractEncryptionOpti try (Cluster cluster = builder().withNodes(1).withConfig(c -> { c.with(Feature.NATIVE_PROTOCOL); c.set("client_encryption_options", - ImmutableMap.builder().putAll(validKeystore) + ImmutableMap.builder().putAll(validFileBasedKeystores) .put("enabled", true) .put("require_client_auth", "optional") .put("optional", true) @@ -282,7 +282,7 @@ public class NativeTransportEncryptionOptionsTest extends AbstractEncryptionOpti try (Cluster cluster = builder().withNodes(1).withConfig(c -> { c.with(Feature.NATIVE_PROTOCOL); c.set("client_encryption_options", - ImmutableMap.builder().putAll(validKeystore) + ImmutableMap.builder().putAll(validFileBasedKeystores) .put("enabled", true) .put("require_client_auth", "optional") .put("optional", false) @@ -329,7 +329,7 @@ public class NativeTransportEncryptionOptionsTest extends AbstractEncryptionOpti // Server configuration for optional mTLS mode c.with(Feature.NATIVE_PROTOCOL); c.set("client_encryption_options", - ImmutableMap.builder().putAll(validKeystore) + ImmutableMap.builder().putAll(validFileBasedKeystores) .put("enabled", true) .put("require_client_auth", "false") .put("optional", true) @@ -384,7 +384,7 @@ public class NativeTransportEncryptionOptionsTest extends AbstractEncryptionOpti try (Cluster cluster = builder().withNodes(1).withConfig(c -> { c.with(Feature.NATIVE_PROTOCOL); c.set("client_encryption_options", - ImmutableMap.builder().putAll(validKeystore) + ImmutableMap.builder().putAll(validFileBasedKeystores) .put("enabled", true) .put("require_client_auth", true) .put("require_endpoint_verification", requireEndpointVerification) diff --git a/test/distributed/org/apache/cassandra/distributed/test/ResourceLeakTest.java b/test/distributed/org/apache/cassandra/distributed/test/ResourceLeakTest.java index d1a5d08152..03cb62d5cb 100644 --- a/test/distributed/org/apache/cassandra/distributed/test/ResourceLeakTest.java +++ b/test/distributed/org/apache/cassandra/distributed/test/ResourceLeakTest.java @@ -49,7 +49,7 @@ import static org.apache.cassandra.distributed.api.Feature.GOSSIP; import static org.apache.cassandra.distributed.api.Feature.JMX; import static org.apache.cassandra.distributed.api.Feature.NATIVE_PROTOCOL; import static org.apache.cassandra.distributed.api.Feature.NETWORK; -import static org.apache.cassandra.distributed.test.jmx.JMXGetterCheckTest.testAllValidGetters; +import static org.apache.cassandra.distributed.test.jmx.JMXTestsUtil.testAllValidGetters; import static org.apache.cassandra.utils.FBUtilities.now; import static org.hamcrest.Matchers.lessThanOrEqualTo; import static org.hamcrest.Matchers.startsWith; @@ -167,7 +167,7 @@ public class ResourceLeakTest extends TestBaseImpl Assert.assertThat(defaultDomain, startsWith(JMXUtil.getJmxHost(config) + ":" + config.jmxPort())); } } - testAllValidGetters(cluster); + testAllValidGetters(cluster, null); } catch (Exception e) { diff --git a/test/distributed/org/apache/cassandra/distributed/test/SSTableLoaderEncryptionOptionsTest.java b/test/distributed/org/apache/cassandra/distributed/test/SSTableLoaderEncryptionOptionsTest.java index 94ea1d0441..ea3c980586 100644 --- a/test/distributed/org/apache/cassandra/distributed/test/SSTableLoaderEncryptionOptionsTest.java +++ b/test/distributed/org/apache/cassandra/distributed/test/SSTableLoaderEncryptionOptionsTest.java @@ -61,12 +61,12 @@ public class SSTableLoaderEncryptionOptionsTest extends AbstractEncryptionOption CLUSTER = Cluster.build().withNodes(1).withConfig(c -> { c.with(Feature.NATIVE_PROTOCOL, Feature.NETWORK, Feature.GOSSIP); // need gossip to get hostid for java driver c.set("server_encryption_options", - ImmutableMap.builder().putAll(validKeystore) + ImmutableMap.builder().putAll(validFileBasedKeystores) .put("internode_encryption", "all") .put("optional", false) .build()); c.set("client_encryption_options", - ImmutableMap.builder().putAll(validKeystore) + ImmutableMap.builder().putAll(validFileBasedKeystores) .put("enabled", true) .put("optional", false) .put("accepted_protocols", Collections.singletonList("TLSv1.2")) diff --git a/test/distributed/org/apache/cassandra/distributed/test/WithPropertiesTest.java b/test/distributed/org/apache/cassandra/distributed/test/WithPropertiesTest.java new file mode 100644 index 0000000000..ad3c6dbd4d --- /dev/null +++ b/test/distributed/org/apache/cassandra/distributed/test/WithPropertiesTest.java @@ -0,0 +1,44 @@ +/* + * 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.distributed.test; + +import org.junit.Assert; +import org.junit.Test; + +import org.apache.cassandra.config.CassandraRelevantProperties; +import org.apache.cassandra.distributed.shared.WithProperties; + +public class WithPropertiesTest +{ + @Test + public void testPreserveBeforeSet() + { + CassandraRelevantProperties booleanProperty = CassandraRelevantProperties.COM_SUN_MANAGEMENT_JMXREMOTE_SSL; + boolean defaultPropertyValue = Boolean.parseBoolean(booleanProperty.getDefaultValue()); + boolean newPropertyValue = true; + + try (WithProperties properties = new WithProperties() + .preserve(booleanProperty) + .set(booleanProperty, newPropertyValue)) + { + Assert.assertEquals("Property value must match", newPropertyValue, booleanProperty.getBoolean()); + } + Assert.assertEquals("Property value must revert to the default value", defaultPropertyValue, booleanProperty.getBoolean()); + } +} diff --git a/test/distributed/org/apache/cassandra/distributed/test/jmx/JMXFeatureTest.java b/test/distributed/org/apache/cassandra/distributed/test/jmx/JMXFeatureTest.java index 2ffddaed7e..99b90a2ec0 100644 --- a/test/distributed/org/apache/cassandra/distributed/test/jmx/JMXFeatureTest.java +++ b/test/distributed/org/apache/cassandra/distributed/test/jmx/JMXFeatureTest.java @@ -36,7 +36,7 @@ import org.apache.cassandra.distributed.shared.ClusterUtils; import org.apache.cassandra.distributed.shared.JMXUtil; import org.apache.cassandra.distributed.test.TestBaseImpl; -import static org.apache.cassandra.distributed.test.jmx.JMXGetterCheckTest.testAllValidGetters; +import static org.apache.cassandra.distributed.test.jmx.JMXTestsUtil.testAllValidGetters; import static org.hamcrest.Matchers.blankOrNullString; import static org.hamcrest.Matchers.containsString; import static org.hamcrest.Matchers.is; @@ -85,7 +85,7 @@ public class JMXFeatureTest extends TestBaseImpl allInstances.addAll(instancesContacted); // Make sure we actually exercise the mbeans by testing a bunch of getters. // Without this it's possible for the test to pass as we don't touch any mBeans that we register. - testAllValidGetters(cluster); + testAllValidGetters(cluster, null); } } Assert.assertEquals("Each instance from each cluster should have been unique", iterations * 2, allInstances.size()); @@ -122,7 +122,7 @@ public class JMXFeatureTest extends TestBaseImpl Assert.assertThat(statusResult.getStderr(), is(blankOrNullString())); Assert.assertThat(statusResult.getStdout(), containsString("UN 127.0.0.1")); testInstance(instances, cluster.get(1)); - testAllValidGetters(cluster); + testAllValidGetters(cluster, null); } } diff --git a/test/distributed/org/apache/cassandra/distributed/test/jmx/JMXGetterCheckTest.java b/test/distributed/org/apache/cassandra/distributed/test/jmx/JMXGetterCheckTest.java index ea95003093..98691b9442 100644 --- a/test/distributed/org/apache/cassandra/distributed/test/jmx/JMXGetterCheckTest.java +++ b/test/distributed/org/apache/cassandra/distributed/test/jmx/JMXGetterCheckTest.java @@ -17,53 +17,14 @@ */ package org.apache.cassandra.distributed.test.jmx; -import java.util.ArrayList; -import java.util.Collections; -import java.util.List; -import java.util.Set; -import java.util.TreeSet; -import javax.management.JMRuntimeException; -import javax.management.MBeanAttributeInfo; -import javax.management.MBeanInfo; -import javax.management.MBeanOperationInfo; -import javax.management.MBeanServerConnection; -import javax.management.ObjectName; -import javax.management.remote.JMXConnector; - -import com.google.common.collect.ImmutableSet; import org.junit.Test; import org.apache.cassandra.distributed.Cluster; import org.apache.cassandra.distributed.api.Feature; -import org.apache.cassandra.distributed.api.IInstanceConfig; -import org.apache.cassandra.distributed.api.IInvokableInstance; -import org.apache.cassandra.distributed.shared.JMXUtil; import org.apache.cassandra.distributed.test.TestBaseImpl; public class JMXGetterCheckTest extends TestBaseImpl { - private static final Set IGNORE_ATTRIBUTES = ImmutableSet.of( - "org.apache.cassandra.net:type=MessagingService:BackPressurePerHost", // throws unsupported saying the feature was removed... dropped in CASSANDRA-15375 - "org.apache.cassandra.db:type=DynamicEndpointSnitch:Scores", // when running in multiple-port-one-IP mode, this fails - "org.apache.cassandra.db:type=StorageService:OutstandingSchemaVersions", // deprecated (TCM) - "org.apache.cassandra.db:type=StorageService:OutstandingSchemaVersionsWithPort" // deprecated (TCM) - ); - private static final Set IGNORE_OPERATIONS = ImmutableSet.of( - "org.apache.cassandra.db:type=StorageService:stopDaemon", // halts the instance, which then causes the JVM to exit - "org.apache.cassandra.db:type=StorageService:drain", // don't drain, it stops things which can cause other APIs to be unstable as we are in a stopped state - "org.apache.cassandra.db:type=StorageService:stopGossiping", // if we stop gossip this can cause other issues, so avoid - "org.apache.cassandra.db:type=StorageService:resetLocalSchema", // this will fail when there are no other nodes which can serve schema - "org.apache.cassandra.db:type=StorageService:joinRing", // Causes bootstrapping errors - "org.apache.cassandra.db:type=Tables,keyspace=system,table=local:loadNewSSTables", // Shouldn't attempt to load SSTables as sometimes the temp directories don't work - "org.apache.cassandra.db:type=CIDRGroupsMappingManager:loadCidrGroupsCache", // CIDR cache isn't enabled by default - "org.apache.cassandra.db:type=StorageService:clearConnectionHistory", // Throws a NullPointerException - "org.apache.cassandra.db:type=StorageService:startGossiping", // causes multiple loops to fail - "org.apache.cassandra.db:type=StorageService:startNativeTransport", // causes multiple loops to fail - "org.apache.cassandra.db:type=StorageService:resumeMove", // throws since there is no move in progress - "org.apache.cassandra.db:type=StorageService:abortMove", // throws since there is no move in progress - "org.apache.cassandra.db:type=CIDRGroupsMappingManager:loadCidrGroupsCache", // AllowAllCIDRAuthorizer doesn't support this operation, as feature is disabled by default - "org.apache.cassandra.db:type=StorageService:forceRemoveCompletion" // deprecated (TCM) - ); @Test public void testGetters() throws Exception @@ -72,95 +33,8 @@ public class JMXGetterCheckTest extends TestBaseImpl { try (Cluster cluster = Cluster.build(1).withConfig(c -> c.with(Feature.values())).start()) { - testAllValidGetters(cluster); + JMXTestsUtil.testAllValidGetters(cluster, null); } } } - - /** - * Tests JMX getters and operations. - * Useful for more than just testing getters, it also is used in JMXFeatureTest - * to make sure we've touched the complete JMX code path. - * @param cluster the cluster to test - * @throws Exception several kinds of exceptions can be thrown, mostly from JMX infrastructure issues. - */ - public static void testAllValidGetters(Cluster cluster) throws Exception - { - for (IInvokableInstance instance: cluster) - { - if (instance.isShutdown()) - { - continue; - } - IInstanceConfig config = instance.config(); - List errors = new ArrayList<>(); - try (JMXConnector jmxc = JMXUtil.getJmxConnector(config)) - { - MBeanServerConnection mbsc = jmxc.getMBeanServerConnection(); - Set metricNames = new TreeSet<>(mbsc.queryNames(null, null)); - for (ObjectName name : metricNames) - { - if (!name.getDomain().startsWith("org.apache.cassandra")) - continue; - MBeanInfo info = mbsc.getMBeanInfo(name); - for (MBeanAttributeInfo a : info.getAttributes()) - { - String fqn = String.format("%s:%s", name, a.getName()); - if (!a.isReadable() || IGNORE_ATTRIBUTES.contains(fqn)) - continue; - try - { - mbsc.getAttribute(name, a.getName()); - } - catch (JMRuntimeException e) - { - errors.add(new Named(String.format("Attribute %s", fqn), e.getCause())); - } - } - - for (MBeanOperationInfo o : info.getOperations()) - { - String fqn = String.format("%s:%s", name, o.getName()); - if (o.getSignature().length != 0 || IGNORE_OPERATIONS.contains(fqn)) - continue; - try - { - mbsc.invoke(name, o.getName(), new Object[0], new String[0]); - } - catch (JMRuntimeException e) - { - errors.add(new Named(String.format("Operation %s", fqn), e.getCause())); - } - } - } - } - if (!errors.isEmpty()) - { - AssertionError root = new AssertionError(); - errors.forEach(root::addSuppressed); - throw root; - } - } - } - - /** - * This class is meant to make new errors easier to read, by adding the JMX endpoint, and cleaning up the unneeded JMX/Reflection logic cluttering the stacktrace - */ - private static class Named extends RuntimeException - { - public Named(String msg, Throwable cause) - { - super(msg + "\nCaused by: " + cause.getClass().getCanonicalName() + ": " + cause.getMessage(), cause.getCause()); - StackTraceElement[] stack = cause.getStackTrace(); - List copy = new ArrayList<>(); - for (StackTraceElement s : stack) - { - if (!s.getClassName().startsWith("org.apache.cassandra")) - break; - copy.add(s); - } - Collections.reverse(copy); - setStackTrace(copy.toArray(new StackTraceElement[0])); - } - } } diff --git a/test/distributed/org/apache/cassandra/distributed/test/jmx/JMXSslConfigDistributedTest.java b/test/distributed/org/apache/cassandra/distributed/test/jmx/JMXSslConfigDistributedTest.java new file mode 100644 index 0000000000..146f548eaf --- /dev/null +++ b/test/distributed/org/apache/cassandra/distributed/test/jmx/JMXSslConfigDistributedTest.java @@ -0,0 +1,177 @@ +/* + * 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.distributed.test.jmx; + +import java.util.Arrays; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import javax.management.remote.rmi.RMIConnectorServer; +import javax.net.ssl.SSLException; +import javax.rmi.ssl.SslRMIClientSocketFactory; + +import com.google.common.collect.ImmutableMap; +import org.junit.Test; + +import org.apache.cassandra.distributed.Cluster; +import org.apache.cassandra.distributed.api.Feature; +import org.apache.cassandra.distributed.impl.JmxTestClientSslContextFactory; +import org.apache.cassandra.distributed.impl.JmxTestClientSslSocketFactory; +import org.apache.cassandra.distributed.shared.WithProperties; +import org.apache.cassandra.distributed.test.AbstractEncryptionOptionsImpl; +import org.apache.cassandra.utils.jmx.JMXSslPropertiesUtil; + +import static org.apache.cassandra.config.CassandraRelevantProperties.COM_SUN_MANAGEMENT_JMXREMOTE_SSL_ENABLED_CIPHER_SUITES; + +/** + * Distributed tests for JMX SSL configuration via the system properties OR the encryption options in the cassandra.yaml. + */ +public class JMXSslConfigDistributedTest extends AbstractEncryptionOptionsImpl +{ + @Test + public void testDefaultEncryptionOptions() throws Throwable + { + // We must set the keystore in the system variable to make sure that the call to SSLContext.getDefault() + // uses it when Client SSL Socketfactory is initialized even if we don't need it here. + // The same default SSLContext.getDefault() will be used by other methods like testSystemSettings() in this test + // for the Server SSL Socketfactory and at that time we will need the keystore to be available + // All of the above is the issue because we run everything (JMX Server, Client) in the same JVM, multiple times + // and the SSLContext.getDefault() relies on static initialization that is reused + try (WithProperties withProperties = JMXSslPropertiesUtil.preserveAllProperties()) + { + setKeystoreProperties(withProperties); + ImmutableMap encryptionOptionsMap = ImmutableMap.builder().putAll(validFileBasedKeystores) + .put("enabled", true) + .put("accepted_protocols", Arrays.asList("TLSv1.2", "TLSv1.3", "TLSv1.1")) + .build(); + + try (Cluster cluster = builder().withNodes(1).withConfig(c -> { + c.with(Feature.JMX).set("jmx_encryption_options", encryptionOptionsMap); + }).start()) + { + Map jmxEnv = new HashMap<>(); + configureClientSocketFactory(jmxEnv, encryptionOptionsMap); + JMXTestsUtil.testAllValidGetters(cluster, jmxEnv); + } + } + } + + @Test + public void testClientAuth() throws Throwable + { + try (WithProperties withProperties = JMXSslPropertiesUtil.preserveAllProperties()) + { + setKeystoreProperties(withProperties); + ImmutableMap encryptionOptionsMap = ImmutableMap.builder().putAll(validFileBasedKeystores) + .put("enabled", true) + .put("require_client_auth", true) + .put("accepted_protocols", Arrays.asList("TLSv1.2", "TLSv1.3", "TLSv1.1")) + .build(); + + try (Cluster cluster = builder().withNodes(1).withConfig(c -> { + c.with(Feature.JMX).set("jmx_encryption_options", encryptionOptionsMap); + }).start()) + { + Map jmxEnv = new HashMap<>(); + configureClientSocketFactory(jmxEnv, encryptionOptionsMap); + JMXTestsUtil.testAllValidGetters(cluster, jmxEnv); + } + } + } + + @Test + public void testSystemSettings() throws Throwable + { + COM_SUN_MANAGEMENT_JMXREMOTE_SSL_ENABLED_CIPHER_SUITES.reset(); + try (WithProperties withProperties = JMXSslPropertiesUtil.use(true, false, + "TLSv1.2,TLSv1.3,TLSv1.1")) + { + setKeystoreProperties(withProperties); + try (Cluster cluster = builder().withNodes(1).withConfig(c -> { + c.with(Feature.JMX); + }).start()) + { + Map jmxEnv = new HashMap<>(); + SslRMIClientSocketFactory clientFactory = new SslRMIClientSocketFactory(); + jmxEnv.put(RMIConnectorServer.RMI_CLIENT_SOCKET_FACTORY_ATTRIBUTE, clientFactory); + jmxEnv.put("com.sun.jndi.rmi.factory.socket", clientFactory); + JMXTestsUtil.testAllValidGetters(cluster, jmxEnv); + } + } + } + + @Test + public void testInvalidKeystorePath() throws Throwable + { + try (Cluster cluster = builder().withNodes(1).withConfig(c -> { + c.with(Feature.JMX).set("jmx_encryption_options", + ImmutableMap.builder() + .put("enabled", true) + .put("keystore", "/path/to/bad/keystore/that/should/not/exist") + .put("keystore_password", "cassandra") + .put("accepted_protocols", Arrays.asList("TLSv1.2", "TLSv1.3", "TLSv1.1")) + .build()); + }).createWithoutStarting()) + { + assertCannotStartDueToConfigurationException(cluster); + } + } + + /** + * Tests {@code disabled} jmx_encryption_options. Here even if the configured {@code keystore} is invalid, it will + * not matter and the JMX server/client should start. + */ + @Test + public void testDisabledEncryptionOptions() throws Throwable + { + try (Cluster cluster = builder().withNodes(1).withConfig(c -> { + c.with(Feature.JMX).set("jmx_encryption_options", + ImmutableMap.builder() + .put("enabled", false) + .put("keystore", "/path/to/bad/keystore/that/should/not/exist") + .put("keystore_password", "cassandra") + .build()); + }).start()) + { + JMXTestsUtil.testAllValidGetters(cluster, null); + } + } + + private void setKeystoreProperties(WithProperties properties) + { + properties.with("javax.net.ssl.trustStore", (String) validFileBasedKeystores.get("truststore"), + "javax.net.ssl.trustStorePassword", (String) validFileBasedKeystores.get("truststore_password"), + "javax.net.ssl.keyStore", (String) validFileBasedKeystores.get("keystore"), + "javax.net.ssl.keyStorePassword", (String) validFileBasedKeystores.get("keystore_password")); + } + + @SuppressWarnings("unchecked") + private void configureClientSocketFactory(Map jmxEnv, Map encryptionOptionsMap) throws SSLException + { + JmxTestClientSslContextFactory clientSslContextFactory = new JmxTestClientSslContextFactory(encryptionOptionsMap); + List cipherSuitesList = (List) encryptionOptionsMap.get("cipher_suites"); + String[] cipherSuites = cipherSuitesList == null ? null : cipherSuitesList.toArray(new String[0]); + List acceptedProtocolList = (List) encryptionOptionsMap.get("accepted_protocols"); + String[] acceptedProtocols = acceptedProtocolList == null ? null : acceptedProtocolList.toArray(new String[0]); + JmxTestClientSslSocketFactory clientFactory = new JmxTestClientSslSocketFactory(clientSslContextFactory.createSSLContext(), + cipherSuites, acceptedProtocols); + jmxEnv.put(RMIConnectorServer.RMI_CLIENT_SOCKET_FACTORY_ATTRIBUTE, clientFactory); + jmxEnv.put("com.sun.jndi.rmi.factory.socket", clientFactory); + } +} diff --git a/test/distributed/org/apache/cassandra/distributed/test/jmx/JMXTestsUtil.java b/test/distributed/org/apache/cassandra/distributed/test/jmx/JMXTestsUtil.java new file mode 100644 index 0000000000..5726c242d9 --- /dev/null +++ b/test/distributed/org/apache/cassandra/distributed/test/jmx/JMXTestsUtil.java @@ -0,0 +1,160 @@ +/* + * 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.distributed.test.jmx; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.TreeSet; +import javax.management.JMRuntimeException; +import javax.management.MBeanAttributeInfo; +import javax.management.MBeanInfo; +import javax.management.MBeanOperationInfo; +import javax.management.MBeanServerConnection; +import javax.management.ObjectName; +import javax.management.remote.JMXConnector; + +import com.google.common.collect.ImmutableSet; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import org.apache.cassandra.distributed.Cluster; +import org.apache.cassandra.distributed.api.IInstanceConfig; +import org.apache.cassandra.distributed.api.IInvokableInstance; +import org.apache.cassandra.distributed.shared.JMXUtil; + +public class JMXTestsUtil +{ + private static final Logger logger = LoggerFactory.getLogger(JMXTestsUtil.class); + private static final Set IGNORE_ATTRIBUTES = ImmutableSet.of( + "org.apache.cassandra.net:type=MessagingService:BackPressurePerHost", // throws unsupported saying the feature was removed... dropped in CASSANDRA-15375 + "org.apache.cassandra.db:type=DynamicEndpointSnitch:Scores", // when running in multiple-port-one-IP mode, this fails + "org.apache.cassandra.db:type=StorageService:OutstandingSchemaVersions", // deprecated (TCM) + "org.apache.cassandra.db:type=StorageService:OutstandingSchemaVersionsWithPort" // deprecated (TCM) + ); + private static final Set IGNORE_OPERATIONS = ImmutableSet.of( + "org.apache.cassandra.db:type=StorageService:stopDaemon", // halts the instance, which then causes the JVM to exit + "org.apache.cassandra.db:type=StorageService:drain", // don't drain, it stops things which can cause other APIs to be unstable as we are in a stopped state + "org.apache.cassandra.db:type=StorageService:stopGossiping", // if we stop gossip this can cause other issues, so avoid + "org.apache.cassandra.db:type=StorageService:resetLocalSchema", // this will fail when there are no other nodes which can serve schema + "org.apache.cassandra.db:type=StorageService:joinRing", // Causes bootstrapping errors + "org.apache.cassandra.db:type=Tables,keyspace=system,table=local:loadNewSSTables", // Shouldn't attempt to load SSTables as sometimes the temp directories don't work + "org.apache.cassandra.db:type=CIDRGroupsMappingManager:loadCidrGroupsCache", // CIDR cache isn't enabled by default + "org.apache.cassandra.db:type=StorageService:clearConnectionHistory", // Throws a NullPointerException + "org.apache.cassandra.db:type=StorageService:startGossiping", // causes multiple loops to fail + "org.apache.cassandra.db:type=StorageService:startNativeTransport", // causes multiple loops to fail + "org.apache.cassandra.db:type=StorageService:resumeMove", // throws since there is no move in progress + "org.apache.cassandra.db:type=StorageService:abortMove", // throws since there is no move in progress + "org.apache.cassandra.db:type=CIDRGroupsMappingManager:loadCidrGroupsCache", // AllowAllCIDRAuthorizer doesn't support this operation, as feature is disabled by default + "org.apache.cassandra.db:type=StorageService:forceRemoveCompletion" // deprecated (TCM) + ); + + /** + * Tests JMX getters and operations and allows passing JMX Env used for the client JMX connection. + * + * @param cluster the cluster to test + * @throws Exception several kinds of exceptions can be thrown, mostly from JMX infrastructure issues. + */ + public static void testAllValidGetters(Cluster cluster, Map jmxEnv) throws Exception + { + for (IInvokableInstance instance : cluster) + { + if (instance.isShutdown()) + { + continue; + } + IInstanceConfig config = instance.config(); + List errors = new ArrayList<>(); + try (JMXConnector jmxc = JMXUtil.getJmxConnector(config, jmxEnv)) + { + MBeanServerConnection mbsc = jmxc.getMBeanServerConnection(); + Set metricNames = new TreeSet<>(mbsc.queryNames(null, null)); + for (ObjectName name : metricNames) + { + if (!name.getDomain().startsWith("org.apache.cassandra")) + continue; + MBeanInfo info = mbsc.getMBeanInfo(name); + for (MBeanAttributeInfo a : info.getAttributes()) + { + String fqn = String.format("%s:%s", name, a.getName()); + if (!a.isReadable() || IGNORE_ATTRIBUTES.contains(fqn)) + continue; + try + { + mbsc.getAttribute(name, a.getName()); + } + catch (JMRuntimeException e) + { + errors.add(new Named(String.format("Attribute %s", fqn), e.getCause())); + } + } + + for (MBeanOperationInfo o : info.getOperations()) + { + String fqn = String.format("%s:%s", name, o.getName()); + if (o.getSignature().length != 0 || IGNORE_OPERATIONS.contains(fqn)) + continue; + try + { + mbsc.invoke(name, o.getName(), new Object[0], new String[0]); + } + catch (JMRuntimeException e) + { + errors.add(new Named(String.format("Operation %s", fqn), e.getCause())); + } + } + } + } + if (!errors.isEmpty()) + { + AssertionError root = new AssertionError(); + for (Named error : errors) + { + // The Named object's message has the cause also so this only logs the message + logger.error("Error {}", error.getMessage()); + root.addSuppressed(error); + } + throw root; + } + } + } + + /** + * This class is meant to make new errors easier to read, by adding the JMX endpoint, and cleaning up the unneeded JMX/Reflection logic cluttering the stacktrace + */ + static class Named extends RuntimeException + { + public Named(String msg, Throwable cause) + { + super(msg + "\nCaused by: " + cause.getClass().getCanonicalName() + ": " + cause.getMessage(), cause.getCause()); + StackTraceElement[] stack = cause.getStackTrace(); + List copy = new ArrayList<>(); + for (StackTraceElement s : stack) + { + if (!s.getClassName().startsWith("org.apache.cassandra")) + break; + copy.add(s); + } + Collections.reverse(copy); + setStackTrace(copy.toArray(new StackTraceElement[0])); + } + } +} diff --git a/test/unit/org/apache/cassandra/config/EncryptionOptionsTest.java b/test/unit/org/apache/cassandra/config/EncryptionOptionsTest.java index cbfea947fe..a295095dbb 100644 --- a/test/unit/org/apache/cassandra/config/EncryptionOptionsTest.java +++ b/test/unit/org/apache/cassandra/config/EncryptionOptionsTest.java @@ -250,7 +250,7 @@ public class EncryptionOptionsTest for(EncryptionOptions.ConfigKey configKey: EncryptionOptions.ConfigKey.values()) { - customSslContextFactoryParams.put(configKey.getKeyName(), "my-custom-value"); + customSslContextFactoryParams.put(configKey.toString(), "my-custom-value"); } EncryptionOptionsTestCase.of(null, absentKeystore, true, customSslContextFactoryParams, ENCRYPTED); diff --git a/test/unit/org/apache/cassandra/config/PropertiesTest.java b/test/unit/org/apache/cassandra/config/PropertiesTest.java index 6fec2dfe68..075dcf1f0d 100644 --- a/test/unit/org/apache/cassandra/config/PropertiesTest.java +++ b/test/unit/org/apache/cassandra/config/PropertiesTest.java @@ -40,8 +40,10 @@ public class PropertiesTest Config config = new Config(); Set keys = ImmutableSet.of("server_encryption_options.enabled", "client_encryption_options.enabled", + "jmx_encryption_options.enabled", "server_encryption_options.optional", - "client_encryption_options.optional"); + "client_encryption_options.optional", + "jmx_encryption_options.optional"); for (Property prop : ps.values()) { // skip these properties as they don't allow get/set within the context of this test diff --git a/test/unit/org/apache/cassandra/db/virtual/SettingsTableTest.java b/test/unit/org/apache/cassandra/db/virtual/SettingsTableTest.java index 6397afff95..1645e3150e 100644 --- a/test/unit/org/apache/cassandra/db/virtual/SettingsTableTest.java +++ b/test/unit/org/apache/cassandra/db/virtual/SettingsTableTest.java @@ -52,6 +52,7 @@ public class SettingsTableTest extends CQLTester config = new Config(); config.client_encryption_options.applyConfig(); config.server_encryption_options.applyConfig(); + config.jmx_encryption_options.applyConfig(); config.sstable_preemptive_open_interval = null; config.index_summary_resize_interval = null; config.cache_load_timeout = new DurationSpec.IntSecondsBound(0); diff --git a/test/unit/org/apache/cassandra/utils/jmx/JMXSslConfigTest.java b/test/unit/org/apache/cassandra/utils/jmx/JMXSslConfigTest.java new file mode 100644 index 0000000000..2e739c558b --- /dev/null +++ b/test/unit/org/apache/cassandra/utils/jmx/JMXSslConfigTest.java @@ -0,0 +1,102 @@ +/* + * 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.utils.jmx; + +import java.net.InetAddress; +import java.util.Map; +import javax.management.remote.rmi.RMIConnectorServer; +import javax.net.ssl.SSLException; +import javax.rmi.ssl.SslRMIServerSocketFactory; + +import org.junit.AfterClass; +import org.junit.Assert; +import org.junit.BeforeClass; +import org.junit.Test; + +import org.apache.cassandra.config.DatabaseDescriptor; +import org.apache.cassandra.distributed.shared.WithProperties; +import org.apache.cassandra.utils.JMXServerUtils; + +import static org.apache.cassandra.config.CassandraRelevantProperties.CASSANDRA_CONFIG; +import static org.apache.cassandra.config.CassandraRelevantProperties.JAVAX_RMI_SSL_CLIENT_ENABLED_CIPHER_SUITES; +import static org.apache.cassandra.config.CassandraRelevantProperties.JAVAX_RMI_SSL_CLIENT_ENABLED_PROTOCOLS; + +/** + * Tests for Local JMX server, the remote JMX SSL configuration via System properties in absence of jmx_encryption_options + * in the cassandra.yaml. This is the behavior before CASSANDRA-18508. + */ +public class JMXSslConfigTest +{ + static WithProperties properties; + + @BeforeClass + public static void setupDatabaseDescriptor() + { + properties = new WithProperties().set(CASSANDRA_CONFIG, "cassandra.yaml"); + DatabaseDescriptor.daemonInitialization(); + } + + @AfterClass + public static void tearDownDatabaseDescriptor() + { + properties.close(); + } + + /** + * Tests for remote JMX SSL configuration specified via the System properties. + */ + @Test + public void testRemoteJmxSystemConfig() throws SSLException + { + InetAddress serverAddress = InetAddress.getLoopbackAddress(); + String enabledProtocols = "TLSv1.2,TLSv1.3,TLSv1.1"; + String cipherSuites = "TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384,TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256"; + + try (WithProperties ignored = JMXSslPropertiesUtil.use(true, true, enabledProtocols, + cipherSuites)) + { + Map env = JMXServerUtils.configureJmxSocketFactories(serverAddress, false); + Assert.assertNotNull("ServerSocketFactory must not be null", env.get(RMIConnectorServer.RMI_SERVER_SOCKET_FACTORY_ATTRIBUTE)); + Assert.assertTrue("RMI_SERVER_SOCKET_FACTORY must be of SslRMIServerSocketFactory type", env.get(RMIConnectorServer.RMI_SERVER_SOCKET_FACTORY_ATTRIBUTE) instanceof SslRMIServerSocketFactory); + Assert.assertNotNull("ClientSocketFactory must not be null", env.get(RMIConnectorServer.RMI_CLIENT_SOCKET_FACTORY_ATTRIBUTE)); + Assert.assertNotNull("com.sun.jndi.rmi.factory.socket must be set in the env", env.get("com.sun.jndi.rmi.factory.socket")); + Assert.assertEquals("protocols must match", enabledProtocols, JAVAX_RMI_SSL_CLIENT_ENABLED_PROTOCOLS.getString()); + Assert.assertEquals("cipher-suites must match", cipherSuites, JAVAX_RMI_SSL_CLIENT_ENABLED_CIPHER_SUITES.getString()); + } + } + + /** + * Tests for {@code localOnly} JMX Server configuration. + */ + @Test + public void testLocalJmxServer() throws SSLException + { + InetAddress serverAddress = InetAddress.getLoopbackAddress(); + try (WithProperties ignored = JMXSslPropertiesUtil.use(false)) + { + Map env = JMXServerUtils.configureJmxSocketFactories(serverAddress, true); + + Assert.assertNull("ClientSocketFactory must be null", env.get(RMIConnectorServer.RMI_CLIENT_SOCKET_FACTORY_ATTRIBUTE)); + Assert.assertNull("com.sun.jndi.rmi.factory.socket must not be set in the env", env.get("com.sun.jndi.rmi.factory.socket")); + Assert.assertNotNull("ServerSocketFactory must not be null", env.get(RMIConnectorServer.RMI_SERVER_SOCKET_FACTORY_ATTRIBUTE)); + Assert.assertNull("protocols must be empty", JAVAX_RMI_SSL_CLIENT_ENABLED_PROTOCOLS.getString()); + Assert.assertNull("cipher-suites must empty", JAVAX_RMI_SSL_CLIENT_ENABLED_CIPHER_SUITES.getString()); + } + } +} diff --git a/test/unit/org/apache/cassandra/utils/jmx/JMXSslConfiguredWithYamlFileOptionsTest.java b/test/unit/org/apache/cassandra/utils/jmx/JMXSslConfiguredWithYamlFileOptionsTest.java new file mode 100644 index 0000000000..3855183a92 --- /dev/null +++ b/test/unit/org/apache/cassandra/utils/jmx/JMXSslConfiguredWithYamlFileOptionsTest.java @@ -0,0 +1,104 @@ +/* + * 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.utils.jmx; + +import java.net.InetAddress; +import java.util.Map; +import javax.management.remote.rmi.RMIConnectorServer; +import javax.net.ssl.SSLException; +import javax.rmi.ssl.SslRMIServerSocketFactory; + +import org.apache.commons.lang3.StringUtils; +import org.junit.AfterClass; +import org.junit.Assert; +import org.junit.BeforeClass; +import org.junit.Test; + +import org.apache.cassandra.config.DatabaseDescriptor; +import org.apache.cassandra.config.EncryptionOptions; +import org.apache.cassandra.distributed.shared.WithProperties; +import org.apache.cassandra.exceptions.ConfigurationException; +import org.apache.cassandra.utils.JMXServerUtils; + +import static org.apache.cassandra.config.CassandraRelevantProperties.CASSANDRA_CONFIG; +import static org.apache.cassandra.config.CassandraRelevantProperties.COM_SUN_MANAGEMENT_JMXREMOTE_SSL; +import static org.apache.cassandra.config.CassandraRelevantProperties.JAVAX_RMI_SSL_CLIENT_ENABLED_CIPHER_SUITES; +import static org.apache.cassandra.config.CassandraRelevantProperties.JAVAX_RMI_SSL_CLIENT_ENABLED_PROTOCOLS; + +/** + * Tests for Default JMX SSL configuration specified in the cassandra.yaml using jmx_encryption_options. + * The default configurtion means keystore/truststore configured with file paths and using {@code DefaultSslContextFactory} + * to initialize the SSLContext. + */ +public class JMXSslConfiguredWithYamlFileOptionsTest +{ + static WithProperties properties; + + @BeforeClass + public static void setupDatabaseDescriptor() + { + properties = new WithProperties().set(CASSANDRA_CONFIG, "cassandra-jmx-sslconfig.yaml"); + DatabaseDescriptor.daemonInitialization(); + } + + @AfterClass + public static void tearDownDatabaseDescriptor() + { + properties.close(); + } + + @Test + public void testYamlFileJmxEncryptionOptions() throws SSLException + { + EncryptionOptions jmxEncryptionOptions = DatabaseDescriptor.getJmxEncryptionOptions(); + String expectedProtocols = StringUtils.join(jmxEncryptionOptions.getAcceptedProtocols(), ","); + String expectedCipherSuites = StringUtils.join(jmxEncryptionOptions.cipherSuitesArray(), ","); + + InetAddress serverAddress = InetAddress.getLoopbackAddress(); + + try (WithProperties ignored = JMXSslPropertiesUtil.use(false)) + { + Map env = JMXServerUtils.configureJmxSocketFactories(serverAddress, false); + Assert.assertTrue("com.sun.management.jmxremote.ssl must be true", COM_SUN_MANAGEMENT_JMXREMOTE_SSL.getBoolean()); + Assert.assertNotNull("ServerSocketFactory must not be null", env.get(RMIConnectorServer.RMI_SERVER_SOCKET_FACTORY_ATTRIBUTE)); + Assert.assertTrue("RMI_SERVER_SOCKET_FACTORY must be of JMXSslRMIServerSocketFactory type", env.get(RMIConnectorServer.RMI_SERVER_SOCKET_FACTORY_ATTRIBUTE) instanceof SslRMIServerSocketFactory); + Assert.assertNotNull("ClientSocketFactory must not be null", env.get(RMIConnectorServer.RMI_CLIENT_SOCKET_FACTORY_ATTRIBUTE)); + Assert.assertNotNull("com.sun.jndi.rmi.factory.socket must be set in the env", env.get("com.sun.jndi.rmi.factory.socket")); + Assert.assertEquals("javax.rmi.ssl.client.enabledProtocols must match", expectedProtocols, JAVAX_RMI_SSL_CLIENT_ENABLED_PROTOCOLS.getString()); + Assert.assertEquals("javax.rmi.ssl.client.enabledCipherSuites must match", expectedCipherSuites, JAVAX_RMI_SSL_CLIENT_ENABLED_CIPHER_SUITES.getString()); + } + } + + /** + * Tests for the error scenario when the JMX SSL configuration is provided as + * system configuration as well as encryption_options. + * + * @throws SSLException + */ + @Test(expected = ConfigurationException.class) + public void testDuplicateConfig() throws SSLException + { + InetAddress serverAddress = InetAddress.getLoopbackAddress(); + + try (WithProperties ignored = JMXSslPropertiesUtil.use(true)) + { + JMXServerUtils.configureJmxSocketFactories(serverAddress, false); + } + } +} diff --git a/test/unit/org/apache/cassandra/utils/jmx/JMXSslDisabledEncryptionOptionsTest.java b/test/unit/org/apache/cassandra/utils/jmx/JMXSslDisabledEncryptionOptionsTest.java new file mode 100644 index 0000000000..ef2070f2aa --- /dev/null +++ b/test/unit/org/apache/cassandra/utils/jmx/JMXSslDisabledEncryptionOptionsTest.java @@ -0,0 +1,102 @@ +/* + * 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.utils.jmx; + +import java.net.InetAddress; +import java.util.Map; +import javax.management.remote.rmi.RMIConnectorServer; +import javax.net.ssl.SSLException; + +import org.junit.AfterClass; +import org.junit.Assert; +import org.junit.BeforeClass; +import org.junit.Test; + +import org.apache.cassandra.config.DatabaseDescriptor; +import org.apache.cassandra.distributed.shared.WithProperties; +import org.apache.cassandra.utils.JMXServerUtils; + +import static org.apache.cassandra.config.CassandraRelevantProperties.CASSANDRA_CONFIG; +import static org.apache.cassandra.config.CassandraRelevantProperties.COM_SUN_MANAGEMENT_JMXREMOTE_SSL; +import static org.apache.cassandra.config.CassandraRelevantProperties.JAVAX_RMI_SSL_CLIENT_ENABLED_CIPHER_SUITES; +import static org.apache.cassandra.config.CassandraRelevantProperties.JAVAX_RMI_SSL_CLIENT_ENABLED_PROTOCOLS; + +/** + * Tests for disabling jmx_encryption_options in the cassandra.yaml. + */ +public class JMXSslDisabledEncryptionOptionsTest +{ + static WithProperties properties; + + @BeforeClass + public static void setupDatabaseDescriptor() + { + properties = new WithProperties().set(CASSANDRA_CONFIG, "cassandra-jmx-disabled-sslconfig.yaml"); + DatabaseDescriptor.daemonInitialization(); + } + + @AfterClass + public static void tearDownDatabaseDescriptor() + { + properties.close(); + } + + /** + * Tests disabled JMX SSL configuration. This means, all below are either not configured OR configured with + * disabled mode. + * 1. local only JMX server + * 2. System properties set for remote JMX SSL + * 3. jmx_encryption_options in the cassandra.yaml + */ + @Test + public void testDisabledJmxSslConfigs() throws SSLException + { + InetAddress serverAddress = InetAddress.getLoopbackAddress(); + + try (WithProperties ignored = JMXSslPropertiesUtil.use(false)) + { + Map env = JMXServerUtils.configureJmxSocketFactories(serverAddress, false); + Assert.assertTrue("no properties must be set", env.isEmpty()); + Assert.assertFalse("com.sun.management.jmxremote.ssl must be false", COM_SUN_MANAGEMENT_JMXREMOTE_SSL.getBoolean()); + Assert.assertNull("javax.rmi.ssl.client.enabledProtocols must be null", JAVAX_RMI_SSL_CLIENT_ENABLED_PROTOCOLS.getString()); + Assert.assertNull("javax.rmi.ssl.client.enabledCipherSuites must be null", JAVAX_RMI_SSL_CLIENT_ENABLED_CIPHER_SUITES.getString()); + } + } + + /** + * Tests fallback to the `local only` JMX server when jmx_encryption_options are disabled in the cassandra.yaml + * and no System settings provided for the remote SSL config. + */ + @Test + public void testFallbackToLocalJmxServer() throws SSLException + { + InetAddress serverAddress = InetAddress.getLoopbackAddress(); + + try (WithProperties ignored = JMXSslPropertiesUtil.use(false)) + { + Map env = JMXServerUtils.configureJmxSocketFactories(serverAddress, true); + Assert.assertFalse("com.sun.management.jmxremote.ssl must be false", COM_SUN_MANAGEMENT_JMXREMOTE_SSL.getBoolean()); + Assert.assertNull("com.sun.jndi.rmi.factory.socket must be null", env.get("com.sun.jndi.rmi.factory.socket")); + Assert.assertNotNull("ServerSocketFactory must not be null", env.get(RMIConnectorServer.RMI_SERVER_SOCKET_FACTORY_ATTRIBUTE)); + Assert.assertFalse("com.sun.management.jmxremote.ssl must be false", COM_SUN_MANAGEMENT_JMXREMOTE_SSL.getBoolean()); + Assert.assertNull("javax.rmi.ssl.client.enabledProtocols must be null", JAVAX_RMI_SSL_CLIENT_ENABLED_PROTOCOLS.getString()); + Assert.assertNull("javax.rmi.ssl.client.enabledCipherSuites must be null", JAVAX_RMI_SSL_CLIENT_ENABLED_CIPHER_SUITES.getString()); + } + } +} diff --git a/test/unit/org/apache/cassandra/utils/jmx/JMXSslPEMConfigTest.java b/test/unit/org/apache/cassandra/utils/jmx/JMXSslPEMConfigTest.java new file mode 100644 index 0000000000..c7db7b2c72 --- /dev/null +++ b/test/unit/org/apache/cassandra/utils/jmx/JMXSslPEMConfigTest.java @@ -0,0 +1,84 @@ +/* + * 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.utils.jmx; + +import java.net.InetAddress; +import java.util.Map; +import javax.management.remote.rmi.RMIConnectorServer; +import javax.net.ssl.SSLException; +import javax.rmi.ssl.SslRMIServerSocketFactory; + +import org.apache.commons.lang3.StringUtils; +import org.junit.AfterClass; +import org.junit.Assert; +import org.junit.BeforeClass; +import org.junit.Test; + +import org.apache.cassandra.config.DatabaseDescriptor; +import org.apache.cassandra.config.EncryptionOptions; +import org.apache.cassandra.distributed.shared.WithProperties; +import org.apache.cassandra.utils.JMXServerUtils; + +import static org.apache.cassandra.config.CassandraRelevantProperties.CASSANDRA_CONFIG; +import static org.apache.cassandra.config.CassandraRelevantProperties.COM_SUN_MANAGEMENT_JMXREMOTE_SSL; +import static org.apache.cassandra.config.CassandraRelevantProperties.JAVAX_RMI_SSL_CLIENT_ENABLED_CIPHER_SUITES; +import static org.apache.cassandra.config.CassandraRelevantProperties.JAVAX_RMI_SSL_CLIENT_ENABLED_PROTOCOLS; + +/** + * Tests for JMX SSL config using keystore/truststore in the PEM format with jmx_encryption_options in the cassandra.yaml. + */ +public class JMXSslPEMConfigTest +{ + static WithProperties properties; + + @BeforeClass + public static void setupDatabaseDescriptor() + { + properties = new WithProperties().set(CASSANDRA_CONFIG, "cassandra-jmx-pem-sslconfig.yaml"); + DatabaseDescriptor.daemonInitialization(); + } + + @AfterClass + public static void tearDownDatabaseDescriptor() + { + properties.close(); + } + + @Test + public void testPEMBasedJmxSslConfig() throws SSLException + { + EncryptionOptions jmxEncryptionOptions = DatabaseDescriptor.getJmxEncryptionOptions(); + String expectedProtocols = StringUtils.join(jmxEncryptionOptions.getAcceptedProtocols(), ","); + String expectedCipherSuites = StringUtils.join(jmxEncryptionOptions.cipherSuitesArray(), ","); + + InetAddress serverAddress = InetAddress.getLoopbackAddress(); + + try (WithProperties ignored = JMXSslPropertiesUtil.use(false)) + { + Map env = JMXServerUtils.configureJmxSocketFactories(serverAddress, false); + Assert.assertTrue("com.sun.management.jmxremote.ssl must be true", COM_SUN_MANAGEMENT_JMXREMOTE_SSL.getBoolean()); + Assert.assertNotNull("ServerSocketFactory must not be null", env.get(RMIConnectorServer.RMI_SERVER_SOCKET_FACTORY_ATTRIBUTE)); + Assert.assertTrue("RMI_SERVER_SOCKET_FACTORY must be of JMXSslRMIServerSocketFactory type", env.get(RMIConnectorServer.RMI_SERVER_SOCKET_FACTORY_ATTRIBUTE) instanceof SslRMIServerSocketFactory); + Assert.assertNotNull("ClientSocketFactory must not be null", env.get(RMIConnectorServer.RMI_CLIENT_SOCKET_FACTORY_ATTRIBUTE)); + Assert.assertNotNull("com.sun.jndi.rmi.factory.socket must be set in the env", env.get("com.sun.jndi.rmi.factory.socket")); + Assert.assertEquals("javax.rmi.ssl.client.enabledProtocols must match", expectedProtocols, JAVAX_RMI_SSL_CLIENT_ENABLED_PROTOCOLS.getString()); + Assert.assertEquals("javax.rmi.ssl.client.enabledCipherSuites must match", expectedCipherSuites, JAVAX_RMI_SSL_CLIENT_ENABLED_CIPHER_SUITES.getString()); + } + } +} diff --git a/test/unit/org/apache/cassandra/utils/jmx/JMXSslPropertiesUtil.java b/test/unit/org/apache/cassandra/utils/jmx/JMXSslPropertiesUtil.java new file mode 100644 index 0000000000..7675ce8fe2 --- /dev/null +++ b/test/unit/org/apache/cassandra/utils/jmx/JMXSslPropertiesUtil.java @@ -0,0 +1,65 @@ +/* + * 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.utils.jmx; + +import org.apache.cassandra.distributed.shared.WithProperties; + +import static org.apache.cassandra.config.CassandraRelevantProperties.COM_SUN_MANAGEMENT_JMXREMOTE_SSL; +import static org.apache.cassandra.config.CassandraRelevantProperties.COM_SUN_MANAGEMENT_JMXREMOTE_SSL_ENABLED_CIPHER_SUITES; +import static org.apache.cassandra.config.CassandraRelevantProperties.COM_SUN_MANAGEMENT_JMXREMOTE_SSL_ENABLED_PROTOCOLS; +import static org.apache.cassandra.config.CassandraRelevantProperties.COM_SUN_MANAGEMENT_JMXREMOTE_SSL_NEED_CLIENT_AUTH; +import static org.apache.cassandra.config.CassandraRelevantProperties.JAVAX_RMI_SSL_CLIENT_ENABLED_CIPHER_SUITES; +import static org.apache.cassandra.config.CassandraRelevantProperties.JAVAX_RMI_SSL_CLIENT_ENABLED_PROTOCOLS; + +public final class JMXSslPropertiesUtil +{ + public static WithProperties use(boolean enableRemoteSsl, boolean needClientAuth, String enabledProtocols, String cipherSuites) + { + return preserveAllProperties() + .set(COM_SUN_MANAGEMENT_JMXREMOTE_SSL, enableRemoteSsl) + .set(COM_SUN_MANAGEMENT_JMXREMOTE_SSL_NEED_CLIENT_AUTH, needClientAuth) + .set(COM_SUN_MANAGEMENT_JMXREMOTE_SSL_ENABLED_PROTOCOLS, enabledProtocols) + .set(COM_SUN_MANAGEMENT_JMXREMOTE_SSL_ENABLED_CIPHER_SUITES, cipherSuites); + } + + public static WithProperties use(boolean enableRemoteSsl, boolean needClientAuth, String enabledProtocols) + { + return preserveAllProperties() + .set(COM_SUN_MANAGEMENT_JMXREMOTE_SSL, enableRemoteSsl) + .set(COM_SUN_MANAGEMENT_JMXREMOTE_SSL_NEED_CLIENT_AUTH, needClientAuth) + .set(COM_SUN_MANAGEMENT_JMXREMOTE_SSL_ENABLED_PROTOCOLS, enabledProtocols); + } + + public static WithProperties use(boolean enableRemoteSsl) + { + return preserveAllProperties() + .set(COM_SUN_MANAGEMENT_JMXREMOTE_SSL, enableRemoteSsl); + } + + public static WithProperties preserveAllProperties() + { + return new WithProperties() + .preserve(COM_SUN_MANAGEMENT_JMXREMOTE_SSL) + .preserve(COM_SUN_MANAGEMENT_JMXREMOTE_SSL_NEED_CLIENT_AUTH) + .preserve(COM_SUN_MANAGEMENT_JMXREMOTE_SSL_ENABLED_PROTOCOLS) + .preserve(COM_SUN_MANAGEMENT_JMXREMOTE_SSL_ENABLED_CIPHER_SUITES) + .preserve(JAVAX_RMI_SSL_CLIENT_ENABLED_PROTOCOLS) + .preserve(JAVAX_RMI_SSL_CLIENT_ENABLED_CIPHER_SUITES); + } +}