diff --git a/conf/cassandra.yaml b/conf/cassandra.yaml index 5c5019962e..978b991449 100644 --- a/conf/cassandra.yaml +++ b/conf/cassandra.yaml @@ -1567,6 +1567,9 @@ client_encryption_options: # # Must be an instance of org.apache.cassandra.security.ISslContextFactory # class_name: org.apache.cassandra.security.DefaultSslContextFactory # Verify client certificates + # - true/REQUIRED, Verifies the client and forces the client to send client certificate + # - false/NOT_REQUIRED, Doesn't verify the client + # - optional, Optionally verifies the client if client certificate is sent, but doesn't force client certificate to send certificates require_client_auth: false # require_endpoint_verification: false # Set trustore and truststore_password if require_client_auth is true diff --git a/doc/modules/cassandra/pages/getting-started/mtlsauthenticators.adoc b/doc/modules/cassandra/pages/getting-started/mtlsauthenticators.adoc index e3cd6ef79f..719f069739 100644 --- a/doc/modules/cassandra/pages/getting-started/mtlsauthenticators.adoc +++ b/doc/modules/cassandra/pages/getting-started/mtlsauthenticators.adoc @@ -66,6 +66,59 @@ authenticator: After the bounce, C* will accept mTLS connections from the clients and if their identity is present in the `identity_to_roles` table, access will be granted. +== Configuring mTLS with password fallback authenticator for client connections + +Operators that wish to migrate cannot immediately change the configuration to require +mTLS authentication as it will break existing non-mTLS based clients of the cluster. +In order to make a smooth transition from non-mTLS based authentication to mTLS authentication, +the operator can run Cassandra in optional mTLS mode and configure authenticator to be +`MutualTlsWithPasswordFallbackAuthenticator` which can accept both certificate based +and password based connections. + +Below are the steps to configure C* in optional mTLS mode with fallback authenticator. +Note that the following steps uses SPIFFE identity as an example, If you are using +a custom validator, use appropriate identity in place of `spiffe://testdomain.com/testIdentifier/testValue`. + +*STEP 1: Add authorized users to system_auth.identity_to_roles table* + +Note that only users with permissions to create/modify roles can add/remove identities. +Client certificates with the identities in this table will be trusted by C*. +[source, plaintext] +---- +ADD IDENTITY 'spiffe://testdomain.com/testIdentifier/testValue' TO ROLE 'read_only_user' +---- + +*STEP 2: Configure Cassandra.yaml with right properties* + +`client_encryption_options` configuration for mTLS connections, Note that require_client_auth configuration +is optional. +[source, plaintext] +---- +client_encryption_options: + enabled: true + optional: true + keystore: conf/.keystore + keystore_password: cassandra + truststore: conf/.truststore + truststore_password: cassandra + require_client_auth: optional // to enable mTLS in optional mode +---- +Configure fallback authenticator and the validator for client connections . If you are +implementing a custom validator, use that instead of Spiffe validator +[source, plaintext] +---- +authenticator: + class_name : org.apache.cassandra.auth.MutualTlsWithPasswordFallbackAuthenticator + parameters : + validator_class_name: org.apache.cassandra.auth.SpiffeCertificateValidator +---- + +*STEP 3: Bounce the cluster* + +After the bounce, C* will accept both mTLS connections and password based connections from +the clients. This configuration should be used during transition phase and the require_client_auth +configuration should be set to true when all the clients start making mTLS connections to the cluster. + == Configuring mTLS authenticator for Internode connections Internode authenticator trusts certificates whose identities are present in 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 f101e8955d..5ecec4ed26 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", Boolean.FALSE); + commonConfig.put("require_client_auth", "false"; commonConfig.put("cipher_suites", Arrays.asList("TLS_RSA_WITH_AES_128_CBC_SHA")); } diff --git a/examples/ssl-factory/test/unit/org/apache/cassandra/security/KubernetesSecretsSslContextFactoryTest.java b/examples/ssl-factory/test/unit/org/apache/cassandra/security/KubernetesSecretsSslContextFactoryTest.java index 0c364196b8..77e9788d73 100644 --- a/examples/ssl-factory/test/unit/org/apache/cassandra/security/KubernetesSecretsSslContextFactoryTest.java +++ b/examples/ssl-factory/test/unit/org/apache/cassandra/security/KubernetesSecretsSslContextFactoryTest.java @@ -80,7 +80,7 @@ public class KubernetesSecretsSslContextFactoryTest * done to avoid having a dependency on env in the unit test. */ commonConfig.put("MY_TRUSTSTORE_PASSWORD", "cassandra"); - commonConfig.put("require_client_auth", Boolean.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/auth/MutualTlsAuthenticator.java b/src/java/org/apache/cassandra/auth/MutualTlsAuthenticator.java index c327a4bd21..01ff6919fa 100644 --- a/src/java/org/apache/cassandra/auth/MutualTlsAuthenticator.java +++ b/src/java/org/apache/cassandra/auth/MutualTlsAuthenticator.java @@ -40,6 +40,8 @@ import org.apache.cassandra.exceptions.ConfigurationException; import org.apache.cassandra.schema.SchemaConstants; import org.apache.cassandra.utils.NoSpamLogger; +import static org.apache.cassandra.config.EncryptionOptions.ClientAuth.REQUIRED; + /* * Performs mTLS authentication for client connections by extracting identities from client certificate * and verifying them against the authorized identities in IdentityCache. IdentityCache is a loading cache that @@ -79,7 +81,6 @@ public class MutualTlsAuthenticator implements IAuthenticator } certificateValidator = ParameterizedClass.newInstance(new ParameterizedClass(certificateValidatorClassName), Arrays.asList("", AuthConfig.class.getPackage().getName())); - checkMtlsConfigurationIsValid(DatabaseDescriptor.getRawConfig()); AuthCacheService.instance.register(identityCache); } @@ -98,7 +99,14 @@ public class MutualTlsAuthenticator implements IAuthenticator @Override public void validateConfiguration() throws ConfigurationException { - + Config config = DatabaseDescriptor.getRawConfig(); + if (!config.client_encryption_options.getEnabled() || config.client_encryption_options.getClientAuth() != REQUIRED) + { + String msg = "MutualTlsAuthenticator requires client_encryption_options.enabled to be true" + + " & client_encryption_options.require_client_auth to be true"; + logger.error(msg); + throw new ConfigurationException(msg); + } } @Override @@ -175,17 +183,6 @@ public class MutualTlsAuthenticator implements IAuthenticator } } - private void checkMtlsConfigurationIsValid(Config config) - { - if (!config.client_encryption_options.getEnabled() || !config.client_encryption_options.require_client_auth) - { - String msg = "MutualTlsAuthenticator requires client_encryption_options.enabled to be true" + - " & client_encryption_options.require_client_auth to be true"; - logger.error(msg); - throw new ConfigurationException(msg); - } - } - static class IdentityCache extends AuthCache { IdentityCache() diff --git a/src/java/org/apache/cassandra/auth/MutualTlsInternodeAuthenticator.java b/src/java/org/apache/cassandra/auth/MutualTlsInternodeAuthenticator.java index 3c466df5fa..cf5d764c9d 100644 --- a/src/java/org/apache/cassandra/auth/MutualTlsInternodeAuthenticator.java +++ b/src/java/org/apache/cassandra/auth/MutualTlsInternodeAuthenticator.java @@ -46,6 +46,8 @@ import org.apache.cassandra.exceptions.AuthenticationException; import org.apache.cassandra.exceptions.ConfigurationException; import org.apache.cassandra.utils.NoSpamLogger; +import static org.apache.cassandra.config.EncryptionOptions.ClientAuth.REQUIRED; + /* * Performs mTLS authentication for internode connections by extracting identities from the certificates of incoming * connection and verifying them against a list of authorized peers. Authorized peers can be configured in @@ -89,7 +91,6 @@ public class MutualTlsInternodeAuthenticator implements IInternodeAuthenticator certificateValidator = ParameterizedClass.newInstance(new ParameterizedClass(certificateValidatorClassName), Arrays.asList("", AuthConfig.class.getPackage().getName())); Config config = DatabaseDescriptor.getRawConfig(); - checkInternodeMtlsConfigurationIsValid(config); if (parameters.containsKey(TRUSTED_PEER_IDENTITIES)) { @@ -146,6 +147,15 @@ public class MutualTlsInternodeAuthenticator implements IInternodeAuthenticator @Override public void validateConfiguration() throws ConfigurationException { + Config config = DatabaseDescriptor.getRawConfig(); + if (config.server_encryption_options.internode_encryption == EncryptionOptions.ServerEncryptionOptions.InternodeEncryption.none + || config.server_encryption_options.getClientAuth() != REQUIRED) + { + String msg = "MutualTlsInternodeAuthenticator requires server_encryption_options.internode_encryption to be enabled" + + " & server_encryption_options.require_client_auth to be true"; + logger.error(msg); + throw new ConfigurationException(msg); + } } @@ -212,15 +222,4 @@ public class MutualTlsInternodeAuthenticator implements IInternodeAuthenticator return allUsers; } - private void checkInternodeMtlsConfigurationIsValid(Config config) - { - if (config.server_encryption_options.internode_encryption == EncryptionOptions.ServerEncryptionOptions.InternodeEncryption.none - || !config.server_encryption_options.require_client_auth) - { - String msg = "MutualTlsInternodeAuthenticator requires server_encryption_options.internode_encryption to be enabled" + - " & server_encryption_options.require_client_auth to be true"; - logger.error(msg); - throw new ConfigurationException(msg); - } - } } diff --git a/src/java/org/apache/cassandra/auth/MutualTlsWithPasswordFallbackAuthenticator.java b/src/java/org/apache/cassandra/auth/MutualTlsWithPasswordFallbackAuthenticator.java index eba0ea54c0..4c86f6a3fe 100644 --- a/src/java/org/apache/cassandra/auth/MutualTlsWithPasswordFallbackAuthenticator.java +++ b/src/java/org/apache/cassandra/auth/MutualTlsWithPasswordFallbackAuthenticator.java @@ -22,6 +22,11 @@ import java.net.InetAddress; import java.security.cert.Certificate; import java.util.Map; +import org.apache.cassandra.config.Config; +import org.apache.cassandra.config.DatabaseDescriptor; +import org.apache.cassandra.config.EncryptionOptions; +import org.apache.cassandra.exceptions.ConfigurationException; + /** * This authenticator can be used in optional mTLS mode, If the client doesn't make an mTLS connection * this fallbacks to password authentication. @@ -50,4 +55,15 @@ public class MutualTlsWithPasswordFallbackAuthenticator extends PasswordAuthenti } return mutualTlsAuthenticator.newSaslNegotiator(clientAddress, certificates); } + + @Override + public void validateConfiguration() throws ConfigurationException + { + Config config = DatabaseDescriptor.getRawConfig(); + if (config.client_encryption_options.getClientAuth() == EncryptionOptions.ClientAuth.NOT_REQUIRED) + { + String msg = "MutualTlsWithPasswordFallbackAuthenticator requires client_encryption_options.require_client_auth to be optional/true"; + throw new ConfigurationException(msg); + } + } } diff --git a/src/java/org/apache/cassandra/config/DatabaseDescriptor.java b/src/java/org/apache/cassandra/config/DatabaseDescriptor.java index b304100957..4b0f036914 100644 --- a/src/java/org/apache/cassandra/config/DatabaseDescriptor.java +++ b/src/java/org/apache/cassandra/config/DatabaseDescriptor.java @@ -140,6 +140,7 @@ import static org.apache.cassandra.config.CassandraRelevantProperties.UNSAFE_SYS import static org.apache.cassandra.config.DataRateSpec.DataRateUnit.BYTES_PER_SECOND; import static org.apache.cassandra.config.DataRateSpec.DataRateUnit.MEBIBYTES_PER_SECOND; import static org.apache.cassandra.config.DataStorageSpec.DataStorageUnit.MEBIBYTES; +import static org.apache.cassandra.config.EncryptionOptions.ClientAuth.REQUIRED; import static org.apache.cassandra.db.ConsistencyLevel.ALL; import static org.apache.cassandra.db.ConsistencyLevel.EACH_QUORUM; import static org.apache.cassandra.db.ConsistencyLevel.LOCAL_QUORUM; @@ -1241,8 +1242,8 @@ public class DatabaseDescriptor try { - SSLFactory.validateSslContext("Internode messaging", conf.server_encryption_options, true, true); - SSLFactory.validateSslContext("Native transport", conf.client_encryption_options, conf.client_encryption_options.require_client_auth, true); + SSLFactory.validateSslContext("Internode messaging", conf.server_encryption_options, REQUIRED, true); + SSLFactory.validateSslContext("Native transport", conf.client_encryption_options, conf.client_encryption_options.getClientAuth(), true); SSLFactory.initHotReloading(conf.server_encryption_options, conf.client_encryption_options, false); } catch (IOException e) diff --git a/src/java/org/apache/cassandra/config/EncryptionOptions.java b/src/java/org/apache/cassandra/config/EncryptionOptions.java index 9db2406a44..91d9ae5ee2 100644 --- a/src/java/org/apache/cassandra/config/EncryptionOptions.java +++ b/src/java/org/apache/cassandra/config/EncryptionOptions.java @@ -33,6 +33,7 @@ import com.google.common.collect.ImmutableList; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import org.apache.cassandra.exceptions.ConfigurationException; import org.apache.cassandra.locator.IEndpointSnitch; import org.apache.cassandra.locator.InetAddressAndPort; import org.apache.cassandra.security.DisableSslContextFactory; @@ -67,6 +68,42 @@ public class EncryptionOptions } } + public enum ClientAuth + { + REQUIRED("true"), + NOT_REQUIRED("false"), + OPTIONAL("optional"); + private final String value; + private static final Map VALUES = new HashMap<>(); + static + { + for (ClientAuth clientAuth : ClientAuth.values()) + { + VALUES.put(clientAuth.value, clientAuth); + VALUES.put(clientAuth.name().toLowerCase(), clientAuth); + } + } + + ClientAuth(String value) + { + this.value = value; + } + + public static ClientAuth from(String value) + { + if (VALUES.containsKey(value.toLowerCase())) + { + return VALUES.get(value.toLowerCase()); + } + throw new ConfigurationException(value + " is not a valid ClientAuth option"); + } + + public String value() + { + return value; + } + } + /* * If the ssl_context_factory is configured, most likely it won't use file based keystores and truststores and * can choose to completely customize SSL context's creation. Most likely it won't also use keystore_password and @@ -84,7 +121,7 @@ public class EncryptionOptions protected List accepted_protocols; public final String algorithm; public final String store_type; - public final boolean require_client_auth; + public final String require_client_auth; public final boolean require_endpoint_verification; // ServerEncryptionOptions does not use the enabled flag at all instead using the existing // internode_encryption option. So we force this private and expose through isEnabled @@ -159,7 +196,7 @@ public class EncryptionOptions accepted_protocols = null; algorithm = null; store_type = "JKS"; - require_client_auth = false; + require_client_auth = "false"; require_endpoint_verification = false; enabled = null; optional = null; @@ -168,7 +205,7 @@ public class EncryptionOptions public EncryptionOptions(ParameterizedClass ssl_context_factory, String keystore, String keystore_password, String truststore, String truststore_password, List cipher_suites, String protocol, List accepted_protocols, String algorithm, String store_type, - boolean require_client_auth, boolean require_endpoint_verification, Boolean enabled, + String require_client_auth, boolean require_endpoint_verification, Boolean enabled, Boolean optional) { this.ssl_context_factory = ssl_context_factory; @@ -402,6 +439,11 @@ public class EncryptionOptions return sslContextFactoryInstance == null ? null : sslContextFactoryInstance.getAcceptedProtocols(); } + public ClientAuth getClientAuth() + { + return this.require_client_auth == null ? ClientAuth.NOT_REQUIRED : ClientAuth.from(this.require_client_auth); + } + public String[] acceptedProtocolsArray() { List ap = getAcceptedProtocols(); @@ -520,11 +562,11 @@ public class EncryptionOptions optional).applyConfig(); } - public EncryptionOptions withRequireClientAuth(boolean require_client_auth) + public EncryptionOptions withRequireClientAuth(ClientAuth require_client_auth) { return new EncryptionOptions(ssl_context_factory, keystore, keystore_password, truststore, truststore_password, cipher_suites, protocol, accepted_protocols, algorithm, - store_type, require_client_auth, require_endpoint_verification, enabled, + store_type, require_client_auth.value, require_endpoint_verification, enabled, optional).applyConfig(); } @@ -567,7 +609,7 @@ public class EncryptionOptions EncryptionOptions opt = (EncryptionOptions)o; return enabled == opt.enabled && optional == opt.optional && - require_client_auth == opt.require_client_auth && + require_client_auth.equals(opt.require_client_auth) && require_endpoint_verification == opt.require_endpoint_verification && Objects.equals(keystore, opt.keystore) && Objects.equals(keystore_password, opt.keystore_password) && @@ -600,7 +642,7 @@ public class EncryptionOptions result += 31 * (enabled == null ? 0 : Boolean.hashCode(enabled)); result += 31 * (optional == null ? 0 : Boolean.hashCode(optional)); result += 31 * (cipher_suites == null ? 0 : cipher_suites.hashCode()); - result += 31 * Boolean.hashCode(require_client_auth); + result += 31 * require_client_auth.hashCode(); result += 31 * Boolean.hashCode(require_endpoint_verification); result += 31 * (ssl_context_factory == null ? 0 : ssl_context_factory.hashCode()); return result; @@ -632,7 +674,7 @@ public class EncryptionOptions String keystore_password,String outbound_keystore, String outbound_keystore_password, String truststore, String truststore_password, List cipher_suites, String protocol, List accepted_protocols, - String algorithm, String store_type, boolean require_client_auth, + String algorithm, String store_type, String require_client_auth, boolean require_endpoint_verification, Boolean optional, InternodeEncryption internode_encryption, boolean legacy_ssl_storage_port_enabled) { @@ -679,7 +721,7 @@ public class EncryptionOptions logger.warn("Setting server_encryption_options.enabled has no effect, use internode_encryption"); } - if (require_client_auth && (internode_encryption == InternodeEncryption.rack || internode_encryption == InternodeEncryption.dc)) + if (getClientAuth() != ClientAuth.NOT_REQUIRED && (internode_encryption == InternodeEncryption.rack || internode_encryption == InternodeEncryption.dc)) { logger.warn("Setting require_client_auth is incompatible with 'rack' and 'dc' internode_encryption values." + " It is possible for an internode connection to pretend to be in the same rack/dc by spoofing" @@ -875,12 +917,12 @@ public class EncryptionOptions legacy_ssl_storage_port_enabled).applyConfigInternal(); } - public ServerEncryptionOptions withRequireClientAuth(boolean require_client_auth) + public ServerEncryptionOptions withRequireClientAuth(ClientAuth require_client_auth) { return new ServerEncryptionOptions(ssl_context_factory, keystore, keystore_password, outbound_keystore, outbound_keystore_password, truststore, truststore_password, cipher_suites, protocol, accepted_protocols, - algorithm, store_type, require_client_auth, + algorithm, store_type, require_client_auth.value, require_endpoint_verification, optional, internode_encryption, legacy_ssl_storage_port_enabled).applyConfigInternal(); } diff --git a/src/java/org/apache/cassandra/net/InboundConnectionInitiator.java b/src/java/org/apache/cassandra/net/InboundConnectionInitiator.java index c76d9b0375..cd4e33244c 100644 --- a/src/java/org/apache/cassandra/net/InboundConnectionInitiator.java +++ b/src/java/org/apache/cassandra/net/InboundConnectionInitiator.java @@ -64,6 +64,7 @@ import static java.lang.Math.*; import static java.util.concurrent.TimeUnit.MILLISECONDS; import static org.apache.cassandra.auth.IInternodeAuthenticator.InternodeConnectionDirection.INBOUND; import static org.apache.cassandra.concurrent.ExecutorFactory.Global.executorFactory; +import static org.apache.cassandra.config.EncryptionOptions.ClientAuth.REQUIRED; import static org.apache.cassandra.net.InternodeConnectionUtils.DISCARD_HANDLER_NAME; import static org.apache.cassandra.net.InternodeConnectionUtils.SSL_FACTORY_CONTEXT_DESCRIPTION; import static org.apache.cassandra.net.InternodeConnectionUtils.SSL_HANDLER_NAME; @@ -521,7 +522,7 @@ public class InboundConnectionInitiator private static SslHandler getSslHandler(String description, Channel channel, EncryptionOptions.ServerEncryptionOptions encryptionOptions) throws IOException { - final boolean verifyPeerCertificate = true; + final EncryptionOptions.ClientAuth verifyPeerCertificate = REQUIRED; SslContext sslContext = SSLFactory.getOrCreateSslContext(encryptionOptions, verifyPeerCertificate, ISslContextFactory.SocketType.SERVER, SSL_FACTORY_CONTEXT_DESCRIPTION); diff --git a/src/java/org/apache/cassandra/net/OutboundConnectionInitiator.java b/src/java/org/apache/cassandra/net/OutboundConnectionInitiator.java index 63b99a45db..b95d20a158 100644 --- a/src/java/org/apache/cassandra/net/OutboundConnectionInitiator.java +++ b/src/java/org/apache/cassandra/net/OutboundConnectionInitiator.java @@ -29,6 +29,7 @@ import com.google.common.annotations.VisibleForTesting; import io.netty.util.concurrent.Future; //checkstyle: permit this import import io.netty.util.concurrent.Promise; //checkstyle: permit this import +import org.apache.cassandra.config.EncryptionOptions; import org.apache.cassandra.utils.concurrent.AsyncPromise; import org.slf4j.Logger; @@ -65,6 +66,9 @@ import org.apache.cassandra.utils.memory.BufferPools; import static java.util.concurrent.TimeUnit.*; import static org.apache.cassandra.auth.IInternodeAuthenticator.InternodeConnectionDirection.OUTBOUND; import static org.apache.cassandra.auth.IInternodeAuthenticator.InternodeConnectionDirection.OUTBOUND_PRECONNECT; +import static org.apache.cassandra.config.EncryptionOptions.ClientAuth.NOT_REQUIRED; +import static org.apache.cassandra.config.EncryptionOptions.ClientAuth.OPTIONAL; +import static org.apache.cassandra.config.EncryptionOptions.ClientAuth.REQUIRED; import static org.apache.cassandra.net.InternodeConnectionUtils.DISCARD_HANDLER_NAME; import static org.apache.cassandra.net.InternodeConnectionUtils.SSL_FACTORY_CONTEXT_DESCRIPTION; import static org.apache.cassandra.net.InternodeConnectionUtils.SSL_HANDLER_NAME; @@ -241,14 +245,18 @@ public class OutboundConnectionInitiator accepted_protocols; protected final String algorithm; protected final String store_type; - protected final boolean require_client_auth; + protected final EncryptionOptions.ClientAuth clientAuth; protected final boolean require_endpoint_verification; /* ServerEncryptionOptions does not use the enabled flag at all instead using the existing @@ -86,7 +90,7 @@ abstract public class AbstractSslContextFactory implements ISslContextFactory accepted_protocols = null; algorithm = null; store_type = "JKS"; - require_client_auth = false; + clientAuth = NOT_REQUIRED; require_endpoint_verification = false; enabled = null; optional = null; @@ -101,7 +105,7 @@ abstract public class AbstractSslContextFactory implements ISslContextFactory accepted_protocols = getStringList("accepted_protocols"); algorithm = getString("algorithm"); store_type = getString("store_type", "JKS"); - require_client_auth = getBoolean("require_client_auth", false); + clientAuth = parameters.get("require_client_auth") == null ? NOT_REQUIRED : EncryptionOptions.ClientAuth.from(getString("require_client_auth")); require_endpoint_verification = getBoolean("require_endpoint_verification", false); enabled = getBoolean("enabled"); optional = getBoolean("optional"); @@ -149,9 +153,15 @@ abstract public class AbstractSslContextFactory implements ISslContextFactory @Override public SSLContext createJSSESslContext(boolean verifyPeerCertificate) throws SSLException + { + return createJSSESslContext(verifyPeerCertificate ? REQUIRED : NOT_REQUIRED); + } + + @Override + public SSLContext createJSSESslContext(EncryptionOptions.ClientAuth clientAuth) throws SSLException { TrustManager[] trustManagers = null; - if (verifyPeerCertificate) + if (clientAuth != NOT_REQUIRED) trustManagers = buildTrustManagerFactory().getTrustManagers(); KeyManagerFactory kmf = buildKeyManagerFactory(); @@ -171,6 +181,13 @@ abstract public class AbstractSslContextFactory implements ISslContextFactory @Override public SslContext createNettySslContext(boolean verifyPeerCertificate, SocketType socketType, CipherSuiteFilter cipherFilter) throws SSLException + { + return createNettySslContext(verifyPeerCertificate ? REQUIRED : NOT_REQUIRED, socketType, cipherFilter); + } + + @Override + public SslContext createNettySslContext(EncryptionOptions.ClientAuth clientAuth, SocketType socketType, + CipherSuiteFilter cipherFilter) throws SSLException { /* There is a case where the netty/openssl combo might not support using KeyManagerFactory. Specifically, @@ -184,8 +201,7 @@ abstract public class AbstractSslContextFactory implements ISslContextFactory if (socketType == SocketType.SERVER) { KeyManagerFactory kmf = buildKeyManagerFactory(); - builder = SslContextBuilder.forServer(kmf).clientAuth(this.require_client_auth ? ClientAuth.REQUIRE : - ClientAuth.NONE); + builder = SslContextBuilder.forServer(kmf).clientAuth(toNettyClientAuth(this.clientAuth)); } else { @@ -200,7 +216,7 @@ abstract public class AbstractSslContextFactory implements ISslContextFactory if (cipher_suites != null && !cipher_suites.isEmpty()) builder.ciphers(cipher_suites, cipherFilter); - if (verifyPeerCertificate) + if (clientAuth != NOT_REQUIRED) builder.trustManager(buildTrustManagerFactory()); return builder.build(); @@ -274,4 +290,19 @@ abstract public class AbstractSslContextFactory implements ISslContextFactory * @throws SSLException */ abstract protected KeyManagerFactory buildOutboundKeyManagerFactory() throws SSLException; + + private ClientAuth toNettyClientAuth(EncryptionOptions.ClientAuth clientAuth) + { + switch (clientAuth) + { + case REQUIRED: + return ClientAuth.REQUIRE; + case NOT_REQUIRED: + return ClientAuth.NONE; + case OPTIONAL: + return ClientAuth.OPTIONAL; + default: + throw new RuntimeException("Unsupported client auth " + clientAuth); + } + } } diff --git a/src/java/org/apache/cassandra/security/ISslContextFactory.java b/src/java/org/apache/cassandra/security/ISslContextFactory.java index 11c4717b3c..1db5f579b1 100644 --- a/src/java/org/apache/cassandra/security/ISslContextFactory.java +++ b/src/java/org/apache/cassandra/security/ISslContextFactory.java @@ -24,6 +24,7 @@ import javax.net.ssl.SSLException; import io.netty.handler.ssl.CipherSuiteFilter; import io.netty.handler.ssl.SslContext; +import org.apache.cassandra.config.EncryptionOptions; /** * The purpose of this interface is to provide pluggable mechanism for creating custom JSSE and Netty SSLContext @@ -57,9 +58,33 @@ public interface ISslContextFactory * @param verifyPeerCertificate {@code true} if SSL peer's certificate needs to be verified; {@code false} otherwise * @return JSSE's {@link SSLContext} * @throws SSLException in case the Ssl Context creation fails for some reason + * + * @deprecated See CASSANDRA-18811 */ + @Deprecated(since = "5.1") SSLContext createJSSESslContext(boolean verifyPeerCertificate) throws SSLException; + /** + * Creates JSSE SSLContext. + * + * @param clientAuth {@code REQUIRED} if SSL peer's certificate needs to be verified; {@code OPTIONAL} if peer's + * certificate needs to be optionally verfied; {@code NOT_REQUIRED} otherwise. + * @return JSSE's {@link SSLContext} + * @throws SSLException in case the Ssl Context creation fails for some reason + */ + default SSLContext createJSSESslContext(EncryptionOptions.ClientAuth clientAuth) throws SSLException + { + switch (clientAuth) + { + case REQUIRED: + return createJSSESslContext(true); + case NOT_REQUIRED: + case OPTIONAL: + default: + return createJSSESslContext(false); + } + } + /** * Creates Netty's SslContext object. * @@ -69,10 +94,38 @@ public interface ISslContextFactory * {@link io.netty.handler.ssl.SslContextBuilder#ciphers(Iterable, CipherSuiteFilter)} * @return Netty's {@link SslContext} * @throws SSLException in case the Ssl Context creation fails for some reason + * + * @deprecated See CASSANDRA-18811 */ + @Deprecated(since = "5.1") SslContext createNettySslContext(boolean verifyPeerCertificate, SocketType socketType, CipherSuiteFilter cipherFilter) throws SSLException; + /** + * Creates Netty's SslContext object. + * + * @param clientAuth {@code REQUIRED} if SSL peer's certificate needs to be verified; {@code OPTIONAL} if peer's + * * certificate needs to be optionally verfied; {@code NOT_REQUIRED} otherwise. + * @param socketType {@link SocketType} for Netty's Inbound or Outbound channels + * @param cipherFilter to allow Netty's cipher suite filtering, e.g. + * {@link io.netty.handler.ssl.SslContextBuilder#ciphers(Iterable, CipherSuiteFilter)} + * @return Netty's {@link SslContext} + * @throws SSLException in case the Ssl Context creation fails for some reason + */ + default SslContext createNettySslContext(EncryptionOptions.ClientAuth clientAuth, SocketType socketType, + CipherSuiteFilter cipherFilter) throws SSLException + { + switch (clientAuth) + { + case REQUIRED: + return createNettySslContext(true, socketType, cipherFilter); + case NOT_REQUIRED: + case OPTIONAL: + default: + return createNettySslContext(false, socketType, cipherFilter); + } + } + /** * Initializes hot reloading of the security keys/certs. The implementation must guarantee this to be thread safe. * diff --git a/src/java/org/apache/cassandra/security/SSLFactory.java b/src/java/org/apache/cassandra/security/SSLFactory.java index 5a175b3fd6..c55e970b4f 100644 --- a/src/java/org/apache/cassandra/security/SSLFactory.java +++ b/src/java/org/apache/cassandra/security/SSLFactory.java @@ -47,6 +47,8 @@ import org.apache.cassandra.security.ISslContextFactory.SocketType; import static org.apache.cassandra.config.CassandraRelevantProperties.DISABLE_TCACTIVE_OPENSSL; +import static org.apache.cassandra.config.EncryptionOptions.ClientAuth.REQUIRED; + /** * A Factory for providing and setting up client {@link SSLSocket}s. Also provides * methods for creating both JSSE {@link SSLContext} instances as well as netty {@link SslContext} instances. @@ -122,15 +124,15 @@ public final class SSLFactory /** * Create a JSSE {@link SSLContext}. */ - public static SSLContext createSSLContext(EncryptionOptions options, boolean verifyPeerCertificate) throws IOException + public static SSLContext createSSLContext(EncryptionOptions options, EncryptionOptions.ClientAuth clientAuth) throws IOException { - return options.sslContextFactoryInstance.createJSSESslContext(verifyPeerCertificate); + return options.sslContextFactoryInstance.createJSSESslContext(clientAuth); } /** * get a netty {@link SslContext} instance */ - public static SslContext getOrCreateSslContext(EncryptionOptions options, boolean verifyPeerCertificate, + public static SslContext getOrCreateSslContext(EncryptionOptions options, EncryptionOptions.ClientAuth clientAuth, SocketType socketType, String contextDescription) throws IOException { @@ -141,7 +143,7 @@ public final class SSLFactory if (sslContext != null) return sslContext; - sslContext = createNettySslContext(options, verifyPeerCertificate, socketType); + sslContext = createNettySslContext(options, clientAuth, socketType); SslContext previous = cachedSslContexts.putIfAbsent(key, sslContext); if (previous == null) @@ -154,20 +156,20 @@ public final class SSLFactory /** * Create a Netty {@link SslContext} */ - static SslContext createNettySslContext(EncryptionOptions options, boolean verifyPeerCertificate, + static SslContext createNettySslContext(EncryptionOptions options, EncryptionOptions.ClientAuth clientAuth, SocketType socketType) throws IOException { - return createNettySslContext(options, verifyPeerCertificate, socketType, + return createNettySslContext(options, clientAuth, socketType, LoggingCipherSuiteFilter.QUIET_FILTER); } /** * Create a Netty {@link SslContext} with a supplied cipherFilter */ - static SslContext createNettySslContext(EncryptionOptions options, boolean verifyPeerCertificate, + static SslContext createNettySslContext(EncryptionOptions options, EncryptionOptions.ClientAuth clientAuth, SocketType socketType, CipherSuiteFilter cipherFilter) throws IOException { - return options.sslContextFactoryInstance.createNettySslContext(verifyPeerCertificate, socketType, + return options.sslContextFactoryInstance.createNettySslContext(clientAuth, socketType, cipherFilter); } @@ -206,7 +208,7 @@ public final class SSLFactory try { validateSslContext(key.contextDescription, opts, - opts instanceof EncryptionOptions.ServerEncryptionOptions || opts.require_client_auth, false); + opts instanceof EncryptionOptions.ServerEncryptionOptions? REQUIRED : opts.getClientAuth(), false); logger.info("SSL certificates have been updated for {}. Resetting the ssl contexts for new " + "connections.", key.contextDescription); clearSslContextCache(key.encryptionOptions, keysToCheck); @@ -353,7 +355,7 @@ public final class SSLFactory return !string.equals("SSLv2Hello"); } - public static void validateSslContext(String contextDescription, EncryptionOptions options, boolean verifyPeerCertificate, boolean logProtocolAndCiphers) throws IOException + public static void validateSslContext(String contextDescription, EncryptionOptions options, EncryptionOptions.ClientAuth clientAuth, boolean logProtocolAndCiphers) throws IOException { if (options != null && options.tlsEncryptionPolicy() != EncryptionOptions.TlsEncryptionPolicy.UNENCRYPTED) { @@ -361,7 +363,7 @@ public final class SSLFactory { CipherSuiteFilter loggingCipherSuiteFilter = logProtocolAndCiphers ? new LoggingCipherSuiteFilter(contextDescription) : LoggingCipherSuiteFilter.QUIET_FILTER; - SslContext serverSslContext = createNettySslContext(options, verifyPeerCertificate, SocketType.SERVER, loggingCipherSuiteFilter); + SslContext serverSslContext = createNettySslContext(options, clientAuth, SocketType.SERVER, loggingCipherSuiteFilter); try { SSLEngine engine = serverSslContext.newEngine(ByteBufAllocator.DEFAULT); @@ -406,7 +408,7 @@ public final class SSLFactory } // Make sure it is possible to build the client context too - SslContext clientSslContext = createNettySslContext(options, verifyPeerCertificate, SocketType.CLIENT); + SslContext clientSslContext = createNettySslContext(options, clientAuth, SocketType.CLIENT); ReferenceCountUtil.release(clientSslContext); } catch (Exception e) @@ -421,8 +423,8 @@ public final class SSLFactory */ public static void validateSslCerts(EncryptionOptions.ServerEncryptionOptions serverOpts, EncryptionOptions clientOpts) throws IOException { - validateSslContext("server_encryption_options", serverOpts, true, false); - validateSslContext("client_encryption_options", clientOpts, clientOpts.require_client_auth, false); + validateSslContext("server_encryption_options", serverOpts, REQUIRED, false); + validateSslContext("client_encryption_options", clientOpts, clientOpts.getClientAuth(), false); } static class CacheKey diff --git a/src/java/org/apache/cassandra/tools/BulkLoader.java b/src/java/org/apache/cassandra/tools/BulkLoader.java index 8802b9e837..367903a595 100644 --- a/src/java/org/apache/cassandra/tools/BulkLoader.java +++ b/src/java/org/apache/cassandra/tools/BulkLoader.java @@ -48,6 +48,7 @@ import org.apache.cassandra.utils.JVMStabilityInspector; import org.apache.cassandra.utils.NativeSSTableLoaderClient; import org.apache.cassandra.utils.OutputHandler; +import static org.apache.cassandra.config.EncryptionOptions.ClientAuth.REQUIRED; import static org.apache.cassandra.utils.Clock.Global.nanoTime; public class BulkLoader @@ -264,7 +265,7 @@ public class BulkLoader SSLContext sslContext; try { - sslContext = SSLFactory.createSSLContext(clientEncryptionOptions, true); + sslContext = SSLFactory.createSSLContext(clientEncryptionOptions, REQUIRED); } catch (IOException e) { diff --git a/src/java/org/apache/cassandra/tools/LoaderOptions.java b/src/java/org/apache/cassandra/tools/LoaderOptions.java index 03c8ee6024..1f368168a7 100644 --- a/src/java/org/apache/cassandra/tools/LoaderOptions.java +++ b/src/java/org/apache/cassandra/tools/LoaderOptions.java @@ -52,6 +52,7 @@ import org.apache.cassandra.locator.InetAddressAndPort; import org.apache.cassandra.tools.BulkLoader.CmdLineOptions; import static org.apache.cassandra.config.DataRateSpec.DataRateUnit.MEBIBYTES_PER_SECOND; +import static org.apache.cassandra.config.EncryptionOptions.ClientAuth.REQUIRED; public class LoaderOptions { @@ -648,7 +649,7 @@ public class LoaderOptions { // if a keystore was provided, lets assume we'll need to use clientEncOptions = clientEncOptions.withKeyStore(cmd.getOptionValue(SSL_KEYSTORE)) - .withRequireClientAuth(true); + .withRequireClientAuth(REQUIRED); } if (cmd.hasOption(SSL_KEYSTORE_PW)) diff --git a/src/java/org/apache/cassandra/transport/PipelineConfigurator.java b/src/java/org/apache/cassandra/transport/PipelineConfigurator.java index d19af211e2..1bcebf96cf 100644 --- a/src/java/org/apache/cassandra/transport/PipelineConfigurator.java +++ b/src/java/org/apache/cassandra/transport/PipelineConfigurator.java @@ -168,7 +168,7 @@ public class PipelineConfigurator logger.debug("Enabling optionally encrypted CQL connections between client and server"); return channel -> { SslContext sslContext = SSLFactory.getOrCreateSslContext(encryptionOptions, - encryptionOptions.require_client_auth, + encryptionOptions.getClientAuth(), ISslContextFactory.SocketType.SERVER, SSL_FACTORY_CONTEXT_DESCRIPTION); @@ -204,7 +204,7 @@ public class PipelineConfigurator logger.debug("Enabling encrypted CQL connections between client and server"); return channel -> { SslContext sslContext = SSLFactory.getOrCreateSslContext(encryptionOptions, - encryptionOptions.require_client_auth, + encryptionOptions.getClientAuth(), ISslContextFactory.SocketType.SERVER, SSL_FACTORY_CONTEXT_DESCRIPTION); InetSocketAddress peer = encryptionOptions.require_endpoint_verification ? (InetSocketAddress) channel.remoteAddress() : null; diff --git a/src/java/org/apache/cassandra/transport/SimpleClient.java b/src/java/org/apache/cassandra/transport/SimpleClient.java index f4a1b9fd3f..0fa3a7af31 100644 --- a/src/java/org/apache/cassandra/transport/SimpleClient.java +++ b/src/java/org/apache/cassandra/transport/SimpleClient.java @@ -623,7 +623,7 @@ public class SimpleClient implements Closeable protected void initChannel(Channel channel) throws Exception { super.initChannel(channel); - SslContext sslContext = SSLFactory.getOrCreateSslContext(encryptionOptions, encryptionOptions.require_client_auth, + SslContext sslContext = SSLFactory.getOrCreateSslContext(encryptionOptions, encryptionOptions.getClientAuth(), ISslContextFactory.SocketType.CLIENT, SSL_FACTORY_CONTEXT_DESCRIPTION); InetSocketAddress peer = encryptionOptions.require_endpoint_verification ? new InetSocketAddress(host, port) : null; channel.pipeline().addFirst("ssl", newSslHandler(channel, sslContext, peer)); diff --git a/test/distributed/org/apache/cassandra/distributed/test/AbstractEncryptionOptionsImpl.java b/test/distributed/org/apache/cassandra/distributed/test/AbstractEncryptionOptionsImpl.java index b488867433..5f119c6bdf 100644 --- a/test/distributed/org/apache/cassandra/distributed/test/AbstractEncryptionOptionsImpl.java +++ b/test/distributed/org/apache/cassandra/distributed/test/AbstractEncryptionOptionsImpl.java @@ -54,6 +54,7 @@ import org.apache.cassandra.security.ISslContextFactory; import org.apache.cassandra.security.SSLFactory; import static java.util.concurrent.TimeUnit.SECONDS; +import static org.apache.cassandra.config.EncryptionOptions.ClientAuth.REQUIRED; import static org.apache.cassandra.distributed.test.AbstractEncryptionOptionsImpl.ConnectResult.CONNECTING; import static org.apache.cassandra.distributed.test.AbstractEncryptionOptionsImpl.ConnectResult.UNINITIALIZED; import static org.apache.cassandra.utils.concurrent.Condition.newOneTimeCondition; @@ -199,8 +200,8 @@ public class AbstractEncryptionOptionsImpl extends TestBaseImpl setProtocolAndCipher(null, null); SslContext sslContext = SSLFactory.getOrCreateSslContext( - encryptionOptions.withAcceptedProtocols(acceptedProtocols).withCipherSuites(cipherSuites), - true, ISslContextFactory.SocketType.CLIENT, "test"); + encryptionOptions.withAcceptedProtocols(acceptedProtocols).withCipherSuites(cipherSuites), + REQUIRED, ISslContextFactory.SocketType.CLIENT, "test"); EventLoopGroup workerGroup = new NioEventLoopGroup(); Bootstrap b = new Bootstrap(); diff --git a/test/distributed/org/apache/cassandra/distributed/test/NativeTransportEncryptionOptionsTest.java b/test/distributed/org/apache/cassandra/distributed/test/NativeTransportEncryptionOptionsTest.java index 3e8c926480..86d4e8fa26 100644 --- a/test/distributed/org/apache/cassandra/distributed/test/NativeTransportEncryptionOptionsTest.java +++ b/test/distributed/org/apache/cassandra/distributed/test/NativeTransportEncryptionOptionsTest.java @@ -41,6 +41,9 @@ import com.datastax.shaded.netty.handler.ssl.SslContextBuilder; import org.apache.cassandra.distributed.Cluster; import org.apache.cassandra.distributed.api.Feature; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertNull; + public class NativeTransportEncryptionOptionsTest extends AbstractEncryptionOptionsImpl { @Rule @@ -154,9 +157,9 @@ public class NativeTransportEncryptionOptionsTest extends AbstractEncryptionOpti /** * Tests that the negotiated protocol is the highest common protocol between the client and server. - *

- * Note: This test uses TLSV1.1, which is disabled by default in JDK 8 and higher. If the test fails with - * FAILED_TO_NEGOTIATE, it may be necessary to check the java.security file in your JDK installation and remove + *

+ * Note: This test uses TLSV1.1, which is disabled by default in JDK 8 and higher. If the test fails with + * FAILED_TO_NEGOTIATE, it may be necessary to check the java.security file in your JDK installation and remove * TLSv1.1 from the jdk.tls.disabledAlgorithms. * @see CASSANDRA-18540 * @see @@ -278,6 +281,138 @@ public class NativeTransportEncryptionOptionsTest extends AbstractEncryptionOpti testEndpointVerification(true, true); } + @Test + public void testOptionalMtlsModeAllowNonSSLConnections() throws Exception + { + // When server is configured in optional mTLS mode and allowing ssl connections, it should accept + // non-ssl, ssl, mTLS based connections + try (Cluster cluster = builder().withNodes(1).withConfig(c -> { + c.with(Feature.NATIVE_PROTOCOL); + c.set("client_encryption_options", + ImmutableMap.builder().putAll(validKeystore) + .put("enabled", true) + .put("require_client_auth", "optional") + .put("optional", true) + .build()); + }).start()) + { + InetAddress address = cluster.get(1).config().broadcastAddress().getAddress(); + + // non-ssl connections should succeed + com.datastax.driver.core.Cluster nonSSLDriver = com.datastax.driver.core.Cluster.builder() + .addContactPoint(address.getHostAddress()) + .build(); + assertNotNull(nonSSLDriver.connect()); + + // ssl connections should succeed + com.datastax.driver.core.Cluster sslDriver = com.datastax.driver.core.Cluster.builder() + .addContactPoint(address.getHostAddress()) + .withSSL(sslOptions(false)) + .build(); + assertNotNull(sslDriver.connect()); + + // mtls connections should succeed + com.datastax.driver.core.Cluster mtlsDriver = com.datastax.driver.core.Cluster.builder() + .addContactPoint(address.getHostAddress()) + .withSSL(sslOptions(true)) + .build(); + assertNotNull(mtlsDriver.connect()); + } + } + + @Test + public void testOptionalMtlsModeDoNotAllowNonSSLConnections() throws Exception + { + // When server is configured in optional mTLS mode restricting non-ssl connections, it should accept + // ssl, mTLS based connections and reject any non-ssl based connections + try (Cluster cluster = builder().withNodes(1).withConfig(c -> { + c.with(Feature.NATIVE_PROTOCOL); + c.set("client_encryption_options", + ImmutableMap.builder().putAll(validKeystore) + .put("enabled", true) + .put("require_client_auth", "optional") + .put("optional", false) + .build()); + }).start()) + { + InetAddress address = cluster.get(1).config().broadcastAddress().getAddress(); + + // ssl connections should succeed + com.datastax.driver.core.Cluster sslDriver = com.datastax.driver.core.Cluster.builder() + .addContactPoint(address.getHostAddress()) + .withSSL(sslOptions(false)) + .build(); + assertNotNull(sslDriver.connect()); + + // mtls connections should succeed + com.datastax.driver.core.Cluster mtlsDriver = com.datastax.driver.core.Cluster.builder() + .addContactPoint(address.getHostAddress()) + .withSSL(sslOptions(true)) + .build(); + assertNotNull(mtlsDriver.connect()); + + // non-ssl connections should not succeed + com.datastax.driver.core.Cluster nonSSLDriver = com.datastax.driver.core.Cluster.builder() + .addContactPoint(address.getHostAddress()) + .build(); + expectedException.expect(NoHostAvailableException.class); + assertNull(nonSSLDriver.connect()); + } + } + + @Test + public void testOptionalSSLMode() throws Exception + { + // When server is configured in optional ssl mode, it should accept + // non-ssl, ssl + try (Cluster cluster = builder().withNodes(1).withConfig(c -> { + // Server configuration for optional mTLS mode + c.with(Feature.NATIVE_PROTOCOL); + c.set("client_encryption_options", + ImmutableMap.builder().putAll(validKeystore) + .put("enabled", true) + .put("require_client_auth", "false") + .put("optional", true) + .build()); + }).start()) + { + InetAddress address = cluster.get(1).config().broadcastAddress().getAddress(); + + // non-ssl connections should succeed + com.datastax.driver.core.Cluster nonSSLDriver = com.datastax.driver.core.Cluster.builder() + .addContactPoint(address.getHostAddress()) + .build(); + assertNotNull(nonSSLDriver.connect()); + + // ssl connections should succeed + com.datastax.driver.core.Cluster sslDriver = com.datastax.driver.core.Cluster.builder() + .addContactPoint(address.getHostAddress()) + .withSSL(sslOptions(false)) + .build(); + assertNotNull(sslDriver.connect()); + + // mtls connections should succeed + com.datastax.driver.core.Cluster mtlsDriver = com.datastax.driver.core.Cluster.builder() + .addContactPoint(address.getHostAddress()) + .withSSL(sslOptions(true)) + .build(); + assertNotNull(mtlsDriver.connect()); + } + } + + private SSLOptions sslOptions(boolean withKeyStore) throws Exception + { + SslContextBuilder sslContextBuilder = SslContextBuilder.forClient(); + sslContextBuilder.trustManager(createTrustManagerFactory("test/conf/cassandra_ssl_test.truststore", "cassandra")); + if (withKeyStore) + { + sslContextBuilder.keyManager(createKeyManagerFactory("test/conf/cassandra_ssl_test_outbound.keystore", "cassandra")); + } + + SslContext sslContext = sslContextBuilder.build(); + return socketChannel -> sslContext.newHandler(socketChannel.alloc()); + } + private void testEndpointVerification(boolean requireEndpointVerification, boolean ipInSAN) throws Throwable { try (Cluster cluster = builder().withNodes(1).withConfig(c -> { diff --git a/test/unit/org/apache/cassandra/auth/MutualTlsAuthenticatorTest.java b/test/unit/org/apache/cassandra/auth/MutualTlsAuthenticatorTest.java index b30a8f8d0b..14b31a3c87 100644 --- a/test/unit/org/apache/cassandra/auth/MutualTlsAuthenticatorTest.java +++ b/test/unit/org/apache/cassandra/auth/MutualTlsAuthenticatorTest.java @@ -37,6 +37,7 @@ import org.junit.runners.Parameterized; import org.apache.cassandra.SchemaLoader; import org.apache.cassandra.config.Config; import org.apache.cassandra.config.DatabaseDescriptor; +import org.apache.cassandra.config.EncryptionOptions; import org.apache.cassandra.exceptions.AuthenticationException; import org.apache.cassandra.exceptions.ConfigurationException; import org.apache.cassandra.net.MessagingService; @@ -47,6 +48,7 @@ import org.apache.cassandra.utils.MBeanWrapper; import static org.apache.cassandra.auth.AuthTestUtils.getMockInetAddress; import static org.apache.cassandra.auth.AuthTestUtils.initializeIdentityRolesTable; import static org.apache.cassandra.auth.AuthTestUtils.loadCertificateChain; +import static org.apache.cassandra.config.EncryptionOptions.ClientAuth.REQUIRED; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertNull; @@ -78,7 +80,7 @@ public class MutualTlsAuthenticatorTest ((CassandraRoleManager)DatabaseDescriptor.getRoleManager()).loadIdentityStatement(); final Config config = DatabaseDescriptor.getRawConfig(); config.client_encryption_options = config.client_encryption_options.withEnabled(true) - .withRequireClientAuth(true); + .withRequireClientAuth(REQUIRED); } @After @@ -172,6 +174,22 @@ public class MutualTlsAuthenticatorTest assertEquals("readonly_user", urnCache.get(identity2)); } + @Test + public void testValidateConfiguration() + { + // In strict mTLS mode mtls authenticator should check for require_client_auth to be true + final Config config = DatabaseDescriptor.getRawConfig(); + String msg = "MutualTlsAuthenticator requires client_encryption_options.enabled to be true" + + " & client_encryption_options.require_client_auth to be true"; + MutualTlsAuthenticator mutualTlsAuthenticator = createAndInitializeMtlsAuthenticator(); + + config.client_encryption_options = config.client_encryption_options.withEnabled(true) + .withRequireClientAuth(EncryptionOptions.ClientAuth.NOT_REQUIRED); + expectedException.expect(ConfigurationException.class); + expectedException.expectMessage(msg); + mutualTlsAuthenticator.validateConfiguration(); + } + MutualTlsAuthenticator createAndInitializeMtlsAuthenticator() { Map parameters = Collections.singletonMap("validator_class_name", getValidatorClass()); diff --git a/test/unit/org/apache/cassandra/auth/MutualTlsWithPasswordFallbackAuthenticatorTest.java b/test/unit/org/apache/cassandra/auth/MutualTlsWithPasswordFallbackAuthenticatorTest.java index 6a522ca853..95f7c133ea 100644 --- a/test/unit/org/apache/cassandra/auth/MutualTlsWithPasswordFallbackAuthenticatorTest.java +++ b/test/unit/org/apache/cassandra/auth/MutualTlsWithPasswordFallbackAuthenticatorTest.java @@ -33,6 +33,7 @@ import org.junit.Test; import org.apache.cassandra.SchemaLoader; import org.apache.cassandra.config.Config; import org.apache.cassandra.config.DatabaseDescriptor; +import org.apache.cassandra.config.EncryptionOptions; import static org.apache.cassandra.auth.AuthTestUtils.getMockInetAddress; import static org.junit.Assert.assertNotNull; @@ -50,7 +51,7 @@ public class MutualTlsWithPasswordFallbackAuthenticatorTest SchemaLoader.loadSchema(); Config config = DatabaseDescriptor.getRawConfig(); config.client_encryption_options = config.client_encryption_options.withEnabled(true) - .withRequireClientAuth(true); + .withRequireClientAuth(EncryptionOptions.ClientAuth.OPTIONAL); Map parameters = Collections.singletonMap("validator_class_name", "org.apache.cassandra.auth.SpiffeCertificateValidator"); fallbackAuthenticator = new MutualTlsWithPasswordFallbackAuthenticator(parameters); fallbackAuthenticator.setup(); @@ -88,4 +89,11 @@ public class MutualTlsWithPasswordFallbackAuthenticatorTest IAuthenticator.SaslNegotiator mutualtlsAuthenticator = fallbackAuthenticator.newSaslNegotiator(getMockInetAddress(), clientCertificatesCorp); assertTrue(mutualtlsAuthenticator instanceof MutualTlsAuthenticator.CertificateNegotiator); } + + @Test + public void testValidateConfiguration() + { + // In optional mTLS mode fallback authenticator should not check for require_client_auth to be true + fallbackAuthenticator.validateConfiguration(); + } } diff --git a/test/unit/org/apache/cassandra/config/ConfigCompatibilityTest.java b/test/unit/org/apache/cassandra/config/ConfigCompatibilityTest.java index e6e714daaa..f965182915 100644 --- a/test/unit/org/apache/cassandra/config/ConfigCompatibilityTest.java +++ b/test/unit/org/apache/cassandra/config/ConfigCompatibilityTest.java @@ -104,6 +104,7 @@ public class ConfigCompatibilityTest .add("authenticator types do not match; org.apache.cassandra.config.ParameterizedClass != java.lang.String") .add("Property internode_authenticator used to be a value-type, but now is nested type class org.apache.cassandra.config.ParameterizedClass") .add("Property authenticator used to be a value-type, but now is nested type class org.apache.cassandra.config.ParameterizedClass") + .add("require_client_auth types do not match; java.lang.String != java.lang.Boolean") .build(); /** @@ -146,7 +147,7 @@ public class ConfigCompatibilityTest public void diff_5_0() throws IOException { diff(TEST_DIR + "/version=5.0-alpha1.yml", ImmutableSet.builder() - .build(), ImmutableSet.of()); + .build(), EXPECTED_FOR_50); } private void diff(String original, Set ignore, Set expectedErrors) throws IOException diff --git a/test/unit/org/apache/cassandra/config/EncryptionOptionsEqualityTest.java b/test/unit/org/apache/cassandra/config/EncryptionOptionsEqualityTest.java index e5aabc8ff3..7c86069a80 100644 --- a/test/unit/org/apache/cassandra/config/EncryptionOptionsEqualityTest.java +++ b/test/unit/org/apache/cassandra/config/EncryptionOptionsEqualityTest.java @@ -26,6 +26,8 @@ import org.junit.Test; import org.apache.cassandra.security.DefaultSslContextFactory; import org.apache.cassandra.security.DummySslContextFactoryImpl; +import static org.apache.cassandra.config.EncryptionOptions.ClientAuth.NOT_REQUIRED; +import static org.apache.cassandra.config.EncryptionOptions.ClientAuth.REQUIRED; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertNotEquals; @@ -46,7 +48,7 @@ public class EncryptionOptionsEqualityTest .withOutboundKeystore("test/conf/cassandra_outbound.keystore") .withOutboundKeystorePassword("cassandra") .withProtocol("TLSv1.1") - .withRequireClientAuth(true) + .withRequireClientAuth(REQUIRED) .withRequireEndpointVerification(false); } @@ -60,7 +62,7 @@ public class EncryptionOptionsEqualityTest .withTrustStore("test/conf/cassandra_ssl_test.truststore") .withTrustStorePassword("cassandra") .withProtocol("TLSv1.1") - .withRequireClientAuth(true) + .withRequireClientAuth(REQUIRED) .withRequireEndpointVerification(false); EncryptionOptions encryptionOptions2 = @@ -71,7 +73,7 @@ public class EncryptionOptionsEqualityTest .withTrustStore("test/conf/cassandra_ssl_test.truststore") .withTrustStorePassword("cassandra") .withProtocol("TLSv1.1") - .withRequireClientAuth(true) + .withRequireClientAuth(REQUIRED) .withRequireEndpointVerification(false); assertEquals(encryptionOptions1, encryptionOptions2); @@ -88,7 +90,7 @@ public class EncryptionOptionsEqualityTest new EncryptionOptions() .withSslContextFactory(new ParameterizedClass(DummySslContextFactoryImpl.class.getName(), parameters1)) .withProtocol("TLSv1.1") - .withRequireClientAuth(true) + .withRequireClientAuth(REQUIRED) .withRequireEndpointVerification(false); Map parameters2 = new HashMap<>(); @@ -98,7 +100,7 @@ public class EncryptionOptionsEqualityTest new EncryptionOptions() .withSslContextFactory(new ParameterizedClass(DummySslContextFactoryImpl.class.getName(), parameters2)) .withProtocol("TLSv1.1") - .withRequireClientAuth(true) + .withRequireClientAuth(REQUIRED) .withRequireEndpointVerification(false); assertEquals(encryptionOptions1, encryptionOptions2); @@ -115,7 +117,7 @@ public class EncryptionOptionsEqualityTest new EncryptionOptions() .withSslContextFactory(new ParameterizedClass(DummySslContextFactoryImpl.class.getName(), parameters1)) .withProtocol("TLSv1.1") - .withRequireClientAuth(false) + .withRequireClientAuth(NOT_REQUIRED) .withRequireEndpointVerification(true); Map parameters2 = new HashMap<>(); @@ -125,7 +127,7 @@ public class EncryptionOptionsEqualityTest new EncryptionOptions() .withSslContextFactory(new ParameterizedClass(DefaultSslContextFactory.class.getName(), parameters2)) .withProtocol("TLSv1.1") - .withRequireClientAuth(false) + .withRequireClientAuth(NOT_REQUIRED) .withRequireEndpointVerification(true); assertNotEquals(encryptionOptions1, encryptionOptions2); diff --git a/test/unit/org/apache/cassandra/config/EncryptionOptionsTest.java b/test/unit/org/apache/cassandra/config/EncryptionOptionsTest.java index 0ff2124794..f09527067f 100644 --- a/test/unit/org/apache/cassandra/config/EncryptionOptionsTest.java +++ b/test/unit/org/apache/cassandra/config/EncryptionOptionsTest.java @@ -61,7 +61,7 @@ public class EncryptionOptionsTest new HashMap<>()), keystorePath, "dummypass", "dummytruststore", "dummypass", - Collections.emptyList(), null, null, null, "JKS", false, false, enabled, optional) + Collections.emptyList(), null, null, null, "JKS", "false", false, enabled, optional) .applyConfig(), expected, String.format("optional=%s keystore=%s enabled=%s", optional, keystorePath, enabled)); @@ -75,7 +75,7 @@ public class EncryptionOptionsTest customSslContextFactoryParams), keystorePath, "dummypass", "dummytruststore", "dummypass", - Collections.emptyList(), null, null, null, "JKS", false, false, enabled, optional) + Collections.emptyList(), null, null, null, "JKS", "false", false, enabled, optional) .applyConfig(), expected, String.format("optional=%s keystore=%s enabled=%s", optional, keystorePath, enabled)); @@ -126,7 +126,7 @@ public class EncryptionOptionsTest { return new ServerEncryptionOptionsTestCase(new EncryptionOptions.ServerEncryptionOptions(new ParameterizedClass("org.apache.cassandra.security.DefaultSslContextFactory", new HashMap<>()), keystorePath, "dummypass", keystorePath, "dummypass", "dummytruststore", "dummypass", - Collections.emptyList(), null, null, null, "JKS", false, false, optional, internodeEncryption, false) + Collections.emptyList(), null, null, null, "JKS", "false", false, optional, internodeEncryption, false) .applyConfig(), expected, String.format("optional=%s keystore=%s internode=%s", optional, keystorePath, internodeEncryption)); diff --git a/test/unit/org/apache/cassandra/db/virtual/SettingsTableTest.java b/test/unit/org/apache/cassandra/db/virtual/SettingsTableTest.java index d9aabcb054..e52ec022ed 100644 --- a/test/unit/org/apache/cassandra/db/virtual/SettingsTableTest.java +++ b/test/unit/org/apache/cassandra/db/virtual/SettingsTableTest.java @@ -37,6 +37,8 @@ import org.apache.cassandra.cql3.CQLTester; import org.apache.cassandra.security.SSLFactory; import org.yaml.snakeyaml.introspector.Property; +import static org.apache.cassandra.config.EncryptionOptions.ClientAuth.REQUIRED; + public class SettingsTableTest extends CQLTester { private static final String KS_NAME = "vts"; @@ -201,7 +203,7 @@ public class SettingsTableTest extends CQLTester // name doesn't match yaml check(pre + "client_auth", "false"); - config.server_encryption_options = config.server_encryption_options.withRequireClientAuth(true); + config.server_encryption_options = config.server_encryption_options.withRequireClientAuth(REQUIRED); check(pre + "client_auth", "true"); // name doesn't match yaml diff --git a/test/unit/org/apache/cassandra/net/ConnectionTest.java b/test/unit/org/apache/cassandra/net/ConnectionTest.java index ff11f9dbb0..1f8c6f612d 100644 --- a/test/unit/org/apache/cassandra/net/ConnectionTest.java +++ b/test/unit/org/apache/cassandra/net/ConnectionTest.java @@ -71,6 +71,7 @@ import org.apache.cassandra.utils.FBUtilities; import static java.util.concurrent.TimeUnit.MILLISECONDS; import static java.util.concurrent.TimeUnit.MINUTES; import static java.util.concurrent.TimeUnit.SECONDS; +import static org.apache.cassandra.config.EncryptionOptions.ClientAuth.NOT_REQUIRED; import static org.apache.cassandra.net.MessagingService.VERSION_40; import static org.apache.cassandra.net.NoPayload.noPayload; import static org.apache.cassandra.net.MessagingService.current_version; @@ -182,7 +183,7 @@ public class ConnectionTest .withKeyStorePassword("cassandra") .withTrustStore("test/conf/cassandra_ssl_test.truststore") .withTrustStorePassword("cassandra") - .withRequireClientAuth(false) + .withRequireClientAuth(NOT_REQUIRED) .withCipherSuites("TLS_RSA_WITH_AES_128_CBC_SHA"); static final List> MODIFIERS = ImmutableList.of( diff --git a/test/unit/org/apache/cassandra/net/HandshakeTest.java b/test/unit/org/apache/cassandra/net/HandshakeTest.java index f2fa960566..ec7e26b322 100644 --- a/test/unit/org/apache/cassandra/net/HandshakeTest.java +++ b/test/unit/org/apache/cassandra/net/HandshakeTest.java @@ -49,6 +49,8 @@ import org.apache.cassandra.security.DefaultSslContextFactory; import org.apache.cassandra.utils.concurrent.AsyncPromise; import static org.apache.cassandra.net.ConnectionType.SMALL_MESSAGES; +import static org.apache.cassandra.config.EncryptionOptions.ClientAuth.NOT_REQUIRED; +import static org.apache.cassandra.config.EncryptionOptions.ClientAuth.REQUIRED; import static org.apache.cassandra.net.MessagingService.current_version; import static org.apache.cassandra.net.MessagingService.minimum_version; import static org.apache.cassandra.net.OutboundConnectionInitiator.Result; @@ -285,12 +287,12 @@ public class HandshakeTest if (sslConnectionType == SslFallbackConnectionType.MTLS) { serverEncryptionOptions = serverEncryptionOptions.withInternodeEncryption(ServerEncryptionOptions.InternodeEncryption.all) - .withRequireClientAuth(true); + .withRequireClientAuth(REQUIRED); } else if (sslConnectionType == SslFallbackConnectionType.SSL) { serverEncryptionOptions = serverEncryptionOptions.withInternodeEncryption(ServerEncryptionOptions.InternodeEncryption.all) - .withRequireClientAuth(false); + .withRequireClientAuth(NOT_REQUIRED); } return serverEncryptionOptions; } diff --git a/test/unit/org/apache/cassandra/security/DefaultSslContextFactoryTest.java b/test/unit/org/apache/cassandra/security/DefaultSslContextFactoryTest.java index c169b9e775..1b7db326e1 100644 --- a/test/unit/org/apache/cassandra/security/DefaultSslContextFactoryTest.java +++ b/test/unit/org/apache/cassandra/security/DefaultSslContextFactoryTest.java @@ -37,6 +37,9 @@ import org.apache.cassandra.distributed.shared.WithProperties; import static org.apache.cassandra.config.CassandraRelevantProperties.DISABLE_TCACTIVE_OPENSSL; +import static org.apache.cassandra.config.EncryptionOptions.ClientAuth.NOT_REQUIRED; +import static org.apache.cassandra.config.EncryptionOptions.ClientAuth.REQUIRED; + public class DefaultSslContextFactoryTest { private Map commonConfig = new HashMap<>(); @@ -46,7 +49,7 @@ public class DefaultSslContextFactoryTest { commonConfig.put("truststore", "test/conf/cassandra_ssl_test.truststore"); commonConfig.put("truststore_password", "cassandra"); - commonConfig.put("require_client_auth", Boolean.FALSE); + commonConfig.put("require_client_auth", "false"); commonConfig.put("cipher_suites", Arrays.asList("TLS_RSA_WITH_AES_128_CBC_SHA")); } @@ -71,9 +74,9 @@ public class DefaultSslContextFactoryTest .withKeyStorePassword("cassandra") .withOutboundKeystore("test/conf/cassandra_ssl_test_outbound.keystore") .withOutboundKeystorePassword("cassandra") - .withRequireClientAuth(false) + .withRequireClientAuth(NOT_REQUIRED) .withCipherSuites("TLS_RSA_WITH_AES_128_CBC_SHA"); - SslContext sslContext = SSLFactory.getOrCreateSslContext(options, true, ISslContextFactory.SocketType.CLIENT, "test"); + SslContext sslContext = SSLFactory.getOrCreateSslContext(options, REQUIRED, ISslContextFactory.SocketType.CLIENT, "test"); Assert.assertNotNull(sslContext); if (OpenSsl.isAvailable()) Assert.assertTrue(sslContext instanceof OpenSslContext); diff --git a/test/unit/org/apache/cassandra/security/DummySslContextFactoryImpl.java b/test/unit/org/apache/cassandra/security/DummySslContextFactoryImpl.java index 3a14ff2937..ca4f4e86f0 100644 --- a/test/unit/org/apache/cassandra/security/DummySslContextFactoryImpl.java +++ b/test/unit/org/apache/cassandra/security/DummySslContextFactoryImpl.java @@ -25,6 +25,7 @@ import javax.net.ssl.SSLException; import io.netty.handler.ssl.CipherSuiteFilter; import io.netty.handler.ssl.SslContext; +import org.apache.cassandra.config.EncryptionOptions; /** * TEST ONLY Class. DON'T use it for anything else. @@ -43,7 +44,19 @@ public class DummySslContextFactoryImpl implements ISslContextFactory } @Override - public SslContext createNettySslContext(boolean verifyPeerCertificate, SocketType socketType, + public SSLContext createJSSESslContext(EncryptionOptions.ClientAuth clientAuth) throws SSLException + { + return null; + } + + @Override + public SslContext createNettySslContext(boolean verifyPeerCertificate, SocketType socketType, CipherSuiteFilter cipherFilter) throws SSLException + { + return null; + } + + @Override + public SslContext createNettySslContext(EncryptionOptions.ClientAuth clientAuth, SocketType socketType, CipherSuiteFilter cipherFilter) throws SSLException { return null; diff --git a/test/unit/org/apache/cassandra/security/FileBasedSslContextFactoryTest.java b/test/unit/org/apache/cassandra/security/FileBasedSslContextFactoryTest.java index 98e0342797..73fe80b304 100644 --- a/test/unit/org/apache/cassandra/security/FileBasedSslContextFactoryTest.java +++ b/test/unit/org/apache/cassandra/security/FileBasedSslContextFactoryTest.java @@ -36,6 +36,8 @@ import org.apache.cassandra.distributed.shared.WithProperties; import static org.apache.cassandra.config.CassandraRelevantProperties.CASSANDRA_CONFIG; +import static org.apache.cassandra.config.EncryptionOptions.ClientAuth.NOT_REQUIRED; + public class FileBasedSslContextFactoryTest { private static final Logger logger = LoggerFactory.getLogger(FileBasedSslContextFactoryTest.class); @@ -65,7 +67,7 @@ public class FileBasedSslContextFactoryTest new HashMap<>())) .withTrustStore("test/conf/cassandra_ssl_test.truststore") .withTrustStorePassword("cassandra") - .withRequireClientAuth(false) + .withRequireClientAuth(NOT_REQUIRED) .withCipherSuites("TLS_RSA_WITH_AES_128_CBC_SHA") .withKeyStore("test/conf/cassandra_ssl_test.keystore") .withKeyStorePassword("cassandra") diff --git a/test/unit/org/apache/cassandra/security/PEMBasedSslContextFactoryTest.java b/test/unit/org/apache/cassandra/security/PEMBasedSslContextFactoryTest.java index a1fb37c1b9..ca319de5fd 100644 --- a/test/unit/org/apache/cassandra/security/PEMBasedSslContextFactoryTest.java +++ b/test/unit/org/apache/cassandra/security/PEMBasedSslContextFactoryTest.java @@ -38,6 +38,8 @@ import org.apache.cassandra.config.ParameterizedClass; import org.apache.cassandra.distributed.shared.WithProperties; import static org.apache.cassandra.config.CassandraRelevantProperties.DISABLE_TCACTIVE_OPENSSL; +import static org.apache.cassandra.config.EncryptionOptions.ClientAuth.NOT_REQUIRED; +import static org.apache.cassandra.config.EncryptionOptions.ClientAuth.REQUIRED; import static org.apache.cassandra.security.PEMBasedSslContextFactory.ConfigKey.ENCODED_CERTIFICATES; import static org.apache.cassandra.security.PEMBasedSslContextFactory.ConfigKey.ENCODED_KEY; import static org.apache.cassandra.security.PEMBasedSslContextFactory.ConfigKey.KEY_PASSWORD; @@ -176,7 +178,7 @@ public class PEMBasedSslContextFactoryTest public void setup() { commonConfig.put(ENCODED_CERTIFICATES.getKeyName(), trusted_certificates); - commonConfig.put("require_client_auth", Boolean.FALSE); + commonConfig.put("require_client_auth", "false"); commonConfig.put("cipher_suites", Arrays.asList("TLS_RSA_WITH_AES_128_CBC_SHA")); } @@ -215,10 +217,10 @@ public class PEMBasedSslContextFactoryTest EncryptionOptions options = new EncryptionOptions().withTrustStore("test/conf/cassandra_ssl_test.truststore.pem") .withKeyStore("test/conf/cassandra_ssl_test.keystore.pem") .withKeyStorePassword("cassandra") - .withRequireClientAuth(false) + .withRequireClientAuth(NOT_REQUIRED) .withCipherSuites("TLS_RSA_WITH_AES_128_CBC_SHA") .withSslContextFactory(sslContextFactory); - SslContext sslContext = SSLFactory.getOrCreateSslContext(options, true, ISslContextFactory.SocketType.SERVER, "test"); + SslContext sslContext = SSLFactory.getOrCreateSslContext(options, REQUIRED, ISslContextFactory.SocketType.SERVER, "test"); Assert.assertNotNull(sslContext); if (OpenSsl.isAvailable()) Assert.assertTrue(sslContext instanceof OpenSslContext); @@ -236,10 +238,10 @@ public class PEMBasedSslContextFactoryTest .withKeyStorePassword("cassandra") .withOutboundKeystore("test/conf/cassandra_ssl_test.keystore.pem") .withOutboundKeystorePassword("cassandra") - .withRequireClientAuth(false) + .withRequireClientAuth(NOT_REQUIRED) .withCipherSuites("TLS_RSA_WITH_AES_128_CBC_SHA") .withSslContextFactory(sslContextFactory); - SslContext sslContext = SSLFactory.getOrCreateSslContext(options, true, ISslContextFactory.SocketType.CLIENT, "test"); + SslContext sslContext = SSLFactory.getOrCreateSslContext(options, REQUIRED, ISslContextFactory.SocketType.CLIENT, "test"); Assert.assertNotNull(sslContext); if (OpenSsl.isAvailable()) Assert.assertTrue(sslContext instanceof OpenSslContext); diff --git a/test/unit/org/apache/cassandra/security/SSLFactoryTest.java b/test/unit/org/apache/cassandra/security/SSLFactoryTest.java index b6e0b8e4b5..a7c14dfcfb 100644 --- a/test/unit/org/apache/cassandra/security/SSLFactoryTest.java +++ b/test/unit/org/apache/cassandra/security/SSLFactoryTest.java @@ -51,6 +51,8 @@ import org.apache.cassandra.config.EncryptionOptions.ServerEncryptionOptions; import org.apache.cassandra.config.ParameterizedClass; import org.apache.cassandra.io.util.File; +import static org.apache.cassandra.config.EncryptionOptions.ClientAuth.NOT_REQUIRED; +import static org.apache.cassandra.config.EncryptionOptions.ClientAuth.REQUIRED; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertNotNull; @@ -81,7 +83,7 @@ public class SSLFactoryTest encryptionOptions = new ServerEncryptionOptions() .withTrustStore("test/conf/cassandra_ssl_test.truststore") .withTrustStorePassword("cassandra") - .withRequireClientAuth(false) + .withRequireClientAuth(NOT_REQUIRED) .withCipherSuites("TLS_RSA_WITH_AES_128_CBC_SHA") .withSslContextFactory(new ParameterizedClass(TestFileBasedSSLContextFactory.class.getName(), new HashMap<>())); @@ -118,8 +120,8 @@ public class SSLFactoryTest options.sslContextFactoryInstance.initHotReloading(); legacyOptions.sslContextFactoryInstance.initHotReloading(); - SslContext oldCtx = SSLFactory.getOrCreateSslContext(options, true, ISslContextFactory.SocketType.CLIENT, "test"); - SslContext oldLegacyCtx = SSLFactory.getOrCreateSslContext(legacyOptions, true, ISslContextFactory.SocketType.CLIENT, "test legacy"); + SslContext oldCtx = SSLFactory.getOrCreateSslContext(options, REQUIRED, ISslContextFactory.SocketType.CLIENT, "test"); + SslContext oldLegacyCtx = SSLFactory.getOrCreateSslContext(legacyOptions, REQUIRED, ISslContextFactory.SocketType.CLIENT, "test legacy"); File keystoreFile = new File(options.keystore); SSLFactory.checkCertFilesForHotReloading(); @@ -127,8 +129,8 @@ public class SSLFactoryTest keystoreFile.trySetLastModified(System.currentTimeMillis() + 15000); SSLFactory.checkCertFilesForHotReloading(); - SslContext newCtx = SSLFactory.getOrCreateSslContext(options, true, ISslContextFactory.SocketType.CLIENT, "test"); - SslContext newLegacyCtx = SSLFactory.getOrCreateSslContext(legacyOptions, true, ISslContextFactory.SocketType.CLIENT, "test legacy"); + SslContext newCtx = SSLFactory.getOrCreateSslContext(options, REQUIRED, ISslContextFactory.SocketType.CLIENT, "test"); + SslContext newLegacyCtx = SSLFactory.getOrCreateSslContext(legacyOptions, REQUIRED, ISslContextFactory.SocketType.CLIENT, "test legacy"); Assert.assertNotSame(oldCtx, newCtx); Assert.assertNotSame(oldLegacyCtx, newLegacyCtx); @@ -151,7 +153,7 @@ public class SSLFactoryTest .withOutboundKeystorePassword("dummyPassword"); // Server socket type should create a keystore with keystore & keystore password - final OpenSslServerContext context = (OpenSslServerContext) SSLFactory.createNettySslContext(options, true, ISslContextFactory.SocketType.SERVER); + final OpenSslServerContext context = (OpenSslServerContext) SSLFactory.createNettySslContext(options, REQUIRED, ISslContextFactory.SocketType.SERVER); assertNotNull(context); // Verify if right certificate is loaded into SslContext @@ -168,7 +170,7 @@ public class SSLFactoryTest .withKeyStorePassword("dummyPassword"); // Client socket type should create a keystore with outbound Keystore & outbound password - final OpenSslClientContext context = (OpenSslClientContext) SSLFactory.createNettySslContext(options, true, ISslContextFactory.SocketType.CLIENT); + final OpenSslClientContext context = (OpenSslClientContext) SSLFactory.createNettySslContext(options, REQUIRED, ISslContextFactory.SocketType.CLIENT); assertNotNull(context); // Verify if right certificate is loaded into SslContext @@ -189,8 +191,8 @@ public class SSLFactoryTest options.sslContextFactoryInstance.initHotReloading(); legacyOptions.sslContextFactoryInstance.initHotReloading(); - SslContext oldCtx = SSLFactory.getOrCreateSslContext(options, true, ISslContextFactory.SocketType.CLIENT, "test"); - SslContext oldLegacyCtx = SSLFactory.getOrCreateSslContext(legacyOptions, true, ISslContextFactory.SocketType.CLIENT, "test legacy"); + SslContext oldCtx = SSLFactory.getOrCreateSslContext(options, REQUIRED, ISslContextFactory.SocketType.CLIENT, "test"); + SslContext oldLegacyCtx = SSLFactory.getOrCreateSslContext(legacyOptions, REQUIRED, ISslContextFactory.SocketType.CLIENT, "test legacy"); File keystoreFile = new File(options.keystore); SSLFactory.checkCertFilesForHotReloading(); @@ -198,8 +200,8 @@ public class SSLFactoryTest keystoreFile.trySetLastModified(System.currentTimeMillis() + 15000); SSLFactory.checkCertFilesForHotReloading(); - SslContext newCtx = SSLFactory.getOrCreateSslContext(options, true, ISslContextFactory.SocketType.CLIENT, "test"); - SslContext newLegacyCtx = SSLFactory.getOrCreateSslContext(legacyOptions, true, ISslContextFactory.SocketType.CLIENT, "test legacy"); + SslContext newCtx = SSLFactory.getOrCreateSslContext(options, REQUIRED, ISslContextFactory.SocketType.CLIENT, "test"); + SslContext newLegacyCtx = SSLFactory.getOrCreateSslContext(legacyOptions, REQUIRED, ISslContextFactory.SocketType.CLIENT, "test legacy"); Assert.assertNotSame(oldCtx, newCtx); Assert.assertNotSame(oldLegacyCtx, newLegacyCtx); @@ -221,7 +223,7 @@ public class SSLFactoryTest .withKeyStorePassword("bad password") .withInternodeEncryption(ServerEncryptionOptions.InternodeEncryption.all); - SSLFactory.validateSslContext("testSslFactorySslInit_BadPassword_ThrowsException", options, false, true); + SSLFactory.validateSslContext("testSslFactorySslInit_BadPassword_ThrowsException", options, NOT_REQUIRED, true); } @Test @@ -239,14 +241,14 @@ public class SSLFactoryTest SSLFactory.initHotReloading(options, options, true); // deliberately not initializing with legacyOptions to match InboundSockets.addBindings - SslContext oldCtx = SSLFactory.getOrCreateSslContext(options, true, ISslContextFactory.SocketType.CLIENT, "test"); - SslContext oldLegacyCtx = SSLFactory.getOrCreateSslContext(legacyOptions, true, ISslContextFactory.SocketType.CLIENT, "test legacy"); + SslContext oldCtx = SSLFactory.getOrCreateSslContext(options, REQUIRED, ISslContextFactory.SocketType.CLIENT, "test"); + SslContext oldLegacyCtx = SSLFactory.getOrCreateSslContext(legacyOptions, REQUIRED, ISslContextFactory.SocketType.CLIENT, "test legacy"); changeKeystorePassword(options.keystore, options.keystore_password, "bad password"); SSLFactory.checkCertFilesForHotReloading(); - SslContext newCtx = SSLFactory.getOrCreateSslContext(options, true, ISslContextFactory.SocketType.CLIENT, "test"); - SslContext newLegacyCtx = SSLFactory.getOrCreateSslContext(legacyOptions, true, ISslContextFactory.SocketType.CLIENT, "test legacy"); + SslContext newCtx = SSLFactory.getOrCreateSslContext(options, REQUIRED, ISslContextFactory.SocketType.CLIENT, "test"); + SslContext newLegacyCtx = SSLFactory.getOrCreateSslContext(legacyOptions, REQUIRED, ISslContextFactory.SocketType.CLIENT, "test legacy"); Assert.assertSame(oldCtx, newCtx); Assert.assertSame(oldLegacyCtx, newLegacyCtx); @@ -270,14 +272,14 @@ public class SSLFactoryTest SSLFactory.initHotReloading(options, options, true); - SslContext oldCtx = SSLFactory.getOrCreateSslContext(options, true, ISslContextFactory.SocketType.CLIENT, "test"); + SslContext oldCtx = SSLFactory.getOrCreateSslContext(options, REQUIRED, ISslContextFactory.SocketType.CLIENT, "test"); SSLFactory.checkCertFilesForHotReloading(); testKeystoreFile.trySetLastModified(System.currentTimeMillis() + 15000); FileUtils.forceDelete(testKeystoreFile.toJavaIOFile()); SSLFactory.checkCertFilesForHotReloading(); - SslContext newCtx = SSLFactory.getOrCreateSslContext(options, true, ISslContextFactory.SocketType.CLIENT, "test"); + SslContext newCtx = SSLFactory.getOrCreateSslContext(options, REQUIRED, ISslContextFactory.SocketType.CLIENT, "test"); Assert.assertSame(oldCtx, newCtx); } @@ -298,7 +300,7 @@ public class SSLFactoryTest ServerEncryptionOptions options = addKeystoreOptions(encryptionOptions) .withCipherSuites("TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256"); - SslContext ctx1 = SSLFactory.getOrCreateSslContext(options, true, + SslContext ctx1 = SSLFactory.getOrCreateSslContext(options, REQUIRED, ISslContextFactory.SocketType.SERVER, "test"); Assert.assertTrue(ctx1.isServer()); @@ -306,7 +308,7 @@ public class SSLFactoryTest options = options.withCipherSuites("TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256"); - SslContext ctx2 = SSLFactory.getOrCreateSslContext(options, true, + SslContext ctx2 = SSLFactory.getOrCreateSslContext(options, REQUIRED, ISslContextFactory.SocketType.CLIENT, "test"); Assert.assertTrue(ctx2.isClient()); @@ -323,7 +325,7 @@ public class SSLFactoryTest new EncryptionOptions() .withSslContextFactory(new ParameterizedClass(DummySslContextFactoryImpl.class.getName(), parameters1)) .withProtocol("TLSv1.1") - .withRequireClientAuth(true) + .withRequireClientAuth(REQUIRED) .withRequireEndpointVerification(false); SSLFactory.CacheKey cacheKey1 = new SSLFactory.CacheKey(encryptionOptions1, ISslContextFactory.SocketType.SERVER, "test" @@ -336,7 +338,7 @@ public class SSLFactoryTest new EncryptionOptions() .withSslContextFactory(new ParameterizedClass(DummySslContextFactoryImpl.class.getName(), parameters2)) .withProtocol("TLSv1.1") - .withRequireClientAuth(true) + .withRequireClientAuth(REQUIRED) .withRequireEndpointVerification(false); SSLFactory.CacheKey cacheKey2 = new SSLFactory.CacheKey(encryptionOptions2, ISslContextFactory.SocketType.SERVER, "test" diff --git a/tools/stress/src/org/apache/cassandra/stress/util/JavaDriverClient.java b/tools/stress/src/org/apache/cassandra/stress/util/JavaDriverClient.java index f938cd9887..3d72828daf 100644 --- a/tools/stress/src/org/apache/cassandra/stress/util/JavaDriverClient.java +++ b/tools/stress/src/org/apache/cassandra/stress/util/JavaDriverClient.java @@ -41,6 +41,8 @@ import org.apache.cassandra.config.EncryptionOptions; import org.apache.cassandra.security.SSLFactory; import org.apache.cassandra.stress.settings.StressSettings; +import static org.apache.cassandra.config.EncryptionOptions.ClientAuth.REQUIRED; + public class JavaDriverClient { @@ -161,7 +163,7 @@ public class JavaDriverClient if (encryptionOptions.getEnabled()) { SSLContext sslContext; - sslContext = SSLFactory.createSSLContext(encryptionOptions, true); + sslContext = SSLFactory.createSSLContext(encryptionOptions, REQUIRED); // Temporarily override newSSLEngine to set accepted protocols until it is added to // RemoteEndpointAwareJdkSSLOptions. See CASSANDRA-13325 and CASSANDRA-16362.