mirror of https://github.com/apache/cassandra
CEP-9 make SSLContext creation pluggable
patch by Maulin Vasavada; reviewed by Jon Meredith, Stefan Miklosovic and Berenguer Blasi for CASSANDRA-16666
This commit is contained in:
parent
c3c2c7efec
commit
24dcc280c2
|
|
@ -1,4 +1,5 @@
|
||||||
4.1
|
4.1
|
||||||
|
* Make SSLContext creation pluggable/extensible (CASSANDRA-16666)
|
||||||
* Add soft/hard limits to local reads to protect against reading too much data in a single query (CASSANDRA-16896)
|
* Add soft/hard limits to local reads to protect against reading too much data in a single query (CASSANDRA-16896)
|
||||||
* Avoid token cache invalidation for removing a non-member node (CASSANDRA-15290)
|
* Avoid token cache invalidation for removing a non-member node (CASSANDRA-15290)
|
||||||
* Allow configuration of consistency levels on auth operations (CASSANDRA-12988)
|
* Allow configuration of consistency levels on auth operations (CASSANDRA-12988)
|
||||||
|
|
|
||||||
|
|
@ -88,6 +88,33 @@ As an alternative to the ``optional`` setting, separate ports can also be config
|
||||||
where operational requirements demand it. To do so, set ``optional`` to false and use the ``native_transport_port_ssl``
|
where operational requirements demand it. To do so, set ``optional`` to false and use the ``native_transport_port_ssl``
|
||||||
setting in ``cassandra.yaml`` to specify the port to be used for secure client communication.
|
setting in ``cassandra.yaml`` to specify the port to be used for secure client communication.
|
||||||
|
|
||||||
|
.. _customizing-ssl-context:
|
||||||
|
|
||||||
|
Customizing SSL Context Creation
|
||||||
|
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||||
|
|
||||||
|
There are situations when the current file-based configurations for keystore/truststore and passwords are not enough and
|
||||||
|
you want to customize how SSL Context is created to your specific needs. In that case, you can create a custom implementation
|
||||||
|
of Cassandra's ``org.apache.cassandra.security.ISslContextFactory`` interface and configure ``cassandra.yaml`` to use it.
|
||||||
|
While the ``ISslContextFactory`` provides full control of building a SSL Context, you can extend
|
||||||
|
``org.apache.cassandra.security.AbstractSslContextFactory`` or ``org.apache.cassandra.security.FileBasedSslContextFactory``
|
||||||
|
to keep your custom implementation to the bare minimum. You can find an example of such a customization
|
||||||
|
in ``examples`` directory for Kubernetes in ``KubernetesSecretsSslContextFactory.java``.
|
||||||
|
|
||||||
|
Below is the example to customize internode ssl configuration with ``YourCassandraSslContextFactory``. The same way you
|
||||||
|
can customize the client-to-node encryption.
|
||||||
|
|
||||||
|
::
|
||||||
|
|
||||||
|
server_encryption_options:
|
||||||
|
ssl_context_factory:
|
||||||
|
class_name: com.your-company.YourCassandraSslContextFactory
|
||||||
|
parameters:
|
||||||
|
key1: "value1"
|
||||||
|
key2: "value2"
|
||||||
|
key3: "value3"
|
||||||
|
internode_encryption: none
|
||||||
|
|
||||||
.. _operation-roles:
|
.. _operation-roles:
|
||||||
|
|
||||||
Roles
|
Roles
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,19 @@
|
||||||
|
Cassandra Custom SslContextFactory Example:
|
||||||
|
==========================================
|
||||||
|
|
||||||
|
Example-1: Custom SslContextFactory implementation based on Kubernetes secrets
|
||||||
|
------------------------------------------------------------------------------
|
||||||
|
For the documentation please refer to the javadocs for the K8SecretsSslContextFactory.java.
|
||||||
|
|
||||||
|
|
||||||
|
Installation:
|
||||||
|
============
|
||||||
|
Step 1: Build the Cassandra classes locally
|
||||||
|
|
||||||
|
change directory to <cassandra_src_dir>
|
||||||
|
run "ant build"
|
||||||
|
|
||||||
|
Step 2: Run tests for the security examples
|
||||||
|
|
||||||
|
change directory to <cassandra_src_dir>/examples/ssl-factory
|
||||||
|
run "ant test"
|
||||||
|
|
@ -0,0 +1,100 @@
|
||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<!--
|
||||||
|
~ 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.
|
||||||
|
-->
|
||||||
|
|
||||||
|
<project default="jar" name="ssl-factory-example">
|
||||||
|
<property name="cassandra.dir" value="../.." />
|
||||||
|
<property name="cassandra.dir.lib" value="${cassandra.dir}/lib" />
|
||||||
|
<property name="cassandra.classes" value="${cassandra.dir}/build/classes/main" />
|
||||||
|
<property name="cassandra.test.lib" value="${cassandra.dir}/build/test/lib" />
|
||||||
|
<property name="build.src" value="${basedir}/src" />
|
||||||
|
<property name="build.dir" value="${basedir}/build" />
|
||||||
|
<property name="conf.dir" value="${basedir}/conf" />
|
||||||
|
<property name="build.classes" value="${build.dir}/classes" />
|
||||||
|
<property name="test.src" value="${basedir}/test/unit" />
|
||||||
|
<property name="test.build.dir" value="${build.dir}/test" />
|
||||||
|
<property name="test.conf.dir" value="${test.src}/conf" />
|
||||||
|
<property name="test.build.classes" value="${test.build.dir}/classes" />
|
||||||
|
<property name="test.build.conf" value="${test.build.dir}/conf" />
|
||||||
|
<property name="final.name" value="ssl-factory-example" />
|
||||||
|
|
||||||
|
<path id="build.classpath">
|
||||||
|
<fileset dir="${cassandra.dir.lib}">
|
||||||
|
<include name="**/*.jar" />
|
||||||
|
</fileset>
|
||||||
|
<fileset dir="${cassandra.dir}/build/lib/jars">
|
||||||
|
<include name="**/*.jar" />
|
||||||
|
</fileset>
|
||||||
|
<pathelement location="${cassandra.classes}" />
|
||||||
|
</path>
|
||||||
|
|
||||||
|
<path id="test.classpath">
|
||||||
|
<fileset dir="${cassandra.test.lib}/jars">
|
||||||
|
<include name="**/*.jar" />
|
||||||
|
<exclude name="**/ant-*.jar"/>
|
||||||
|
</fileset>
|
||||||
|
<path refid="build.classpath"/>
|
||||||
|
<path location="${build.dir}/${final.name}.jar"/>
|
||||||
|
<pathelement location="${test.build.classes}"/>
|
||||||
|
<pathelement location="${test.build.conf}"/>
|
||||||
|
</path>
|
||||||
|
|
||||||
|
<target name="init">
|
||||||
|
<mkdir dir="${build.classes}" />
|
||||||
|
<mkdir dir="${test.build.classes}" />
|
||||||
|
</target>
|
||||||
|
|
||||||
|
<target name="build" depends="init">
|
||||||
|
<javac destdir="${build.classes}" debug="true" includeantruntime="false">
|
||||||
|
<src path="${build.src}" />
|
||||||
|
<classpath refid="build.classpath" />
|
||||||
|
</javac>
|
||||||
|
</target>
|
||||||
|
|
||||||
|
<target name="jar" depends="build">
|
||||||
|
<jar jarfile="${build.dir}/${final.name}.jar">
|
||||||
|
<fileset dir="${build.classes}" />
|
||||||
|
</jar>
|
||||||
|
</target>
|
||||||
|
|
||||||
|
<target name="buildTests" depends="init">
|
||||||
|
<deltree dir="${test.build.dir}/conf"/>
|
||||||
|
<copydir src="test/conf" dest="${test.build.dir}/conf"/>
|
||||||
|
<javac destdir="${test.build.classes}" debug="true" includeantruntime="false">
|
||||||
|
<src path="${test.src}" />
|
||||||
|
<src path="${test.build.dir}/conf" />
|
||||||
|
<classpath refid="test.classpath"/>
|
||||||
|
</javac>
|
||||||
|
</target>
|
||||||
|
|
||||||
|
<target name="test" depends="jar, buildTests">
|
||||||
|
<junit printsummary="on" haltonfailure="yes" fork="true">
|
||||||
|
<classpath refid="test.classpath"/>
|
||||||
|
<formatter type="brief" usefile="false" />
|
||||||
|
<batchtest>
|
||||||
|
<fileset dir="${test.src}" includes="**/*Test.java" />
|
||||||
|
</batchtest>
|
||||||
|
</junit>
|
||||||
|
</target>
|
||||||
|
|
||||||
|
<target name="clean">
|
||||||
|
<delete dir="${build.dir}" />
|
||||||
|
<delete dir="${test.build.classes}" />
|
||||||
|
</target>
|
||||||
|
</project>
|
||||||
|
|
@ -0,0 +1,287 @@
|
||||||
|
/*
|
||||||
|
* 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.security;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.nio.file.Files;
|
||||||
|
import java.nio.file.Paths;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.Optional;
|
||||||
|
|
||||||
|
import com.google.common.annotations.VisibleForTesting;
|
||||||
|
import org.apache.commons.lang3.StringUtils;
|
||||||
|
import org.slf4j.Logger;
|
||||||
|
import org.slf4j.LoggerFactory;
|
||||||
|
|
||||||
|
import org.apache.cassandra.config.EncryptionOptions;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Custom {@link ISslContextFactory} implementation based on Kubernetes Secrets. It allows the keystore and
|
||||||
|
* truststore paths to be configured from the K8 secrets via volumeMount and passwords via K8 secrets environment
|
||||||
|
* variables. The official Kubernetes Secret Spec can be found <a href="https://kubernetes.io/docs/concepts/configuration/secret/ ">here</a>.
|
||||||
|
*
|
||||||
|
* When keystore or truststore is updated, this implementation can detect that based on updated K8 secrets
|
||||||
|
* at the mounted paths ({@code KEYSTORE_UPDATED_TIMESTAMP_PATH} for the keystore and {@code
|
||||||
|
* TRUSTSTORE_UPDATED_TIMESTAMP_PATH} for the truststore. The values in those paths are expected to be numeric values.
|
||||||
|
* The most obvious choice might be to just use the time in nano/milli-seconds precision but any other strategy would work
|
||||||
|
* as well, as far as the comparison of those values can be done in a consistent/predictable manner. Again, those
|
||||||
|
* values do not have to necessarily reflect actual file's update timestamps, using the actual file's timestamps is
|
||||||
|
* just one of the valid options to signal updates.
|
||||||
|
*
|
||||||
|
* Defaults:
|
||||||
|
* <pre>
|
||||||
|
* keystore path = /etc/my-ssl-store/keystore
|
||||||
|
* keystore password = cassandra
|
||||||
|
* keystore updated timestamp path = /etc/my-ssl-store/keystore-last-updatedtime
|
||||||
|
* truststore path = /etc/my-ssl-store/truststore
|
||||||
|
* truststore password = cassandra
|
||||||
|
* truststore updated timestamp path = /etc/my-ssl-store/truststore-last-updatedtime
|
||||||
|
* </pre>
|
||||||
|
*
|
||||||
|
* Customization: In order to customize the K8s secret configuration, override appropriate values in the below Cassandra
|
||||||
|
* configuration. The similar configuration can be applied to {@code client_encryption_options}.
|
||||||
|
* <pre>
|
||||||
|
* server_encryption_options:
|
||||||
|
* internode_encryption: none
|
||||||
|
* ssl_context_factory:
|
||||||
|
* class_name: org.apache.cassandra.security.KubernetesSecretsSslContextFactory
|
||||||
|
* parameters:
|
||||||
|
* KEYSTORE_PASSWORD_ENV_VAR: KEYSTORE_PASSWORD
|
||||||
|
* KEYSTORE_UPDATED_TIMESTAMP_PATH: /etc/my-ssl-store/keystore-last-updatedtime
|
||||||
|
* TRUSTSTORE_PASSWORD_ENV_VAR: TRUSTSTORE_PASSWORD
|
||||||
|
* TRUSTSTORE_UPDATED_TIMESTAMP_PATH: /etc/my-ssl-store/truststore-last-updatedtime
|
||||||
|
* keystore: /etc/my-ssl-store/keystore
|
||||||
|
* truststore: /etc/my-ssl-store/truststore
|
||||||
|
* </pre>
|
||||||
|
*
|
||||||
|
* Below is the corresponding sample YAML configuration for K8 env.
|
||||||
|
* <pre>
|
||||||
|
* apiVersion: v1
|
||||||
|
* kind: Pod
|
||||||
|
* metadata:
|
||||||
|
* name: my-pod
|
||||||
|
* labels:
|
||||||
|
* app: my-app
|
||||||
|
* spec:
|
||||||
|
* containers:
|
||||||
|
* - name: my-app
|
||||||
|
* image: my-app:latest
|
||||||
|
* imagePullPolicy: Always
|
||||||
|
* env:
|
||||||
|
* - name: KEYSTORE_PASSWORD
|
||||||
|
* valueFrom:
|
||||||
|
* secretKeyRef:
|
||||||
|
* name: my-ssl-store
|
||||||
|
* key: keystore-password
|
||||||
|
* - name: TRUSTSTORE_PASSWORD
|
||||||
|
* valueFrom:
|
||||||
|
* secretKeyRef:
|
||||||
|
* name: my-ssl-store
|
||||||
|
* key: truststore-password
|
||||||
|
* volumeMounts:
|
||||||
|
* - name: my-ssl-store
|
||||||
|
* mountPath: "/etc/my-ssl-store"
|
||||||
|
* readOnly: true
|
||||||
|
* volumes:
|
||||||
|
* - name: my-ssl-store
|
||||||
|
* secret:
|
||||||
|
* secretName: my-ssl-store
|
||||||
|
* items:
|
||||||
|
* - key: cassandra_ssl_keystore
|
||||||
|
* path: keystore
|
||||||
|
* - key: keystore-last-updatedtime
|
||||||
|
* path: keystore-last-updatedtime
|
||||||
|
* - key: cassandra_ssl_truststore
|
||||||
|
* path: truststore
|
||||||
|
* - key: truststore-last-updatedtime
|
||||||
|
* path: truststore-last-updatedtime
|
||||||
|
* </pre>
|
||||||
|
*/
|
||||||
|
public class KubernetesSecretsSslContextFactory extends FileBasedSslContextFactory
|
||||||
|
{
|
||||||
|
private static final Logger logger = LoggerFactory.getLogger(KubernetesSecretsSslContextFactory.class);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Use below config-keys to configure this factory.
|
||||||
|
*/
|
||||||
|
public interface ConfigKeys {
|
||||||
|
String KEYSTORE_PASSWORD_ENV_VAR = "KEYSTORE_PASSWORD_ENV_VAR";
|
||||||
|
String TRUSTSTORE_PASSWORD_ENV_VAR = "TRUSTSTORE_PASSWORD_ENV_VAR";
|
||||||
|
String KEYSTORE_UPDATED_TIMESTAMP_PATH = "KEYSTORE_UPDATED_TIMESTAMP_PATH";
|
||||||
|
String TRUSTSTORE_UPDATED_TIMESTAMP_PATH = "TRUSTSTORE_UPDATED_TIMESTAMP_PATH";
|
||||||
|
}
|
||||||
|
|
||||||
|
public static final String DEFAULT_KEYSTORE_PASSWORD = "";
|
||||||
|
public static final String DEFAULT_TRUSTSTORE_PASSWORD = "";
|
||||||
|
|
||||||
|
@VisibleForTesting
|
||||||
|
static final String DEFAULT_KEYSTORE_PASSWORD_ENV_VAR_NAME = "KEYSTORE_PASSWORD";
|
||||||
|
@VisibleForTesting
|
||||||
|
static final String DEFAULT_TRUSTSTORE_PASSWORD_ENV_VAR_NAME = "TRUSTSTORE_PASSWORD";
|
||||||
|
|
||||||
|
private static final String KEYSTORE_PATH_VALUE = "/etc/my-ssl-store/keystore";
|
||||||
|
private static final String TRUSTSTORE_PATH_VALUE = "/etc/my-ssl-store/truststore";
|
||||||
|
private static final String KEYSTORE_PASSWORD_ENV_VAR_NAME = DEFAULT_KEYSTORE_PASSWORD_ENV_VAR_NAME;
|
||||||
|
private static final String KEYSTORE_UPDATED_TIMESTAMP_PATH_VALUE = "/etc/my-ssl-store/keystore-last-updatedtime";
|
||||||
|
private static final String TRUSTSTORE_PASSWORD_ENV_VAR_NAME = DEFAULT_TRUSTSTORE_PASSWORD_ENV_VAR_NAME;
|
||||||
|
private static final String TRUSTSTORE_UPDATED_TIMESTAMP_PATH_VALUE = "/etc/my-ssl-store/truststore-last-updatedtime";
|
||||||
|
|
||||||
|
private final String keystoreUpdatedTimeSecretKeyPath;
|
||||||
|
private final String truststoreUpdatedTimeSecretKeyPath;
|
||||||
|
private long keystoreLastUpdatedTime;
|
||||||
|
private long truststoreLastUpdatedTime;
|
||||||
|
|
||||||
|
public KubernetesSecretsSslContextFactory()
|
||||||
|
{
|
||||||
|
keystore = getString(EncryptionOptions.ConfigKey.KEYSTORE.toString(), KEYSTORE_PATH_VALUE);
|
||||||
|
keystore_password = getValueFromEnv(KEYSTORE_PASSWORD_ENV_VAR_NAME,
|
||||||
|
DEFAULT_KEYSTORE_PASSWORD);
|
||||||
|
truststore = getString(EncryptionOptions.ConfigKey.TRUSTSTORE.toString(), TRUSTSTORE_PATH_VALUE);
|
||||||
|
truststore_password = getValueFromEnv(TRUSTSTORE_PASSWORD_ENV_VAR_NAME,
|
||||||
|
DEFAULT_TRUSTSTORE_PASSWORD);
|
||||||
|
keystoreLastUpdatedTime = System.nanoTime();
|
||||||
|
keystoreUpdatedTimeSecretKeyPath = getString(ConfigKeys.KEYSTORE_UPDATED_TIMESTAMP_PATH,
|
||||||
|
KEYSTORE_UPDATED_TIMESTAMP_PATH_VALUE);
|
||||||
|
truststoreLastUpdatedTime = keystoreLastUpdatedTime;
|
||||||
|
truststoreUpdatedTimeSecretKeyPath = getString(ConfigKeys.TRUSTSTORE_UPDATED_TIMESTAMP_PATH,
|
||||||
|
TRUSTSTORE_UPDATED_TIMESTAMP_PATH_VALUE);
|
||||||
|
}
|
||||||
|
|
||||||
|
public KubernetesSecretsSslContextFactory(Map<String, Object> parameters)
|
||||||
|
{
|
||||||
|
super(parameters);
|
||||||
|
keystore = getString(EncryptionOptions.ConfigKey.KEYSTORE.toString(), KEYSTORE_PATH_VALUE);
|
||||||
|
keystore_password = getValueFromEnv(getString(ConfigKeys.KEYSTORE_PASSWORD_ENV_VAR,
|
||||||
|
KEYSTORE_PASSWORD_ENV_VAR_NAME), DEFAULT_KEYSTORE_PASSWORD);
|
||||||
|
truststore = getString(EncryptionOptions.ConfigKey.TRUSTSTORE.toString(), TRUSTSTORE_PATH_VALUE);
|
||||||
|
truststore_password = getValueFromEnv(getString(ConfigKeys.TRUSTSTORE_PASSWORD_ENV_VAR,
|
||||||
|
TRUSTSTORE_PASSWORD_ENV_VAR_NAME), DEFAULT_TRUSTSTORE_PASSWORD);
|
||||||
|
keystoreLastUpdatedTime = System.nanoTime();
|
||||||
|
keystoreUpdatedTimeSecretKeyPath = getString(ConfigKeys.KEYSTORE_UPDATED_TIMESTAMP_PATH,
|
||||||
|
KEYSTORE_UPDATED_TIMESTAMP_PATH_VALUE);
|
||||||
|
truststoreLastUpdatedTime = keystoreLastUpdatedTime;
|
||||||
|
truststoreUpdatedTimeSecretKeyPath = getString(ConfigKeys.TRUSTSTORE_UPDATED_TIMESTAMP_PATH,
|
||||||
|
TRUSTSTORE_UPDATED_TIMESTAMP_PATH_VALUE);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public synchronized void initHotReloading() {
|
||||||
|
// No-op
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Checks environment variables for {@code K8_SECRET_KEYSTORE_UPDATED_TIMESTAMP_ENV_VAR} and {@code K8_SECRET_TRUSTSTORE_UPDATED_TIMESTAMP_ENV_VAR}
|
||||||
|
* and compares the values for those variables with the current timestamps. In case the environment variables are
|
||||||
|
* not valid (either they are not initialized yet, got removed or got corrupted in-flight), this method considers
|
||||||
|
* that nothing has changed.
|
||||||
|
* @return {@code true} if either of the timestamps (keystore or truststore) got updated;{@code false} otherwise
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
public boolean shouldReload()
|
||||||
|
{
|
||||||
|
return hasKeystoreUpdated() || hasTruststoreUpdated();
|
||||||
|
}
|
||||||
|
|
||||||
|
@VisibleForTesting
|
||||||
|
String getValueFromEnv(String envVarName, String defaultValue) {
|
||||||
|
String valueFromEnv = StringUtils.isEmpty(envVarName) ? null : System.getenv(envVarName);
|
||||||
|
return StringUtils.isEmpty(valueFromEnv) ? defaultValue : valueFromEnv;
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean hasKeystoreUpdated() {
|
||||||
|
long keystoreUpdatedTime = getKeystoreLastUpdatedTime();
|
||||||
|
logger.info("Comparing keystore timestamps oldValue {} and newValue {}", keystoreLastUpdatedTime,
|
||||||
|
keystoreUpdatedTime);
|
||||||
|
if (keystoreUpdatedTime > keystoreLastUpdatedTime) {
|
||||||
|
logger.info("Updating the keystoreLastUpdatedTime from oldValue {} to newValue {}",
|
||||||
|
keystoreLastUpdatedTime, keystoreUpdatedTime);
|
||||||
|
keystoreLastUpdatedTime = keystoreUpdatedTime;
|
||||||
|
return true;
|
||||||
|
} else {
|
||||||
|
logger.info("Based on the comparision, no keystore update needed");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean hasTruststoreUpdated() {
|
||||||
|
long truststoreUpdatedTime = getTruststoreLastUpdatedTime();
|
||||||
|
logger.info("Comparing truststore timestamps oldValue {} and newValue {}", truststoreLastUpdatedTime,
|
||||||
|
truststoreUpdatedTime);
|
||||||
|
if (truststoreUpdatedTime > truststoreLastUpdatedTime) {
|
||||||
|
logger.info("Updating the truststoreLastUpdatedTime from oldValue {} to newValue {}",
|
||||||
|
truststoreLastUpdatedTime, truststoreUpdatedTime);
|
||||||
|
truststoreLastUpdatedTime = truststoreUpdatedTime;
|
||||||
|
return true;
|
||||||
|
} else {
|
||||||
|
logger.info("Based on the comparision, no truststore update needed");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private long getKeystoreLastUpdatedTime() {
|
||||||
|
Optional<String> keystoreUpdatedTimeSecretKeyValue = readSecretFromMountedVolume(keystoreUpdatedTimeSecretKeyPath);
|
||||||
|
if (keystoreUpdatedTimeSecretKeyValue.isPresent())
|
||||||
|
{
|
||||||
|
return parseLastUpdatedTime(keystoreUpdatedTimeSecretKeyValue.get(), keystoreLastUpdatedTime);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
logger.warn("Failed to load {}'s value. Will use existing value {}", keystoreUpdatedTimeSecretKeyPath,
|
||||||
|
keystoreLastUpdatedTime);
|
||||||
|
return keystoreLastUpdatedTime;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private long getTruststoreLastUpdatedTime() {
|
||||||
|
Optional<String> truststoreUpdatedTimeSecretKeyValue = readSecretFromMountedVolume(truststoreUpdatedTimeSecretKeyPath);
|
||||||
|
if (truststoreUpdatedTimeSecretKeyValue.isPresent())
|
||||||
|
{
|
||||||
|
return parseLastUpdatedTime(truststoreUpdatedTimeSecretKeyValue.get(), truststoreLastUpdatedTime);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
logger.warn("Failed to load {}'s value. Will use existing value {}", truststoreUpdatedTimeSecretKeyPath,
|
||||||
|
truststoreLastUpdatedTime);
|
||||||
|
return truststoreLastUpdatedTime;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private Optional<String> readSecretFromMountedVolume(String secretKeyPath) {
|
||||||
|
try
|
||||||
|
{
|
||||||
|
return Optional.of(new String(Files.readAllBytes(Paths.get(secretKeyPath))));
|
||||||
|
}
|
||||||
|
catch (IOException e)
|
||||||
|
{
|
||||||
|
logger.warn(String.format("Failed to read secretKeyPath %s from the mounted volume: %s", secretKeyPath, e.getMessage()));
|
||||||
|
return Optional.empty();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private long parseLastUpdatedTime(String latestUpdatedTime, long currentUpdatedTime) {
|
||||||
|
try
|
||||||
|
{
|
||||||
|
return Long.parseLong(latestUpdatedTime);
|
||||||
|
} catch(NumberFormatException e) {
|
||||||
|
logger.warn("Failed to parse the latestUpdatedTime {}. Will use current time {}", latestUpdatedTime,
|
||||||
|
currentUpdatedTime, e);
|
||||||
|
return currentUpdatedTime;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
|
|
@ -0,0 +1,296 @@
|
||||||
|
/*
|
||||||
|
* 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.security;
|
||||||
|
|
||||||
|
import java.io.File;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.io.OutputStream;
|
||||||
|
import java.nio.file.Files;
|
||||||
|
import java.nio.file.Paths;
|
||||||
|
import java.util.Arrays;
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.Map;
|
||||||
|
import javax.net.ssl.KeyManagerFactory;
|
||||||
|
import javax.net.ssl.TrustManagerFactory;
|
||||||
|
|
||||||
|
import org.apache.commons.lang3.StringUtils;
|
||||||
|
import org.junit.Assert;
|
||||||
|
import org.junit.Before;
|
||||||
|
import org.junit.BeforeClass;
|
||||||
|
import org.junit.Test;
|
||||||
|
import org.slf4j.Logger;
|
||||||
|
import org.slf4j.LoggerFactory;
|
||||||
|
|
||||||
|
import org.apache.cassandra.config.EncryptionOptions;
|
||||||
|
|
||||||
|
import static org.apache.cassandra.security.KubernetesSecretsSslContextFactory.ConfigKeys.KEYSTORE_PASSWORD_ENV_VAR;
|
||||||
|
import static org.apache.cassandra.security.KubernetesSecretsSslContextFactory.ConfigKeys.KEYSTORE_UPDATED_TIMESTAMP_PATH;
|
||||||
|
import static org.apache.cassandra.security.KubernetesSecretsSslContextFactory.ConfigKeys.TRUSTSTORE_PASSWORD_ENV_VAR;
|
||||||
|
import static org.apache.cassandra.security.KubernetesSecretsSslContextFactory.ConfigKeys.TRUSTSTORE_UPDATED_TIMESTAMP_PATH;
|
||||||
|
|
||||||
|
public class KubernetesSecretsSslContextFactoryTest
|
||||||
|
{
|
||||||
|
private static final Logger logger = LoggerFactory.getLogger(KubernetesSecretsSslContextFactoryTest.class);
|
||||||
|
private static final String TRUSTSTORE_PATH = EncryptionOptions.ConfigKey.TRUSTSTORE.toString();
|
||||||
|
private static final String KEYSTORE_PATH = EncryptionOptions.ConfigKey.KEYSTORE.toString();
|
||||||
|
|
||||||
|
private Map<String, Object> commonConfig = new HashMap<>();
|
||||||
|
private final static String truststoreUpdatedTimestampFilepath = "build/test/conf/cassandra_truststore_last_updatedtime";
|
||||||
|
private final static String keystoreUpdatedTimestampFilepath = "build/test/conf/cassandra_keystore_last_updatedtime";
|
||||||
|
|
||||||
|
private static class KubernetesSecretsSslContextFactoryForTestOnly extends KubernetesSecretsSslContextFactory
|
||||||
|
{
|
||||||
|
|
||||||
|
public KubernetesSecretsSslContextFactoryForTestOnly()
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
public KubernetesSecretsSslContextFactoryForTestOnly(Map<String, Object> config)
|
||||||
|
{
|
||||||
|
super(config);
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* This is overriden to first give priority to the input map configuration since we should not be setting env
|
||||||
|
* variables from the unit tests. However, if the input map configuration doesn't have the value for the
|
||||||
|
* given key then fallback to loading from the real environment variables.
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
String getValueFromEnv(String envVarName, String defaultValue)
|
||||||
|
{
|
||||||
|
String envVarValue = parameters.get(envVarName) != null ? parameters.get(envVarName).toString() : null;
|
||||||
|
if (StringUtils.isEmpty(envVarValue))
|
||||||
|
{
|
||||||
|
logger.info("Configuration doesn't have env variable {}. Will use parent's implementation", envVarName);
|
||||||
|
return super.getValueFromEnv(envVarName, defaultValue);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
logger.info("Configuration has env variable {} with value {}. Will use that.",
|
||||||
|
envVarName, envVarValue);
|
||||||
|
return envVarValue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@BeforeClass
|
||||||
|
public static void prepare()
|
||||||
|
{
|
||||||
|
deleteFileIfExists(truststoreUpdatedTimestampFilepath);
|
||||||
|
deleteFileIfExists(keystoreUpdatedTimestampFilepath);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void deleteFileIfExists(String filePath)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
logger.info("Deleting the file {} to prepare for the tests", new File(filePath).getAbsolutePath());
|
||||||
|
File file = new File(filePath);
|
||||||
|
if (file.exists())
|
||||||
|
{
|
||||||
|
file.delete();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (Exception e)
|
||||||
|
{
|
||||||
|
logger.warn("File {} could not be deleted.", filePath, e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Before
|
||||||
|
public void setup()
|
||||||
|
{
|
||||||
|
commonConfig.put(TRUSTSTORE_PATH, "build/test/conf/cassandra_ssl_test.truststore");
|
||||||
|
commonConfig.put(TRUSTSTORE_PASSWORD_ENV_VAR, "MY_TRUSTSTORE_PASSWORD");
|
||||||
|
commonConfig.put(TRUSTSTORE_UPDATED_TIMESTAMP_PATH, truststoreUpdatedTimestampFilepath);
|
||||||
|
/*
|
||||||
|
* 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("MY_TRUSTSTORE_PASSWORD", "cassandra");
|
||||||
|
commonConfig.put("require_client_auth", Boolean.FALSE);
|
||||||
|
commonConfig.put("cipher_suites", Arrays.asList("TLS_RSA_WITH_AES_128_CBC_SHA"));
|
||||||
|
}
|
||||||
|
|
||||||
|
private void addKeystoreOptions(Map<String, Object> config)
|
||||||
|
{
|
||||||
|
config.put(KEYSTORE_PATH, "build/test/conf/cassandra_ssl_test.keystore");
|
||||||
|
config.put(KEYSTORE_PASSWORD_ENV_VAR, "MY_KEYSTORE_PASSWORD");
|
||||||
|
config.put(KEYSTORE_UPDATED_TIMESTAMP_PATH, keystoreUpdatedTimestampFilepath);
|
||||||
|
/*
|
||||||
|
* 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.
|
||||||
|
*/
|
||||||
|
config.put("MY_KEYSTORE_PASSWORD", "cassandra");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test(expected = IOException.class)
|
||||||
|
public void buildTrustManagerFactoryWithInvalidTruststoreFile() throws IOException
|
||||||
|
{
|
||||||
|
Map<String, Object> config = new HashMap<>();
|
||||||
|
config.putAll(commonConfig);
|
||||||
|
config.put(TRUSTSTORE_PATH, "/this/is/probably/not/a/file/on/your/test/machine");
|
||||||
|
|
||||||
|
KubernetesSecretsSslContextFactory kubernetesSecretsSslContextFactory = new KubernetesSecretsSslContextFactoryForTestOnly(config);
|
||||||
|
kubernetesSecretsSslContextFactory.checkedExpiry = false;
|
||||||
|
kubernetesSecretsSslContextFactory.buildTrustManagerFactory();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test(expected = IOException.class)
|
||||||
|
public void buildTrustManagerFactoryWithBadPassword() throws IOException
|
||||||
|
{
|
||||||
|
Map<String, Object> config = new HashMap<>();
|
||||||
|
config.putAll(commonConfig);
|
||||||
|
config.remove(TRUSTSTORE_PASSWORD_ENV_VAR);
|
||||||
|
config.put(KubernetesSecretsSslContextFactory.DEFAULT_TRUSTSTORE_PASSWORD_ENV_VAR_NAME, "HomeOfBadPasswords");
|
||||||
|
|
||||||
|
KubernetesSecretsSslContextFactory kubernetesSecretsSslContextFactory = new KubernetesSecretsSslContextFactoryForTestOnly(config);
|
||||||
|
kubernetesSecretsSslContextFactory.checkedExpiry = false;
|
||||||
|
kubernetesSecretsSslContextFactory.buildTrustManagerFactory();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void buildTrustManagerFactoryWithEmptyPassword() throws IOException
|
||||||
|
{
|
||||||
|
Map<String, Object> config = new HashMap<>();
|
||||||
|
config.putAll(commonConfig);
|
||||||
|
config.put(TRUSTSTORE_PATH, "build/test/conf/cassandra_ssl_test.truststore-without-password");
|
||||||
|
config.remove(TRUSTSTORE_PASSWORD_ENV_VAR);
|
||||||
|
config.put(KubernetesSecretsSslContextFactory.DEFAULT_TRUSTSTORE_PASSWORD_ENV_VAR_NAME, "");
|
||||||
|
|
||||||
|
KubernetesSecretsSslContextFactory kubernetesSecretsSslContextFactory = new KubernetesSecretsSslContextFactoryForTestOnly(config);
|
||||||
|
kubernetesSecretsSslContextFactory.checkedExpiry = false;
|
||||||
|
kubernetesSecretsSslContextFactory.buildTrustManagerFactory();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void buildTrustManagerFactoryHappyPath() throws IOException
|
||||||
|
{
|
||||||
|
Map<String, Object> config = new HashMap<>();
|
||||||
|
config.putAll(commonConfig);
|
||||||
|
|
||||||
|
KubernetesSecretsSslContextFactory kubernetesSecretsSslContextFactory = new KubernetesSecretsSslContextFactoryForTestOnly(config);
|
||||||
|
kubernetesSecretsSslContextFactory.checkedExpiry = false;
|
||||||
|
TrustManagerFactory trustManagerFactory = kubernetesSecretsSslContextFactory.buildTrustManagerFactory();
|
||||||
|
Assert.assertNotNull(trustManagerFactory);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test(expected = IOException.class)
|
||||||
|
public void buildKeyManagerFactoryWithInvalidKeystoreFile() throws IOException
|
||||||
|
{
|
||||||
|
Map<String, Object> config = new HashMap<>();
|
||||||
|
config.putAll(commonConfig);
|
||||||
|
config.put(KEYSTORE_PATH, "/this/is/probably/not/a/file/on/your/test/machine");
|
||||||
|
|
||||||
|
KubernetesSecretsSslContextFactory kubernetesSecretsSslContextFactory = new KubernetesSecretsSslContextFactoryForTestOnly(config);
|
||||||
|
kubernetesSecretsSslContextFactory.checkedExpiry = false;
|
||||||
|
kubernetesSecretsSslContextFactory.buildKeyManagerFactory();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test(expected = IOException.class)
|
||||||
|
public void buildKeyManagerFactoryWithBadPassword() throws IOException
|
||||||
|
{
|
||||||
|
Map<String, Object> config = new HashMap<>();
|
||||||
|
config.putAll(commonConfig);
|
||||||
|
config.put(KEYSTORE_PATH, "build/test/conf/cassandra_ssl_test.keystore");
|
||||||
|
config.put(KubernetesSecretsSslContextFactory.DEFAULT_KEYSTORE_PASSWORD_ENV_VAR_NAME, "HomeOfBadPasswords");
|
||||||
|
|
||||||
|
KubernetesSecretsSslContextFactory kubernetesSecretsSslContextFactory = new KubernetesSecretsSslContextFactoryForTestOnly(config);
|
||||||
|
kubernetesSecretsSslContextFactory.buildKeyManagerFactory();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void buildKeyManagerFactoryHappyPath() throws IOException
|
||||||
|
{
|
||||||
|
Map<String, Object> config = new HashMap<>();
|
||||||
|
config.putAll(commonConfig);
|
||||||
|
|
||||||
|
KubernetesSecretsSslContextFactory kubernetesSecretsSslContextFactory1 = new KubernetesSecretsSslContextFactoryForTestOnly(config);
|
||||||
|
// Make sure the exiry check didn't happen so far for the private key
|
||||||
|
Assert.assertFalse(kubernetesSecretsSslContextFactory1.checkedExpiry);
|
||||||
|
|
||||||
|
addKeystoreOptions(config);
|
||||||
|
KubernetesSecretsSslContextFactory kubernetesSecretsSslContextFactory2 = new KubernetesSecretsSslContextFactoryForTestOnly(config);
|
||||||
|
// Trigger the private key loading. That will also check for expired private key
|
||||||
|
kubernetesSecretsSslContextFactory2.buildKeyManagerFactory();
|
||||||
|
// Now we should have checked the private key's expiry
|
||||||
|
Assert.assertTrue(kubernetesSecretsSslContextFactory2.checkedExpiry);
|
||||||
|
|
||||||
|
// Make sure that new factory object preforms the fresh private key expiry check
|
||||||
|
KubernetesSecretsSslContextFactory kubernetesSecretsSslContextFactory3 = new KubernetesSecretsSslContextFactoryForTestOnly(config);
|
||||||
|
Assert.assertFalse(kubernetesSecretsSslContextFactory3.checkedExpiry);
|
||||||
|
kubernetesSecretsSslContextFactory3.buildKeyManagerFactory();
|
||||||
|
Assert.assertTrue(kubernetesSecretsSslContextFactory3.checkedExpiry);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void checkTruststoreUpdateReloading() throws IOException
|
||||||
|
{
|
||||||
|
Map<String, Object> config = new HashMap<>();
|
||||||
|
config.putAll(commonConfig);
|
||||||
|
addKeystoreOptions(config);
|
||||||
|
|
||||||
|
KubernetesSecretsSslContextFactory kubernetesSecretsSslContextFactory = new KubernetesSecretsSslContextFactoryForTestOnly(config);
|
||||||
|
kubernetesSecretsSslContextFactory.checkedExpiry = false;
|
||||||
|
TrustManagerFactory trustManagerFactory = kubernetesSecretsSslContextFactory.buildTrustManagerFactory();
|
||||||
|
Assert.assertNotNull(trustManagerFactory);
|
||||||
|
Assert.assertFalse(kubernetesSecretsSslContextFactory.shouldReload());
|
||||||
|
|
||||||
|
updateTimestampFile(config, TRUSTSTORE_UPDATED_TIMESTAMP_PATH);
|
||||||
|
Assert.assertTrue(kubernetesSecretsSslContextFactory.shouldReload());
|
||||||
|
|
||||||
|
config.remove(TRUSTSTORE_UPDATED_TIMESTAMP_PATH);
|
||||||
|
Assert.assertFalse(kubernetesSecretsSslContextFactory.shouldReload());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void checkKeystoreUpdateReloading() throws IOException
|
||||||
|
{
|
||||||
|
Map<String, Object> config = new HashMap<>();
|
||||||
|
config.putAll(commonConfig);
|
||||||
|
addKeystoreOptions(config);
|
||||||
|
|
||||||
|
KubernetesSecretsSslContextFactory kubernetesSecretsSslContextFactory = new KubernetesSecretsSslContextFactoryForTestOnly(config);
|
||||||
|
kubernetesSecretsSslContextFactory.checkedExpiry = false;
|
||||||
|
KeyManagerFactory keyManagerFactory = kubernetesSecretsSslContextFactory.buildKeyManagerFactory();
|
||||||
|
Assert.assertNotNull(keyManagerFactory);
|
||||||
|
Assert.assertFalse(kubernetesSecretsSslContextFactory.shouldReload());
|
||||||
|
|
||||||
|
updateTimestampFile(config, KEYSTORE_UPDATED_TIMESTAMP_PATH);
|
||||||
|
Assert.assertTrue(kubernetesSecretsSslContextFactory.shouldReload());
|
||||||
|
|
||||||
|
config.remove(KEYSTORE_UPDATED_TIMESTAMP_PATH);
|
||||||
|
Assert.assertFalse(kubernetesSecretsSslContextFactory.shouldReload());
|
||||||
|
}
|
||||||
|
|
||||||
|
private void updateTimestampFile(Map<String, Object> config, String filePathKey)
|
||||||
|
{
|
||||||
|
String filePath = config.containsKey(filePathKey) ? config.get(filePathKey).toString() : null;
|
||||||
|
try (OutputStream os = Files.newOutputStream(Paths.get(filePath)))
|
||||||
|
{
|
||||||
|
String timestamp = String.valueOf(System.nanoTime());
|
||||||
|
os.write(timestamp.getBytes());
|
||||||
|
logger.info("Successfully wrote to file {}", filePath);
|
||||||
|
}
|
||||||
|
catch (IOException e)
|
||||||
|
{
|
||||||
|
logger.warn("Failed to write to filePath {} from the mounted volume", filePath, e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -17,27 +17,38 @@
|
||||||
*/
|
*/
|
||||||
package org.apache.cassandra.config;
|
package org.apache.cassandra.config;
|
||||||
|
|
||||||
import java.io.File;
|
import java.util.Arrays;
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.HashSet;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
import java.util.Objects;
|
import java.util.Objects;
|
||||||
|
import java.util.Set;
|
||||||
|
|
||||||
import com.google.common.annotations.VisibleForTesting;
|
import com.google.common.annotations.VisibleForTesting;
|
||||||
import com.google.common.collect.ImmutableList;
|
import com.google.common.collect.ImmutableList;
|
||||||
|
|
||||||
import org.slf4j.Logger;
|
import org.slf4j.Logger;
|
||||||
import org.slf4j.LoggerFactory;
|
import org.slf4j.LoggerFactory;
|
||||||
|
|
||||||
import org.apache.cassandra.locator.IEndpointSnitch;
|
import org.apache.cassandra.locator.IEndpointSnitch;
|
||||||
import org.apache.cassandra.locator.InetAddressAndPort;
|
import org.apache.cassandra.locator.InetAddressAndPort;
|
||||||
import org.apache.cassandra.security.SSLFactory;
|
import org.apache.cassandra.security.ISslContextFactory;
|
||||||
|
import org.apache.cassandra.utils.FBUtilities;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This holds various options used for enabling SSL/TLS encryption.
|
||||||
|
* Examples of such options are: supported cipher-suites, ssl protocol with version, accepted protocols, end-point
|
||||||
|
* verification, require client-auth/cert etc.
|
||||||
|
*/
|
||||||
public class EncryptionOptions
|
public class EncryptionOptions
|
||||||
{
|
{
|
||||||
Logger logger = LoggerFactory.getLogger(EncryptionOptions.class);
|
Logger logger = LoggerFactory.getLogger(EncryptionOptions.class);
|
||||||
|
|
||||||
public enum TlsEncryptionPolicy
|
public enum TlsEncryptionPolicy
|
||||||
{
|
{
|
||||||
UNENCRYPTED("unencrypted"), OPTIONAL("optionally encrypted"), ENCRYPTED("encrypted");
|
UNENCRYPTED("unencrypted"),
|
||||||
|
OPTIONAL("optionally encrypted"),
|
||||||
|
ENCRYPTED("encrypted");
|
||||||
|
|
||||||
private final String description;
|
private final String description;
|
||||||
|
|
||||||
|
|
@ -52,6 +63,12 @@ public class EncryptionOptions
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* 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
|
||||||
|
* truststore_passwords configurations as they are in plaintext format.
|
||||||
|
*/
|
||||||
|
public final ParameterizedClass ssl_context_factory;
|
||||||
public final String keystore;
|
public final String keystore;
|
||||||
public final String keystore_password;
|
public final String keystore_password;
|
||||||
public final String truststore;
|
public final String truststore;
|
||||||
|
|
@ -73,11 +90,58 @@ public class EncryptionOptions
|
||||||
protected Boolean optional;
|
protected Boolean optional;
|
||||||
|
|
||||||
// Calculated by calling applyConfig() after populating/parsing
|
// Calculated by calling applyConfig() after populating/parsing
|
||||||
protected Boolean isEnabled = null;
|
protected Boolean isEnabled;
|
||||||
protected Boolean isOptional = null;
|
protected Boolean isOptional;
|
||||||
|
|
||||||
|
/*
|
||||||
|
* We will wait to initialize this until applyConfig() call to make sure we do it only when the caller is ready
|
||||||
|
* to use this option instance.
|
||||||
|
*/
|
||||||
|
public ISslContextFactory sslContextFactoryInstance;
|
||||||
|
|
||||||
|
public enum ConfigKey
|
||||||
|
{
|
||||||
|
KEYSTORE("keystore"),
|
||||||
|
KEYSTORE_PASSWORD("keystore_password"),
|
||||||
|
TRUSTSTORE("truststore"),
|
||||||
|
TRUSTSTORE_PASSWORD("truststore_password"),
|
||||||
|
CIPHER_SUITES("cipher_suites"),
|
||||||
|
PROTOCOL("protocol"),
|
||||||
|
ACCEPTED_PROTOCOLS("accepted_protocols"),
|
||||||
|
ALGORITHM("algorithm"),
|
||||||
|
STORE_TYPE("store_type"),
|
||||||
|
REQUIRE_CLIENT_AUTH("require_client_auth"),
|
||||||
|
REQUIRE_ENDPOINT_VERIFICATION("require_endpoint_verification"),
|
||||||
|
ENABLED("enabled"),
|
||||||
|
OPTIONAL("optional");
|
||||||
|
|
||||||
|
final String keyName;
|
||||||
|
|
||||||
|
ConfigKey(String keyName)
|
||||||
|
{
|
||||||
|
this.keyName=keyName;
|
||||||
|
}
|
||||||
|
|
||||||
|
String getKeyName()
|
||||||
|
{
|
||||||
|
return keyName;
|
||||||
|
}
|
||||||
|
|
||||||
|
static Set<String> asSet()
|
||||||
|
{
|
||||||
|
Set<String> valueSet = new HashSet<>();
|
||||||
|
ConfigKey[] values = values();
|
||||||
|
for(ConfigKey key: values) {
|
||||||
|
valueSet.add(key.getKeyName().toLowerCase());
|
||||||
|
}
|
||||||
|
return valueSet;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public EncryptionOptions()
|
public EncryptionOptions()
|
||||||
{
|
{
|
||||||
|
ssl_context_factory = new ParameterizedClass("org.apache.cassandra.security.DefaultSslContextFactory",
|
||||||
|
new HashMap<>());
|
||||||
keystore = "conf/.keystore";
|
keystore = "conf/.keystore";
|
||||||
keystore_password = "cassandra";
|
keystore_password = "cassandra";
|
||||||
truststore = "conf/.truststore";
|
truststore = "conf/.truststore";
|
||||||
|
|
@ -93,8 +157,13 @@ public class EncryptionOptions
|
||||||
optional = null;
|
optional = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
public EncryptionOptions(String keystore, String keystore_password, String truststore, String truststore_password, List<String> cipher_suites, String protocol, List<String> accepted_protocols, String algorithm, String store_type, boolean require_client_auth, boolean require_endpoint_verification, Boolean enabled, Boolean optional)
|
public EncryptionOptions(ParameterizedClass ssl_context_factory, String keystore, String keystore_password,
|
||||||
|
String truststore, String truststore_password, List<String> cipher_suites,
|
||||||
|
String protocol, List<String> accepted_protocols, String algorithm, String store_type,
|
||||||
|
boolean require_client_auth, boolean require_endpoint_verification, Boolean enabled,
|
||||||
|
Boolean optional)
|
||||||
{
|
{
|
||||||
|
this.ssl_context_factory = ssl_context_factory;
|
||||||
this.keystore = keystore;
|
this.keystore = keystore;
|
||||||
this.keystore_password = keystore_password;
|
this.keystore_password = keystore_password;
|
||||||
this.truststore = truststore;
|
this.truststore = truststore;
|
||||||
|
|
@ -112,6 +181,7 @@ public class EncryptionOptions
|
||||||
|
|
||||||
public EncryptionOptions(EncryptionOptions options)
|
public EncryptionOptions(EncryptionOptions options)
|
||||||
{
|
{
|
||||||
|
ssl_context_factory = options.ssl_context_factory;
|
||||||
keystore = options.keystore;
|
keystore = options.keystore;
|
||||||
keystore_password = options.keystore_password;
|
keystore_password = options.keystore_password;
|
||||||
truststore = options.truststore;
|
truststore = options.truststore;
|
||||||
|
|
@ -130,11 +200,15 @@ public class EncryptionOptions
|
||||||
/* Computes enabled and optional before use. Because the configuration can be loaded
|
/* Computes enabled and optional before use. Because the configuration can be loaded
|
||||||
* through pluggable mechanisms this is the only safe way to make sure that
|
* through pluggable mechanisms this is the only safe way to make sure that
|
||||||
* enabled and optional are set correctly.
|
* enabled and optional are set correctly.
|
||||||
|
*
|
||||||
|
* It also initializes the ISslContextFactory's instance
|
||||||
*/
|
*/
|
||||||
public EncryptionOptions applyConfig()
|
public EncryptionOptions applyConfig()
|
||||||
{
|
{
|
||||||
ensureConfigNotApplied();
|
ensureConfigNotApplied();
|
||||||
|
|
||||||
|
initializeSslContextFactory();
|
||||||
|
|
||||||
isEnabled = this.enabled != null && enabled;
|
isEnabled = this.enabled != null && enabled;
|
||||||
|
|
||||||
if (optional != null)
|
if (optional != null)
|
||||||
|
|
@ -144,7 +218,7 @@ public class EncryptionOptions
|
||||||
// If someone is asking for an _insecure_ connection and not explicitly telling us to refuse
|
// If someone is asking for an _insecure_ connection and not explicitly telling us to refuse
|
||||||
// encrypted connections AND they have a keystore file, we assume they would like to be able
|
// encrypted connections AND they have a keystore file, we assume they would like to be able
|
||||||
// to transition to encrypted connections in the future.
|
// to transition to encrypted connections in the future.
|
||||||
else if (new File(keystore).exists())
|
else if (sslContextFactoryInstance.hasKeystore())
|
||||||
{
|
{
|
||||||
isOptional = !isEnabled;
|
isOptional = !isEnabled;
|
||||||
}
|
}
|
||||||
|
|
@ -156,6 +230,63 @@ public class EncryptionOptions
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Prepares the parameterized keys provided in the configuration for {@link ISslContextFactory} to be passed in
|
||||||
|
* as the constructor for its implementation.
|
||||||
|
*
|
||||||
|
* @throws IllegalArgumentException in case any pre-defined key, as per {@link ConfigKey}, for the encryption
|
||||||
|
* options is duplicated in the parameterized keys.
|
||||||
|
*/
|
||||||
|
private void prepareSslContextFactoryParameterizedKeys(Map<String,Object> sslContextFactoryParameters)
|
||||||
|
{
|
||||||
|
if (ssl_context_factory.parameters != null)
|
||||||
|
{
|
||||||
|
Set<String> configKeys = ConfigKey.asSet();
|
||||||
|
for (Map.Entry<String, String> entry : ssl_context_factory.parameters.entrySet())
|
||||||
|
{
|
||||||
|
if(configKeys.contains(entry.getKey().toLowerCase()))
|
||||||
|
{
|
||||||
|
throw new IllegalArgumentException("SslContextFactory "+ssl_context_factory.class_name+" should " +
|
||||||
|
"configure '"+entry.getKey()+"' as encryption_options instead of" +
|
||||||
|
" parameterized keys");
|
||||||
|
}
|
||||||
|
sslContextFactoryParameters.put(entry.getKey(),entry.getValue());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void initializeSslContextFactory() {
|
||||||
|
Map<String,Object> sslContextFactoryParameters = new HashMap<>();
|
||||||
|
prepareSslContextFactoryParameterizedKeys(sslContextFactoryParameters);
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Copy all configs to the Map to pass it on to the ISslContextFactory's implementation
|
||||||
|
*/
|
||||||
|
putSslContextFactoryParameter(sslContextFactoryParameters, ConfigKey.KEYSTORE, this.keystore);
|
||||||
|
putSslContextFactoryParameter(sslContextFactoryParameters, ConfigKey.KEYSTORE_PASSWORD, this.keystore_password);
|
||||||
|
putSslContextFactoryParameter(sslContextFactoryParameters, ConfigKey.TRUSTSTORE, this.truststore);
|
||||||
|
putSslContextFactoryParameter(sslContextFactoryParameters, ConfigKey.TRUSTSTORE_PASSWORD, this.truststore_password);
|
||||||
|
putSslContextFactoryParameter(sslContextFactoryParameters, ConfigKey.CIPHER_SUITES, this.cipher_suites);
|
||||||
|
putSslContextFactoryParameter(sslContextFactoryParameters, ConfigKey.PROTOCOL, this.protocol);
|
||||||
|
putSslContextFactoryParameter(sslContextFactoryParameters, ConfigKey.ACCEPTED_PROTOCOLS, this.accepted_protocols);
|
||||||
|
putSslContextFactoryParameter(sslContextFactoryParameters, ConfigKey.ALGORITHM, this.algorithm);
|
||||||
|
putSslContextFactoryParameter(sslContextFactoryParameters, ConfigKey.STORE_TYPE, this.store_type);
|
||||||
|
putSslContextFactoryParameter(sslContextFactoryParameters, ConfigKey.REQUIRE_CLIENT_AUTH, this.require_client_auth);
|
||||||
|
putSslContextFactoryParameter(sslContextFactoryParameters, ConfigKey.REQUIRE_ENDPOINT_VERIFICATION, this.require_endpoint_verification);
|
||||||
|
putSslContextFactoryParameter(sslContextFactoryParameters, ConfigKey.ENABLED, this.enabled);
|
||||||
|
putSslContextFactoryParameter(sslContextFactoryParameters, ConfigKey.OPTIONAL, this.optional);
|
||||||
|
|
||||||
|
sslContextFactoryInstance = FBUtilities.newSslContextFactory(ssl_context_factory.class_name,
|
||||||
|
sslContextFactoryParameters);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void putSslContextFactoryParameter(Map<String,Object> existingParameters, ConfigKey configKey,
|
||||||
|
Object value) {
|
||||||
|
if (value != null) {
|
||||||
|
existingParameters.put(configKey.getKeyName(), value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private void ensureConfigApplied()
|
private void ensureConfigApplied()
|
||||||
{
|
{
|
||||||
if (isEnabled == null || isOptional == null)
|
if (isEnabled == null || isOptional == null)
|
||||||
|
|
@ -236,48 +367,9 @@ public class EncryptionOptions
|
||||||
this.accepted_protocols = accepted_protocols == null ? null : ImmutableList.copyOf(accepted_protocols);
|
this.accepted_protocols = accepted_protocols == null ? null : ImmutableList.copyOf(accepted_protocols);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* This list is substituted in configurations that have explicitly specified the original "TLS" default,
|
|
||||||
* by extracting it from the default "TLS" SSL Context instance
|
|
||||||
*/
|
|
||||||
static private final List<String> TLS_PROTOCOL_SUBSTITUTION = SSLFactory.tlsInstanceProtocolSubstitution();
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Combine the pre-4.0 protocol field with the accepted_protocols list, substituting a list of
|
|
||||||
* explicit protocols for the previous catchall default of "TLS"
|
|
||||||
* @return array of protocol names suitable for passing to SslContextBuilder.protocols, or null if the default
|
|
||||||
*/
|
|
||||||
public List<String> acceptedProtocols()
|
public List<String> acceptedProtocols()
|
||||||
{
|
{
|
||||||
if (accepted_protocols == null)
|
return sslContextFactoryInstance.getAcceptedProtocols();
|
||||||
{
|
|
||||||
if (protocol == null)
|
|
||||||
{
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
// TLS is accepted by SSLContext.getInstance as a shorthand for give me an engine that
|
|
||||||
// can speak some of the TLS protocols. It is not supported by SSLEngine.setAcceptedProtocols
|
|
||||||
// so substitute if the user hasn't provided an accepted protocol configuration
|
|
||||||
else if (protocol.equalsIgnoreCase("TLS"))
|
|
||||||
{
|
|
||||||
return TLS_PROTOCOL_SUBSTITUTION;
|
|
||||||
}
|
|
||||||
else // the user was trying to limit to a single specific protocol, so try that
|
|
||||||
{
|
|
||||||
return ImmutableList.of(protocol);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (protocol != null && !protocol.equalsIgnoreCase("TLS") &&
|
|
||||||
accepted_protocols.stream().noneMatch(ap -> ap.equalsIgnoreCase(protocol)))
|
|
||||||
{
|
|
||||||
// If the user provided a non-generic default protocol, append it to accepted_protocols - they wanted
|
|
||||||
// it after all.
|
|
||||||
return ImmutableList.<String>builder().addAll(accepted_protocols).add(protocol).build();
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
return accepted_protocols;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public String[] acceptedProtocolsArray()
|
public String[] acceptedProtocolsArray()
|
||||||
|
|
@ -286,11 +378,6 @@ public class EncryptionOptions
|
||||||
return ap == null ? new String[0] : ap.toArray(new String[0]);
|
return ap == null ? new String[0] : ap.toArray(new String[0]);
|
||||||
}
|
}
|
||||||
|
|
||||||
public String[] cipherSuitesArray()
|
|
||||||
{
|
|
||||||
return cipher_suites == null ? new String[0] : cipher_suites.toArray(new String[0]);
|
|
||||||
}
|
|
||||||
|
|
||||||
public TlsEncryptionPolicy tlsEncryptionPolicy()
|
public TlsEncryptionPolicy tlsEncryptionPolicy()
|
||||||
{
|
{
|
||||||
if (isOptional())
|
if (isOptional())
|
||||||
|
|
@ -307,104 +394,126 @@ public class EncryptionOptions
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public EncryptionOptions withSslContextFactory(ParameterizedClass sslContextFactoryClass) {
|
||||||
|
return new EncryptionOptions(sslContextFactoryClass, keystore, keystore_password, truststore,
|
||||||
|
truststore_password, cipher_suites,protocol, accepted_protocols, algorithm,
|
||||||
|
store_type, require_client_auth, require_endpoint_verification,enabled,
|
||||||
|
optional).applyConfig();
|
||||||
|
}
|
||||||
|
|
||||||
public EncryptionOptions withKeyStore(String keystore)
|
public EncryptionOptions withKeyStore(String keystore)
|
||||||
{
|
{
|
||||||
return new EncryptionOptions(keystore, keystore_password, truststore, truststore_password, cipher_suites,
|
return new EncryptionOptions(ssl_context_factory, keystore, keystore_password, truststore,
|
||||||
protocol, accepted_protocols, algorithm, store_type, require_client_auth, require_endpoint_verification,
|
truststore_password, cipher_suites,protocol, accepted_protocols, algorithm,
|
||||||
enabled, optional).applyConfig();
|
store_type, require_client_auth, require_endpoint_verification, enabled,
|
||||||
|
optional).applyConfig();
|
||||||
}
|
}
|
||||||
|
|
||||||
public EncryptionOptions withKeyStorePassword(String keystore_password)
|
public EncryptionOptions withKeyStorePassword(String keystore_password)
|
||||||
{
|
{
|
||||||
return new EncryptionOptions(keystore, keystore_password, truststore, truststore_password, cipher_suites,
|
return new EncryptionOptions(ssl_context_factory, keystore, keystore_password, truststore,
|
||||||
protocol, accepted_protocols, algorithm, store_type, require_client_auth, require_endpoint_verification,
|
truststore_password, cipher_suites,protocol, accepted_protocols, algorithm,
|
||||||
enabled, optional).applyConfig();
|
store_type, require_client_auth, require_endpoint_verification, enabled,
|
||||||
|
optional).applyConfig();
|
||||||
}
|
}
|
||||||
|
|
||||||
public EncryptionOptions withTrustStore(String truststore)
|
public EncryptionOptions withTrustStore(String truststore)
|
||||||
{
|
{
|
||||||
return new EncryptionOptions(keystore, keystore_password, truststore, truststore_password, cipher_suites,
|
return new EncryptionOptions(ssl_context_factory, keystore, keystore_password, truststore,
|
||||||
protocol, accepted_protocols, algorithm, store_type, require_client_auth, require_endpoint_verification,
|
truststore_password, cipher_suites, protocol, accepted_protocols, algorithm,
|
||||||
enabled, optional).applyConfig();
|
store_type, require_client_auth, require_endpoint_verification, enabled,
|
||||||
|
optional).applyConfig();
|
||||||
}
|
}
|
||||||
|
|
||||||
public EncryptionOptions withTrustStorePassword(String truststore_password)
|
public EncryptionOptions withTrustStorePassword(String truststore_password)
|
||||||
{
|
{
|
||||||
return new EncryptionOptions(keystore, keystore_password, truststore, truststore_password, cipher_suites,
|
return new EncryptionOptions(ssl_context_factory, keystore, keystore_password, truststore,
|
||||||
protocol, accepted_protocols, algorithm, store_type, require_client_auth, require_endpoint_verification,
|
truststore_password, cipher_suites, protocol, accepted_protocols, algorithm,
|
||||||
enabled, optional).applyConfig();
|
store_type, require_client_auth, require_endpoint_verification, enabled,
|
||||||
|
optional).applyConfig();
|
||||||
}
|
}
|
||||||
|
|
||||||
public EncryptionOptions withCipherSuites(List<String> cipher_suites)
|
public EncryptionOptions withCipherSuites(List<String> cipher_suites)
|
||||||
{
|
{
|
||||||
return new EncryptionOptions(keystore, keystore_password, truststore, truststore_password, cipher_suites,
|
return new EncryptionOptions(ssl_context_factory, keystore, keystore_password, truststore,
|
||||||
protocol, accepted_protocols, algorithm, store_type, require_client_auth, require_endpoint_verification,
|
truststore_password, cipher_suites, protocol, accepted_protocols, algorithm,
|
||||||
enabled, optional).applyConfig();
|
store_type, require_client_auth, require_endpoint_verification, enabled,
|
||||||
|
optional).applyConfig();
|
||||||
}
|
}
|
||||||
|
|
||||||
public EncryptionOptions withCipherSuites(String ... cipher_suites)
|
public EncryptionOptions withCipherSuites(String ... cipher_suites)
|
||||||
{
|
{
|
||||||
return new EncryptionOptions(keystore, keystore_password, truststore, truststore_password, ImmutableList.copyOf(cipher_suites),
|
return new EncryptionOptions(ssl_context_factory, keystore, keystore_password, truststore,
|
||||||
protocol, accepted_protocols, algorithm, store_type, require_client_auth, require_endpoint_verification,
|
truststore_password, ImmutableList.copyOf(cipher_suites), protocol,
|
||||||
enabled, optional).applyConfig();
|
accepted_protocols, algorithm, store_type, require_client_auth,
|
||||||
|
require_endpoint_verification, enabled, optional).applyConfig();
|
||||||
}
|
}
|
||||||
|
|
||||||
public EncryptionOptions withProtocol(String protocol)
|
public EncryptionOptions withProtocol(String protocol)
|
||||||
{
|
{
|
||||||
return new EncryptionOptions(keystore, keystore_password, truststore, truststore_password, cipher_suites,
|
return new EncryptionOptions(ssl_context_factory, keystore, keystore_password, truststore,
|
||||||
protocol, accepted_protocols, algorithm, store_type, require_client_auth, require_endpoint_verification,
|
truststore_password, cipher_suites, protocol, accepted_protocols, algorithm,
|
||||||
enabled, optional).applyConfig();
|
store_type, require_client_auth, require_endpoint_verification, enabled,
|
||||||
|
optional).applyConfig();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
public EncryptionOptions withAcceptedProtocols(List<String> accepted_protocols)
|
public EncryptionOptions withAcceptedProtocols(List<String> accepted_protocols)
|
||||||
{
|
{
|
||||||
return new EncryptionOptions(keystore, keystore_password, truststore, truststore_password, cipher_suites, protocol,
|
return new EncryptionOptions(ssl_context_factory, keystore, keystore_password, truststore,
|
||||||
accepted_protocols == null ? null : ImmutableList.copyOf(accepted_protocols),
|
truststore_password, cipher_suites,protocol, accepted_protocols == null ? null :
|
||||||
algorithm, store_type, require_client_auth, require_endpoint_verification, enabled, optional).applyConfig();
|
ImmutableList.copyOf(accepted_protocols),
|
||||||
|
algorithm, store_type, require_client_auth, require_endpoint_verification,
|
||||||
|
enabled, optional).applyConfig();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
public EncryptionOptions withAlgorithm(String algorithm)
|
public EncryptionOptions withAlgorithm(String algorithm)
|
||||||
{
|
{
|
||||||
return new EncryptionOptions(keystore, keystore_password, truststore, truststore_password, cipher_suites,
|
return new EncryptionOptions(ssl_context_factory, keystore, keystore_password, truststore,
|
||||||
protocol, accepted_protocols, algorithm, store_type, require_client_auth, require_endpoint_verification,
|
truststore_password, cipher_suites, protocol, accepted_protocols, algorithm,
|
||||||
enabled, optional).applyConfig();
|
store_type, require_client_auth, require_endpoint_verification, enabled,
|
||||||
|
optional).applyConfig();
|
||||||
}
|
}
|
||||||
|
|
||||||
public EncryptionOptions withStoreType(String store_type)
|
public EncryptionOptions withStoreType(String store_type)
|
||||||
{
|
{
|
||||||
return new EncryptionOptions(keystore, keystore_password, truststore, truststore_password, cipher_suites,
|
return new EncryptionOptions(ssl_context_factory, keystore, keystore_password, truststore,
|
||||||
protocol, accepted_protocols, algorithm, store_type, require_client_auth, require_endpoint_verification,
|
truststore_password, cipher_suites, protocol, accepted_protocols, algorithm,
|
||||||
enabled, optional).applyConfig();
|
store_type, require_client_auth, require_endpoint_verification, enabled,
|
||||||
|
optional).applyConfig();
|
||||||
}
|
}
|
||||||
|
|
||||||
public EncryptionOptions withRequireClientAuth(boolean require_client_auth)
|
public EncryptionOptions withRequireClientAuth(boolean require_client_auth)
|
||||||
{
|
{
|
||||||
return new EncryptionOptions(keystore, keystore_password, truststore, truststore_password, cipher_suites,
|
return new EncryptionOptions(ssl_context_factory, keystore, keystore_password, truststore,
|
||||||
protocol, accepted_protocols, algorithm, store_type, require_client_auth, require_endpoint_verification,
|
truststore_password, cipher_suites, protocol, accepted_protocols, algorithm,
|
||||||
enabled, optional).applyConfig();
|
store_type, require_client_auth, require_endpoint_verification, enabled,
|
||||||
|
optional).applyConfig();
|
||||||
}
|
}
|
||||||
|
|
||||||
public EncryptionOptions withRequireEndpointVerification(boolean require_endpoint_verification)
|
public EncryptionOptions withRequireEndpointVerification(boolean require_endpoint_verification)
|
||||||
{
|
{
|
||||||
return new EncryptionOptions(keystore, keystore_password, truststore, truststore_password, cipher_suites,
|
return new EncryptionOptions(ssl_context_factory, keystore, keystore_password, truststore,
|
||||||
protocol, accepted_protocols, algorithm, store_type, require_client_auth, require_endpoint_verification,
|
truststore_password, cipher_suites, protocol, accepted_protocols, algorithm,
|
||||||
enabled, optional).applyConfig();
|
store_type, require_client_auth, require_endpoint_verification, enabled,
|
||||||
|
optional).applyConfig();
|
||||||
}
|
}
|
||||||
|
|
||||||
public EncryptionOptions withEnabled(boolean enabled)
|
public EncryptionOptions withEnabled(boolean enabled)
|
||||||
{
|
{
|
||||||
return new EncryptionOptions(keystore, keystore_password, truststore, truststore_password, cipher_suites,
|
return new EncryptionOptions(ssl_context_factory, keystore, keystore_password, truststore,
|
||||||
protocol, accepted_protocols, algorithm, store_type, require_client_auth, require_endpoint_verification,
|
truststore_password, cipher_suites, protocol, accepted_protocols, algorithm,
|
||||||
enabled, optional).applyConfig();
|
store_type, require_client_auth, require_endpoint_verification, enabled,
|
||||||
|
optional).applyConfig();
|
||||||
}
|
}
|
||||||
|
|
||||||
public EncryptionOptions withOptional(Boolean optional)
|
public EncryptionOptions withOptional(Boolean optional)
|
||||||
{
|
{
|
||||||
return new EncryptionOptions(keystore, keystore_password, truststore, truststore_password, cipher_suites,
|
return new EncryptionOptions(ssl_context_factory, keystore, keystore_password, truststore,
|
||||||
protocol, accepted_protocols, algorithm, store_type, require_client_auth, require_endpoint_verification,
|
truststore_password, cipher_suites, protocol, accepted_protocols, algorithm,
|
||||||
enabled, optional).applyConfig();
|
store_type, require_client_auth, require_endpoint_verification, enabled,
|
||||||
|
optional).applyConfig();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -432,7 +541,8 @@ public class EncryptionOptions
|
||||||
Objects.equals(accepted_protocols, opt.accepted_protocols) &&
|
Objects.equals(accepted_protocols, opt.accepted_protocols) &&
|
||||||
Objects.equals(algorithm, opt.algorithm) &&
|
Objects.equals(algorithm, opt.algorithm) &&
|
||||||
Objects.equals(store_type, opt.store_type) &&
|
Objects.equals(store_type, opt.store_type) &&
|
||||||
Objects.equals(cipher_suites, opt.cipher_suites);
|
Objects.equals(cipher_suites, opt.cipher_suites) &&
|
||||||
|
Objects.equals(ssl_context_factory, opt.ssl_context_factory);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -456,6 +566,7 @@ public class EncryptionOptions
|
||||||
result += 31 * (cipher_suites == null ? 0 : cipher_suites.hashCode());
|
result += 31 * (cipher_suites == null ? 0 : cipher_suites.hashCode());
|
||||||
result += 31 * Boolean.hashCode(require_client_auth);
|
result += 31 * Boolean.hashCode(require_client_auth);
|
||||||
result += 31 * Boolean.hashCode(require_endpoint_verification);
|
result += 31 * Boolean.hashCode(require_endpoint_verification);
|
||||||
|
result += 31 * (ssl_context_factory == null ? 0 : ssl_context_factory.hashCode());
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -475,16 +586,16 @@ public class EncryptionOptions
|
||||||
this.enable_legacy_ssl_storage_port = false;
|
this.enable_legacy_ssl_storage_port = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
public ServerEncryptionOptions(String keystore, String keystore_password, String truststore,
|
public ServerEncryptionOptions(ParameterizedClass sslContextFactoryClass, String keystore,
|
||||||
String truststore_password, List<String> cipher_suites, String protocol,
|
String keystore_password, String truststore, String truststore_password,
|
||||||
List<String> accepted_protocols, String algorithm, String store_type,
|
List<String> cipher_suites, String protocol, List<String> accepted_protocols,
|
||||||
boolean require_client_auth, boolean require_endpoint_verification,
|
String algorithm, String store_type, boolean require_client_auth,
|
||||||
Boolean optional, InternodeEncryption internode_encryption,
|
boolean require_endpoint_verification, Boolean optional,
|
||||||
boolean enable_legacy_ssl_storage_port)
|
InternodeEncryption internode_encryption, boolean enable_legacy_ssl_storage_port)
|
||||||
{
|
{
|
||||||
super(keystore, keystore_password, truststore, truststore_password, cipher_suites, protocol,
|
super(sslContextFactoryClass, keystore, keystore_password, truststore, truststore_password, cipher_suites,
|
||||||
accepted_protocols, algorithm, store_type, require_client_auth, require_endpoint_verification,
|
protocol, accepted_protocols, algorithm, store_type, require_client_auth, require_endpoint_verification,
|
||||||
null, optional);
|
null, optional);
|
||||||
this.internode_encryption = internode_encryption;
|
this.internode_encryption = internode_encryption;
|
||||||
this.enable_legacy_ssl_storage_port = enable_legacy_ssl_storage_port;
|
this.enable_legacy_ssl_storage_port = enable_legacy_ssl_storage_port;
|
||||||
}
|
}
|
||||||
|
|
@ -562,110 +673,148 @@ public class EncryptionOptions
|
||||||
return optional != null && optional;
|
return optional != null && optional;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public ServerEncryptionOptions withSslContextFactory(ParameterizedClass sslContextFactoryClass)
|
||||||
|
{
|
||||||
|
return new ServerEncryptionOptions(sslContextFactoryClass, keystore, keystore_password, truststore,
|
||||||
|
truststore_password, cipher_suites, protocol, accepted_protocols,
|
||||||
|
algorithm, store_type, require_client_auth,
|
||||||
|
require_endpoint_verification, optional, internode_encryption,
|
||||||
|
enable_legacy_ssl_storage_port).applyConfigInternal();
|
||||||
|
}
|
||||||
|
|
||||||
public ServerEncryptionOptions withKeyStore(String keystore)
|
public ServerEncryptionOptions withKeyStore(String keystore)
|
||||||
{
|
{
|
||||||
return new ServerEncryptionOptions(keystore, keystore_password, truststore, truststore_password, cipher_suites,
|
return new ServerEncryptionOptions(ssl_context_factory, keystore, keystore_password, truststore,
|
||||||
protocol, accepted_protocols, algorithm, store_type, require_client_auth, require_endpoint_verification,
|
truststore_password, cipher_suites, protocol, accepted_protocols,
|
||||||
optional, internode_encryption, enable_legacy_ssl_storage_port).applyConfigInternal();
|
algorithm, store_type, require_client_auth,
|
||||||
|
require_endpoint_verification, optional, internode_encryption,
|
||||||
|
enable_legacy_ssl_storage_port).applyConfigInternal();
|
||||||
}
|
}
|
||||||
|
|
||||||
public ServerEncryptionOptions withKeyStorePassword(String keystore_password)
|
public ServerEncryptionOptions withKeyStorePassword(String keystore_password)
|
||||||
{
|
{
|
||||||
return new ServerEncryptionOptions(keystore, keystore_password, truststore, truststore_password, cipher_suites,
|
return new ServerEncryptionOptions(ssl_context_factory, keystore, keystore_password, truststore,
|
||||||
protocol, accepted_protocols, algorithm, store_type, require_client_auth, require_endpoint_verification,
|
truststore_password, cipher_suites, protocol, accepted_protocols,
|
||||||
optional, internode_encryption, enable_legacy_ssl_storage_port).applyConfigInternal();
|
algorithm, store_type, require_client_auth,
|
||||||
|
require_endpoint_verification, optional, internode_encryption,
|
||||||
|
enable_legacy_ssl_storage_port).applyConfigInternal();
|
||||||
}
|
}
|
||||||
|
|
||||||
public ServerEncryptionOptions withTrustStore(String truststore)
|
public ServerEncryptionOptions withTrustStore(String truststore)
|
||||||
{
|
{
|
||||||
return new ServerEncryptionOptions(keystore, keystore_password, truststore, truststore_password, cipher_suites,
|
return new ServerEncryptionOptions(ssl_context_factory, keystore, keystore_password, truststore,
|
||||||
protocol, accepted_protocols, algorithm, store_type, require_client_auth, require_endpoint_verification,
|
truststore_password, cipher_suites, protocol, accepted_protocols,
|
||||||
optional, internode_encryption, enable_legacy_ssl_storage_port).applyConfigInternal();
|
algorithm, store_type, require_client_auth,
|
||||||
|
require_endpoint_verification, optional, internode_encryption,
|
||||||
|
enable_legacy_ssl_storage_port).applyConfigInternal();
|
||||||
}
|
}
|
||||||
|
|
||||||
public ServerEncryptionOptions withTrustStorePassword(String truststore_password)
|
public ServerEncryptionOptions withTrustStorePassword(String truststore_password)
|
||||||
{
|
{
|
||||||
return new ServerEncryptionOptions(keystore, keystore_password, truststore, truststore_password, cipher_suites,
|
return new ServerEncryptionOptions(ssl_context_factory, keystore, keystore_password, truststore,
|
||||||
protocol, accepted_protocols, algorithm, store_type, require_client_auth, require_endpoint_verification,
|
truststore_password, cipher_suites, protocol, accepted_protocols,
|
||||||
optional, internode_encryption, enable_legacy_ssl_storage_port).applyConfigInternal();
|
algorithm, store_type, require_client_auth,
|
||||||
|
require_endpoint_verification, optional, internode_encryption,
|
||||||
|
enable_legacy_ssl_storage_port).applyConfigInternal();
|
||||||
}
|
}
|
||||||
|
|
||||||
public ServerEncryptionOptions withCipherSuites(List<String> cipher_suites)
|
public ServerEncryptionOptions withCipherSuites(List<String> cipher_suites)
|
||||||
{
|
{
|
||||||
return new ServerEncryptionOptions(keystore, keystore_password, truststore, truststore_password, cipher_suites,
|
return new ServerEncryptionOptions(ssl_context_factory, keystore, keystore_password, truststore,
|
||||||
protocol, accepted_protocols, algorithm, store_type, require_client_auth, require_endpoint_verification,
|
truststore_password, cipher_suites, protocol, accepted_protocols,
|
||||||
optional, internode_encryption, enable_legacy_ssl_storage_port).applyConfigInternal();
|
algorithm, store_type, require_client_auth,
|
||||||
|
require_endpoint_verification, optional, internode_encryption,
|
||||||
|
enable_legacy_ssl_storage_port).applyConfigInternal();
|
||||||
}
|
}
|
||||||
|
|
||||||
public ServerEncryptionOptions withCipherSuites(String ... cipher_suites)
|
public ServerEncryptionOptions withCipherSuites(String ... cipher_suites)
|
||||||
{
|
{
|
||||||
return new ServerEncryptionOptions(keystore, keystore_password, truststore, truststore_password, ImmutableList.copyOf(cipher_suites),
|
return new ServerEncryptionOptions(ssl_context_factory, keystore, keystore_password, truststore,
|
||||||
protocol, accepted_protocols, algorithm, store_type, require_client_auth, require_endpoint_verification,
|
truststore_password, Arrays.asList(cipher_suites), protocol,
|
||||||
optional, internode_encryption, enable_legacy_ssl_storage_port).applyConfigInternal();
|
accepted_protocols, algorithm, store_type, require_client_auth,
|
||||||
|
require_endpoint_verification, optional, internode_encryption,
|
||||||
|
enable_legacy_ssl_storage_port).applyConfigInternal();
|
||||||
}
|
}
|
||||||
|
|
||||||
public ServerEncryptionOptions withProtocol(String protocol)
|
public ServerEncryptionOptions withProtocol(String protocol)
|
||||||
{
|
{
|
||||||
return new ServerEncryptionOptions(keystore, keystore_password, truststore, truststore_password, cipher_suites,
|
return new ServerEncryptionOptions(ssl_context_factory, keystore, keystore_password, truststore,
|
||||||
protocol, accepted_protocols, algorithm, store_type, require_client_auth, require_endpoint_verification,
|
truststore_password, cipher_suites, protocol, accepted_protocols,
|
||||||
optional, internode_encryption, enable_legacy_ssl_storage_port).applyConfigInternal();
|
algorithm, store_type, require_client_auth,
|
||||||
|
require_endpoint_verification, optional, internode_encryption,
|
||||||
|
enable_legacy_ssl_storage_port).applyConfigInternal();
|
||||||
}
|
}
|
||||||
|
|
||||||
public ServerEncryptionOptions withAcceptedProtocols(List<String> accepted_protocols)
|
public ServerEncryptionOptions withAcceptedProtocols(List<String> accepted_protocols)
|
||||||
{
|
{
|
||||||
return new ServerEncryptionOptions(keystore, keystore_password, truststore, truststore_password, cipher_suites,
|
return new ServerEncryptionOptions(ssl_context_factory, keystore, keystore_password, truststore,
|
||||||
protocol, accepted_protocols == null ? null : ImmutableList.copyOf(accepted_protocols),
|
truststore_password, cipher_suites, protocol, accepted_protocols,
|
||||||
algorithm, store_type, require_client_auth, require_endpoint_verification,
|
algorithm, store_type, require_client_auth,
|
||||||
optional, internode_encryption, enable_legacy_ssl_storage_port).applyConfigInternal();
|
require_endpoint_verification, optional, internode_encryption,
|
||||||
|
enable_legacy_ssl_storage_port).applyConfigInternal();
|
||||||
}
|
}
|
||||||
|
|
||||||
public ServerEncryptionOptions withAlgorithm(String algorithm)
|
public ServerEncryptionOptions withAlgorithm(String algorithm)
|
||||||
{
|
{
|
||||||
return new ServerEncryptionOptions(keystore, keystore_password, truststore, truststore_password, cipher_suites,
|
return new ServerEncryptionOptions(ssl_context_factory, keystore, keystore_password, truststore,
|
||||||
protocol, accepted_protocols, algorithm, store_type, require_client_auth, require_endpoint_verification,
|
truststore_password, cipher_suites, protocol, accepted_protocols,
|
||||||
optional, internode_encryption, enable_legacy_ssl_storage_port).applyConfigInternal();
|
algorithm, store_type, require_client_auth,
|
||||||
|
require_endpoint_verification, optional, internode_encryption,
|
||||||
|
enable_legacy_ssl_storage_port).applyConfigInternal();
|
||||||
}
|
}
|
||||||
|
|
||||||
public ServerEncryptionOptions withStoreType(String store_type)
|
public ServerEncryptionOptions withStoreType(String store_type)
|
||||||
{
|
{
|
||||||
return new ServerEncryptionOptions(keystore, keystore_password, truststore, truststore_password, cipher_suites,
|
return new ServerEncryptionOptions(ssl_context_factory, keystore, keystore_password, truststore,
|
||||||
protocol, accepted_protocols, algorithm, store_type, require_client_auth, require_endpoint_verification,
|
truststore_password, cipher_suites, protocol, accepted_protocols,
|
||||||
optional, internode_encryption, enable_legacy_ssl_storage_port).applyConfigInternal();
|
algorithm, store_type, require_client_auth,
|
||||||
|
require_endpoint_verification, optional, internode_encryption,
|
||||||
|
enable_legacy_ssl_storage_port).applyConfigInternal();
|
||||||
}
|
}
|
||||||
|
|
||||||
public ServerEncryptionOptions withRequireClientAuth(boolean require_client_auth)
|
public ServerEncryptionOptions withRequireClientAuth(boolean require_client_auth)
|
||||||
{
|
{
|
||||||
return new ServerEncryptionOptions(keystore, keystore_password, truststore, truststore_password, cipher_suites,
|
return new ServerEncryptionOptions(ssl_context_factory, keystore, keystore_password, truststore,
|
||||||
protocol, accepted_protocols, algorithm, store_type, require_client_auth, require_endpoint_verification,
|
truststore_password, cipher_suites, protocol, accepted_protocols,
|
||||||
optional, internode_encryption, enable_legacy_ssl_storage_port).applyConfigInternal();
|
algorithm, store_type, require_client_auth,
|
||||||
|
require_endpoint_verification, optional, internode_encryption,
|
||||||
|
enable_legacy_ssl_storage_port).applyConfigInternal();
|
||||||
}
|
}
|
||||||
|
|
||||||
public ServerEncryptionOptions withRequireEndpointVerification(boolean require_endpoint_verification)
|
public ServerEncryptionOptions withRequireEndpointVerification(boolean require_endpoint_verification)
|
||||||
{
|
{
|
||||||
return new ServerEncryptionOptions(keystore, keystore_password, truststore, truststore_password, cipher_suites,
|
return new ServerEncryptionOptions(ssl_context_factory, keystore, keystore_password, truststore,
|
||||||
protocol, accepted_protocols, algorithm, store_type, require_client_auth, require_endpoint_verification,
|
truststore_password, cipher_suites, protocol, accepted_protocols,
|
||||||
optional, internode_encryption, enable_legacy_ssl_storage_port).applyConfigInternal();
|
algorithm, store_type, require_client_auth,
|
||||||
|
require_endpoint_verification, optional, internode_encryption,
|
||||||
|
enable_legacy_ssl_storage_port).applyConfigInternal();
|
||||||
}
|
}
|
||||||
|
|
||||||
public ServerEncryptionOptions withOptional(boolean optional)
|
public ServerEncryptionOptions withOptional(boolean optional)
|
||||||
{
|
{
|
||||||
return new ServerEncryptionOptions(keystore, keystore_password, truststore, truststore_password, cipher_suites,
|
return new ServerEncryptionOptions(ssl_context_factory, keystore, keystore_password, truststore,
|
||||||
protocol, accepted_protocols, algorithm, store_type, require_client_auth, require_endpoint_verification,
|
truststore_password, cipher_suites, protocol, accepted_protocols,
|
||||||
optional, internode_encryption, enable_legacy_ssl_storage_port).applyConfigInternal();
|
algorithm, store_type, require_client_auth,
|
||||||
|
require_endpoint_verification, optional, internode_encryption,
|
||||||
|
enable_legacy_ssl_storage_port).applyConfigInternal();
|
||||||
}
|
}
|
||||||
|
|
||||||
public ServerEncryptionOptions withInternodeEncryption(InternodeEncryption internode_encryption)
|
public ServerEncryptionOptions withInternodeEncryption(InternodeEncryption internode_encryption)
|
||||||
{
|
{
|
||||||
return new ServerEncryptionOptions(keystore, keystore_password, truststore, truststore_password, cipher_suites,
|
return new ServerEncryptionOptions(ssl_context_factory, keystore, keystore_password, truststore,
|
||||||
protocol, accepted_protocols, algorithm, store_type, require_client_auth, require_endpoint_verification,
|
truststore_password, cipher_suites, protocol, accepted_protocols,
|
||||||
optional, internode_encryption, enable_legacy_ssl_storage_port).applyConfigInternal();
|
algorithm, store_type, require_client_auth,
|
||||||
|
require_endpoint_verification, optional, internode_encryption,
|
||||||
|
enable_legacy_ssl_storage_port).applyConfigInternal();
|
||||||
}
|
}
|
||||||
|
|
||||||
public ServerEncryptionOptions withLegacySslStoragePort(boolean enable_legacy_ssl_storage_port)
|
public ServerEncryptionOptions withLegacySslStoragePort(boolean enable_legacy_ssl_storage_port)
|
||||||
{
|
{
|
||||||
return new ServerEncryptionOptions(keystore, keystore_password, truststore, truststore_password, cipher_suites,
|
return new ServerEncryptionOptions(ssl_context_factory, keystore, keystore_password, truststore,
|
||||||
protocol, accepted_protocols, algorithm, store_type, require_client_auth, require_endpoint_verification,
|
truststore_password, cipher_suites, protocol, accepted_protocols,
|
||||||
optional, internode_encryption, enable_legacy_ssl_storage_port).applyConfigInternal();
|
algorithm, store_type, require_client_auth,
|
||||||
|
require_endpoint_verification, optional, internode_encryption,
|
||||||
|
enable_legacy_ssl_storage_port).applyConfigInternal();
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -59,6 +59,12 @@ public class ParameterizedClass
|
||||||
return Objects.equal(class_name, that.class_name) && Objects.equal(parameters, that.parameters);
|
return Objects.equal(class_name, that.class_name) && Objects.equal(parameters, that.parameters);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int hashCode()
|
||||||
|
{
|
||||||
|
return Objects.hashCode(class_name, parameters);
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public String toString()
|
public String toString()
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -50,6 +50,7 @@ import org.apache.cassandra.config.EncryptionOptions;
|
||||||
import org.apache.cassandra.exceptions.ConfigurationException;
|
import org.apache.cassandra.exceptions.ConfigurationException;
|
||||||
import org.apache.cassandra.locator.InetAddressAndPort;
|
import org.apache.cassandra.locator.InetAddressAndPort;
|
||||||
import org.apache.cassandra.net.OutboundConnectionSettings.Framing;
|
import org.apache.cassandra.net.OutboundConnectionSettings.Framing;
|
||||||
|
import org.apache.cassandra.security.ISslContextFactory;
|
||||||
import org.apache.cassandra.security.SSLFactory;
|
import org.apache.cassandra.security.SSLFactory;
|
||||||
import org.apache.cassandra.streaming.async.StreamingInboundHandler;
|
import org.apache.cassandra.streaming.async.StreamingInboundHandler;
|
||||||
import org.apache.cassandra.utils.memory.BufferPools;
|
import org.apache.cassandra.utils.memory.BufferPools;
|
||||||
|
|
@ -495,8 +496,9 @@ public class InboundConnectionInitiator
|
||||||
|
|
||||||
private static SslHandler getSslHandler(String description, Channel channel, EncryptionOptions.ServerEncryptionOptions encryptionOptions) throws IOException
|
private static SslHandler getSslHandler(String description, Channel channel, EncryptionOptions.ServerEncryptionOptions encryptionOptions) throws IOException
|
||||||
{
|
{
|
||||||
final boolean buildTrustStore = true;
|
final boolean verifyPeerCertificate = true;
|
||||||
SslContext sslContext = SSLFactory.getOrCreateSslContext(encryptionOptions, buildTrustStore, SSLFactory.SocketType.SERVER);
|
SslContext sslContext = SSLFactory.getOrCreateSslContext(encryptionOptions, verifyPeerCertificate,
|
||||||
|
ISslContextFactory.SocketType.SERVER);
|
||||||
InetSocketAddress peer = encryptionOptions.require_endpoint_verification ? (InetSocketAddress) channel.remoteAddress() : null;
|
InetSocketAddress peer = encryptionOptions.require_endpoint_verification ? (InetSocketAddress) channel.remoteAddress() : null;
|
||||||
SslHandler sslHandler = newSslHandler(channel, sslContext, peer);
|
SslHandler sslHandler = newSslHandler(channel, sslContext, peer);
|
||||||
logger.trace("{} inbound netty SslContext: context={}, engine={}", description, sslContext.getClass().getName(), sslHandler.engine().getClass().getName());
|
logger.trace("{} inbound netty SslContext: context={}, engine={}", description, sslContext.getClass().getName(), sslHandler.engine().getClass().getName());
|
||||||
|
|
|
||||||
|
|
@ -53,6 +53,7 @@ import org.apache.cassandra.locator.InetAddressAndPort;
|
||||||
import org.apache.cassandra.net.HandshakeProtocol.Initiate;
|
import org.apache.cassandra.net.HandshakeProtocol.Initiate;
|
||||||
import org.apache.cassandra.net.OutboundConnectionInitiator.Result.MessagingSuccess;
|
import org.apache.cassandra.net.OutboundConnectionInitiator.Result.MessagingSuccess;
|
||||||
import org.apache.cassandra.net.OutboundConnectionInitiator.Result.StreamingSuccess;
|
import org.apache.cassandra.net.OutboundConnectionInitiator.Result.StreamingSuccess;
|
||||||
|
import org.apache.cassandra.security.ISslContextFactory;
|
||||||
import org.apache.cassandra.security.SSLFactory;
|
import org.apache.cassandra.security.SSLFactory;
|
||||||
import org.apache.cassandra.utils.JVMStabilityInspector;
|
import org.apache.cassandra.utils.JVMStabilityInspector;
|
||||||
import org.apache.cassandra.utils.memory.BufferPools;
|
import org.apache.cassandra.utils.memory.BufferPools;
|
||||||
|
|
@ -201,7 +202,8 @@ public class OutboundConnectionInitiator<SuccessType extends OutboundConnectionI
|
||||||
if (settings.withEncryption())
|
if (settings.withEncryption())
|
||||||
{
|
{
|
||||||
// check if we should actually encrypt this connection
|
// check if we should actually encrypt this connection
|
||||||
SslContext sslContext = SSLFactory.getOrCreateSslContext(settings.encryption, true, SSLFactory.SocketType.CLIENT);
|
SslContext sslContext = SSLFactory.getOrCreateSslContext(settings.encryption, true,
|
||||||
|
ISslContextFactory.SocketType.CLIENT);
|
||||||
// for some reason channel.remoteAddress() will return null
|
// for some reason channel.remoteAddress() will return null
|
||||||
InetAddressAndPort address = settings.to;
|
InetAddressAndPort address = settings.to;
|
||||||
InetSocketAddress peer = settings.encryption.require_endpoint_verification ? new InetSocketAddress(address.address, address.port) : null;
|
InetSocketAddress peer = settings.encryption.require_endpoint_verification ? new InetSocketAddress(address.address, address.port) : null;
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,266 @@
|
||||||
|
/*
|
||||||
|
* 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.security;
|
||||||
|
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.List;
|
||||||
|
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 com.google.common.collect.ImmutableList;
|
||||||
|
|
||||||
|
import io.netty.handler.ssl.CipherSuiteFilter;
|
||||||
|
import io.netty.handler.ssl.ClientAuth;
|
||||||
|
import io.netty.handler.ssl.OpenSsl;
|
||||||
|
import io.netty.handler.ssl.SslContext;
|
||||||
|
import io.netty.handler.ssl.SslContextBuilder;
|
||||||
|
import io.netty.handler.ssl.SslProvider;
|
||||||
|
import org.apache.cassandra.config.Config;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Abstract class implementing {@code ISslContextFacotry} to provide most of the functionality that any
|
||||||
|
* implementation might need. This does not assume any file-based credentials for keys/certs hence provide a good base
|
||||||
|
* for any implementation that only need to customize the loading of keys/certs in a custom way.
|
||||||
|
* <p>
|
||||||
|
* {@code CAUTION:} While this is extremely useful abstraction, please be careful if you need to modify this class
|
||||||
|
* given possible custom implementations out there!
|
||||||
|
*
|
||||||
|
* @see DefaultSslContextFactory
|
||||||
|
*/
|
||||||
|
abstract public class AbstractSslContextFactory implements ISslContextFactory
|
||||||
|
{
|
||||||
|
/*
|
||||||
|
This list is substituted in configurations that have explicitly specified the original "TLS" default,
|
||||||
|
by extracting it from the default "TLS" SSL Context instance
|
||||||
|
*/
|
||||||
|
static protected final List<String> TLS_PROTOCOL_SUBSTITUTION = SSLFactory.tlsInstanceProtocolSubstitution();
|
||||||
|
|
||||||
|
protected boolean openSslIsAvailable;
|
||||||
|
|
||||||
|
protected final Map<String, Object> parameters;
|
||||||
|
protected final List<String> cipher_suites;
|
||||||
|
protected final String protocol;
|
||||||
|
protected final List<String> accepted_protocols;
|
||||||
|
protected final String algorithm;
|
||||||
|
protected final String store_type;
|
||||||
|
protected final boolean require_client_auth;
|
||||||
|
protected 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 protected and expose through isEnabled
|
||||||
|
so users of ServerEncryptionOptions can't accidentally use this when they should use isEnabled
|
||||||
|
Long term we need to refactor ClientEncryptionOptions and ServerEncryptionOptions to be separate
|
||||||
|
classes so we can choose appropriate configuration for each.
|
||||||
|
See CASSANDRA-15262 and CASSANDRA-15146
|
||||||
|
*/
|
||||||
|
protected Boolean enabled;
|
||||||
|
protected Boolean optional;
|
||||||
|
|
||||||
|
/* For test only */
|
||||||
|
protected AbstractSslContextFactory()
|
||||||
|
{
|
||||||
|
parameters = new HashMap<>();
|
||||||
|
cipher_suites = null;
|
||||||
|
protocol = null;
|
||||||
|
accepted_protocols = null;
|
||||||
|
algorithm = null;
|
||||||
|
store_type = "JKS";
|
||||||
|
require_client_auth = false;
|
||||||
|
require_endpoint_verification = false;
|
||||||
|
enabled = null;
|
||||||
|
optional = null;
|
||||||
|
deriveIfOpenSslAvailable();
|
||||||
|
}
|
||||||
|
|
||||||
|
protected AbstractSslContextFactory(Map<String, Object> parameters)
|
||||||
|
{
|
||||||
|
this.parameters = parameters;
|
||||||
|
cipher_suites = getStringList("cipher_suites");
|
||||||
|
protocol = getString("protocol");
|
||||||
|
accepted_protocols = getStringList("accepted_protocols");
|
||||||
|
algorithm = getString("algorithm");
|
||||||
|
store_type = getString("store_type", "JKS");
|
||||||
|
require_client_auth = getBoolean("require_client_auth", false);
|
||||||
|
require_endpoint_verification = getBoolean("require_endpoint_verification", false);
|
||||||
|
enabled = getBoolean("enabled");
|
||||||
|
optional = getBoolean("optional");
|
||||||
|
deriveIfOpenSslAvailable();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Dervies if {@code OpenSSL} is available. It allows in-jvm dtests to disable tcnative openssl support by
|
||||||
|
* setting {@code cassandra.disable_tcactive_openssl} system property as {@code true}. Otherwise, it creates a
|
||||||
|
* circular reference that prevents the instance class loader from being garbage collected.
|
||||||
|
*/
|
||||||
|
protected void deriveIfOpenSslAvailable()
|
||||||
|
{
|
||||||
|
if (Boolean.getBoolean(Config.PROPERTY_PREFIX + "disable_tcactive_openssl"))
|
||||||
|
openSslIsAvailable = false;
|
||||||
|
else
|
||||||
|
openSslIsAvailable = OpenSsl.isAvailable();
|
||||||
|
}
|
||||||
|
|
||||||
|
protected String getString(String key, String defaultValue)
|
||||||
|
{
|
||||||
|
return parameters.get(key) == null ? defaultValue : (String) parameters.get(key);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected String getString(String key)
|
||||||
|
{
|
||||||
|
return (String) parameters.get(key);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected List<String> getStringList(String key)
|
||||||
|
{
|
||||||
|
return (List<String>) parameters.get(key);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected Boolean getBoolean(String key, boolean defaultValue)
|
||||||
|
{
|
||||||
|
return parameters.get(key) == null ? defaultValue : (Boolean) parameters.get(key);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected Boolean getBoolean(String key)
|
||||||
|
{
|
||||||
|
return (Boolean) this.parameters.get(key);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public SSLContext createJSSESslContext(boolean verifyPeerCertificate) throws SSLException
|
||||||
|
{
|
||||||
|
TrustManager[] trustManagers = null;
|
||||||
|
if (verifyPeerCertificate)
|
||||||
|
trustManagers = buildTrustManagerFactory().getTrustManagers();
|
||||||
|
|
||||||
|
KeyManagerFactory kmf = buildKeyManagerFactory();
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
SSLContext ctx = SSLContext.getInstance("TLS");
|
||||||
|
ctx.init(kmf.getKeyManagers(), trustManagers, null);
|
||||||
|
return ctx;
|
||||||
|
}
|
||||||
|
catch (Exception e)
|
||||||
|
{
|
||||||
|
throw new SSLException("Error creating/initializing the SSL Context", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public SslContext createNettySslContext(boolean verifyPeerCertificate, SocketType socketType,
|
||||||
|
CipherSuiteFilter cipherFilter) throws SSLException
|
||||||
|
{
|
||||||
|
/*
|
||||||
|
There is a case where the netty/openssl combo might not support using KeyManagerFactory. Specifically,
|
||||||
|
I've seen this with the netty-tcnative dynamic openssl implementation. Using the netty-tcnative
|
||||||
|
static-boringssl works fine with KeyManagerFactory. If we want to support all of the netty-tcnative
|
||||||
|
options, we would need to fall back to passing in a file reference for both a x509 and PKCS#8 private
|
||||||
|
key file in PEM format (see {@link SslContextBuilder#forServer(File, File, String)}). However, we are
|
||||||
|
not supporting that now to keep the config/yaml API simple.
|
||||||
|
*/
|
||||||
|
KeyManagerFactory kmf = buildKeyManagerFactory();
|
||||||
|
SslContextBuilder builder;
|
||||||
|
if (socketType == SocketType.SERVER)
|
||||||
|
{
|
||||||
|
builder = SslContextBuilder.forServer(kmf).clientAuth(this.require_client_auth ? ClientAuth.REQUIRE :
|
||||||
|
ClientAuth.NONE);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
builder = SslContextBuilder.forClient().keyManager(kmf);
|
||||||
|
}
|
||||||
|
|
||||||
|
builder.sslProvider(getSslProvider()).protocols(getAcceptedProtocols());
|
||||||
|
|
||||||
|
// only set the cipher suites if the operator has explicity configured values for it; else, use the default
|
||||||
|
// for each ssl implemention (jdk or openssl)
|
||||||
|
if (cipher_suites != null && !cipher_suites.isEmpty())
|
||||||
|
builder.ciphers(cipher_suites, cipherFilter);
|
||||||
|
|
||||||
|
if (verifyPeerCertificate)
|
||||||
|
builder.trustManager(buildTrustManagerFactory());
|
||||||
|
|
||||||
|
return builder.build();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Combine the pre-4.0 protocol field with the accepted_protocols list, substituting a list of
|
||||||
|
* explicit protocols for the previous catchall default of "TLS"
|
||||||
|
*
|
||||||
|
* @return array of protocol names suitable for passing to SslContextBuilder.protocols, or null if the default
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
public List<String> getAcceptedProtocols()
|
||||||
|
{
|
||||||
|
if (accepted_protocols == null)
|
||||||
|
{
|
||||||
|
if (protocol == null)
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
// TLS is accepted by SSLContext.getInstance as a shorthand for give me an engine that
|
||||||
|
// can speak some TLS protocols. It is not supported by SSLEngine.setAcceptedProtocols
|
||||||
|
// so substitute if the user hasn't provided an accepted protocol configuration
|
||||||
|
else if (protocol.equalsIgnoreCase("TLS"))
|
||||||
|
{
|
||||||
|
return TLS_PROTOCOL_SUBSTITUTION;
|
||||||
|
}
|
||||||
|
else // the user was trying to limit to a single specific protocol, so try that
|
||||||
|
{
|
||||||
|
return ImmutableList.of(protocol);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (protocol != null && !protocol.equalsIgnoreCase("TLS") &&
|
||||||
|
accepted_protocols.stream().noneMatch(ap -> ap.equalsIgnoreCase(protocol)))
|
||||||
|
{
|
||||||
|
// If the user provided a non-generic default protocol, append it to accepted_protocols - they wanted
|
||||||
|
// it after all.
|
||||||
|
return ImmutableList.<String>builder().addAll(accepted_protocols).add(protocol).build();
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
return accepted_protocols;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public List<String> getCipherSuites()
|
||||||
|
{
|
||||||
|
return cipher_suites;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns {@link SslProvider} to be used to build Netty's SslContext.
|
||||||
|
*
|
||||||
|
* @return appropriate SslProvider
|
||||||
|
*/
|
||||||
|
protected SslProvider getSslProvider()
|
||||||
|
{
|
||||||
|
return openSslIsAvailable ? SslProvider.OPENSSL : SslProvider.JDK;
|
||||||
|
}
|
||||||
|
|
||||||
|
abstract protected KeyManagerFactory buildKeyManagerFactory() throws SSLException;
|
||||||
|
|
||||||
|
abstract protected TrustManagerFactory buildTrustManagerFactory() throws SSLException;
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,33 @@
|
||||||
|
/*
|
||||||
|
* 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.security;
|
||||||
|
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Cassandra's default implementation class for the configuration key {@code ssl_context_factory}. It uses
|
||||||
|
* file based keystores.
|
||||||
|
*/
|
||||||
|
public final class DefaultSslContextFactory extends FileBasedSslContextFactory
|
||||||
|
{
|
||||||
|
public DefaultSslContextFactory(Map<String, Object> parameters)
|
||||||
|
{
|
||||||
|
super(parameters);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,211 @@
|
||||||
|
/*
|
||||||
|
* 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.security;
|
||||||
|
|
||||||
|
import java.io.File;
|
||||||
|
import java.io.InputStream;
|
||||||
|
import java.nio.file.Files;
|
||||||
|
import java.nio.file.Paths;
|
||||||
|
import java.security.KeyStore;
|
||||||
|
import java.security.cert.X509Certificate;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.Date;
|
||||||
|
import java.util.Enumeration;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
import javax.net.ssl.KeyManagerFactory;
|
||||||
|
import javax.net.ssl.SSLException;
|
||||||
|
import javax.net.ssl.TrustManagerFactory;
|
||||||
|
|
||||||
|
import com.google.common.annotations.VisibleForTesting;
|
||||||
|
import org.slf4j.Logger;
|
||||||
|
import org.slf4j.LoggerFactory;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Abstract implementation for {@link ISslContextFactory} using file based, standard keystore format with the ability
|
||||||
|
* to hot-reload the files upon file changes (detected by the {@code last modified timestamp}).
|
||||||
|
* <p>
|
||||||
|
* {@code CAUTION:} While this is a useful abstraction, please be careful if you need to modify this class
|
||||||
|
* given possible custom implementations out there!
|
||||||
|
*/
|
||||||
|
abstract public class FileBasedSslContextFactory extends AbstractSslContextFactory
|
||||||
|
{
|
||||||
|
private static final Logger logger = LoggerFactory.getLogger(FileBasedSslContextFactory.class);
|
||||||
|
|
||||||
|
@VisibleForTesting
|
||||||
|
protected volatile boolean checkedExpiry = false;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* List of files that trigger hot reloading of SSL certificates
|
||||||
|
*/
|
||||||
|
protected volatile List<HotReloadableFile> hotReloadableFiles = new ArrayList<>();
|
||||||
|
|
||||||
|
protected String keystore;
|
||||||
|
protected String keystore_password;
|
||||||
|
protected String truststore;
|
||||||
|
protected String truststore_password;
|
||||||
|
|
||||||
|
public FileBasedSslContextFactory()
|
||||||
|
{
|
||||||
|
keystore = "conf/.keystore";
|
||||||
|
keystore_password = "cassandra";
|
||||||
|
truststore = "conf/.truststore";
|
||||||
|
truststore_password = "cassandra";
|
||||||
|
}
|
||||||
|
|
||||||
|
public FileBasedSslContextFactory(Map<String, Object> parameters)
|
||||||
|
{
|
||||||
|
super(parameters);
|
||||||
|
keystore = getString("keystore");
|
||||||
|
keystore_password = getString("keystore_password");
|
||||||
|
truststore = getString("truststore");
|
||||||
|
truststore_password = getString("truststore_password");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean shouldReload()
|
||||||
|
{
|
||||||
|
return hotReloadableFiles.stream().anyMatch(HotReloadableFile::shouldReload);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean hasKeystore()
|
||||||
|
{
|
||||||
|
return keystore != null && new File(keystore).exists();
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean hasTruststore()
|
||||||
|
{
|
||||||
|
return truststore != null && new File(truststore).exists();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public synchronized void initHotReloading()
|
||||||
|
{
|
||||||
|
boolean hasKeystore = hasKeystore();
|
||||||
|
boolean hasTruststore = hasTruststore();
|
||||||
|
|
||||||
|
if (hasKeystore || hasTruststore)
|
||||||
|
{
|
||||||
|
List<HotReloadableFile> fileList = new ArrayList<>();
|
||||||
|
if (hasKeystore)
|
||||||
|
{
|
||||||
|
fileList.add(new HotReloadableFile(keystore));
|
||||||
|
}
|
||||||
|
if (hasTruststore)
|
||||||
|
{
|
||||||
|
fileList.add(new HotReloadableFile(truststore));
|
||||||
|
}
|
||||||
|
hotReloadableFiles = fileList;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Builds required KeyManagerFactory from the file based keystore. It also checks for the PrivateKey's certificate's
|
||||||
|
* expiry and logs {@code warning} for each expired PrivateKey's certitificate.
|
||||||
|
*
|
||||||
|
* @return KeyManagerFactory built from the file based keystore.
|
||||||
|
* @throws SSLException if any issues encountered during the build process
|
||||||
|
*/
|
||||||
|
protected KeyManagerFactory buildKeyManagerFactory() throws SSLException
|
||||||
|
{
|
||||||
|
try (InputStream ksf = Files.newInputStream(Paths.get(keystore)))
|
||||||
|
{
|
||||||
|
KeyManagerFactory kmf = KeyManagerFactory.getInstance(
|
||||||
|
algorithm == null ? KeyManagerFactory.getDefaultAlgorithm() : algorithm);
|
||||||
|
KeyStore ks = KeyStore.getInstance(store_type);
|
||||||
|
ks.load(ksf, keystore_password.toCharArray());
|
||||||
|
if (!checkedExpiry)
|
||||||
|
{
|
||||||
|
for (Enumeration<String> aliases = ks.aliases(); aliases.hasMoreElements(); )
|
||||||
|
{
|
||||||
|
String alias = aliases.nextElement();
|
||||||
|
if (ks.getCertificate(alias).getType().equals("X.509"))
|
||||||
|
{
|
||||||
|
Date expires = ((X509Certificate) ks.getCertificate(alias)).getNotAfter();
|
||||||
|
if (expires.before(new Date()))
|
||||||
|
logger.warn("Certificate for {} expired on {}", alias, expires);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
checkedExpiry = true;
|
||||||
|
}
|
||||||
|
kmf.init(ks, keystore_password.toCharArray());
|
||||||
|
return kmf;
|
||||||
|
}
|
||||||
|
catch (Exception e)
|
||||||
|
{
|
||||||
|
throw new SSLException("failed to build key manager store for secure connections", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Builds TrustManagerFactory from the file based truststore.
|
||||||
|
*
|
||||||
|
* @return TrustManagerFactory from the file based truststore
|
||||||
|
* @throws SSLException if any issues encountered during the build process
|
||||||
|
*/
|
||||||
|
protected TrustManagerFactory buildTrustManagerFactory() throws SSLException
|
||||||
|
{
|
||||||
|
try (InputStream tsf = Files.newInputStream(Paths.get(truststore)))
|
||||||
|
{
|
||||||
|
TrustManagerFactory tmf = TrustManagerFactory.getInstance(
|
||||||
|
algorithm == null ? TrustManagerFactory.getDefaultAlgorithm() : algorithm);
|
||||||
|
KeyStore ts = KeyStore.getInstance(store_type);
|
||||||
|
ts.load(tsf, truststore_password.toCharArray());
|
||||||
|
tmf.init(ts);
|
||||||
|
return tmf;
|
||||||
|
}
|
||||||
|
catch (Exception e)
|
||||||
|
{
|
||||||
|
throw new SSLException("failed to build trust manager store for secure connections", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Helper class for hot reloading SSL Contexts
|
||||||
|
*/
|
||||||
|
private static class HotReloadableFile
|
||||||
|
{
|
||||||
|
private final File file;
|
||||||
|
private volatile long lastModTime;
|
||||||
|
|
||||||
|
HotReloadableFile(String path)
|
||||||
|
{
|
||||||
|
file = new File(path);
|
||||||
|
lastModTime = file.lastModified();
|
||||||
|
}
|
||||||
|
|
||||||
|
boolean shouldReload()
|
||||||
|
{
|
||||||
|
long curModTime = file.lastModified();
|
||||||
|
boolean result = curModTime != lastModTime;
|
||||||
|
lastModTime = curModTime;
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String toString()
|
||||||
|
{
|
||||||
|
return "HotReloadableFile{" +
|
||||||
|
"file=" + file +
|
||||||
|
", lastModTime=" + lastModTime +
|
||||||
|
'}';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,124 @@
|
||||||
|
/*
|
||||||
|
* 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.security;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
import javax.net.ssl.SSLContext;
|
||||||
|
import javax.net.ssl.SSLException;
|
||||||
|
|
||||||
|
import io.netty.handler.ssl.CipherSuiteFilter;
|
||||||
|
import io.netty.handler.ssl.SslContext;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The purpose of this interface is to provide pluggable mechanism for creating custom JSSE and Netty SSLContext
|
||||||
|
* objects. Please use the Cassandra configuration key {@code ssl_context_factory} as part of {@code
|
||||||
|
* client_encryption_options}/{@code server_encryption_options} and provide a custom class-name implementing this
|
||||||
|
* interface with parameters to be used to plugin a your own way to load the SSLContext.
|
||||||
|
* <p>
|
||||||
|
* Implementation of this interface must have a constructor with argument of type {@code Map<String,Object>} to allow
|
||||||
|
* custom parameters, needed by the implementation, to be passed from the yaml configuration. Common SSL
|
||||||
|
* configurations like {@code protocol, algorithm, cipher_suites, accepted_protocols, require_client_auth,
|
||||||
|
* require_endpoint_verification, enabled, optional} will also be passed to that map by Cassanddra.
|
||||||
|
* <p>
|
||||||
|
* Since on top of Netty, Cassandra is internally using JSSE SSLContext also for certain use-cases- this interface
|
||||||
|
* has methods for both.
|
||||||
|
* <p>
|
||||||
|
* Below is an example of how to configure a custom implementation with parameters
|
||||||
|
* <pre>
|
||||||
|
* ssl_context_factory:
|
||||||
|
* class_name: org.apache.cassandra.security.YourSslContextFactoryImpl
|
||||||
|
* parameters:
|
||||||
|
* key1: "value1"
|
||||||
|
* key2: "value2"
|
||||||
|
* key3: "value3"
|
||||||
|
* </pre>
|
||||||
|
*/
|
||||||
|
public interface ISslContextFactory
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Creates JSSE SSLContext.
|
||||||
|
*
|
||||||
|
* @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
|
||||||
|
*/
|
||||||
|
SSLContext createJSSESslContext(boolean verifyPeerCertificate) throws SSLException;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates Netty's SslContext object.
|
||||||
|
*
|
||||||
|
* @param verifyPeerCertificate {@code true} if SSL peer's certificate needs to be verified; {@code false} 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
|
||||||
|
*/
|
||||||
|
SslContext createNettySslContext(boolean verifyPeerCertificate, SocketType socketType,
|
||||||
|
CipherSuiteFilter cipherFilter) throws SSLException;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initializes hot reloading of the security keys/certs. The implementation must guarantee this to be thread safe.
|
||||||
|
*
|
||||||
|
* @throws SSLException
|
||||||
|
*/
|
||||||
|
void initHotReloading() throws SSLException;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns if any changes require the reloading of the SSL context returned by this factory.
|
||||||
|
* This will be called by Cassandra's periodic polling for any potential changes that will reload the SSL context.
|
||||||
|
* However only newer connections established after the reload will use the reloaded SSL context.
|
||||||
|
*
|
||||||
|
* @return {@code true} if SSL Context needs to be reload; {@code false} otherwise
|
||||||
|
*/
|
||||||
|
boolean shouldReload();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns if this factory uses private keystore.
|
||||||
|
*
|
||||||
|
* @return {@code true} by default unless the implementation overrides this
|
||||||
|
*/
|
||||||
|
default boolean hasKeystore()
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the prepared list of accepted protocols.
|
||||||
|
*
|
||||||
|
* @return array of protocol names suitable for passing to Netty's SslContextBuilder.protocols, or null if the
|
||||||
|
* default
|
||||||
|
*/
|
||||||
|
List<String> getAcceptedProtocols();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the list of cipher suites supported by the implementation.
|
||||||
|
*
|
||||||
|
* @return List of supported cipher suites
|
||||||
|
*/
|
||||||
|
List<String> getCipherSuites();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Indicates if the process holds the inbound/listening (Server) end of the socket or the outbound side (Client).
|
||||||
|
*/
|
||||||
|
enum SocketType
|
||||||
|
{
|
||||||
|
SERVER, CLIENT;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -18,48 +18,33 @@
|
||||||
package org.apache.cassandra.security;
|
package org.apache.cassandra.security;
|
||||||
|
|
||||||
|
|
||||||
import java.io.File;
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.io.InputStream;
|
|
||||||
import java.nio.file.Files;
|
|
||||||
import java.nio.file.Paths;
|
|
||||||
import java.security.KeyStore;
|
|
||||||
import java.security.cert.X509Certificate;
|
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.Arrays;
|
import java.util.Arrays;
|
||||||
import java.util.Date;
|
|
||||||
import java.util.Enumeration;
|
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Objects;
|
import java.util.Objects;
|
||||||
import java.util.Set;
|
import java.util.Set;
|
||||||
import java.util.concurrent.ConcurrentHashMap;
|
import java.util.concurrent.ConcurrentHashMap;
|
||||||
import java.util.concurrent.TimeUnit;
|
import java.util.concurrent.TimeUnit;
|
||||||
import java.util.stream.Collectors;
|
import java.util.stream.Collectors;
|
||||||
import javax.net.ssl.KeyManagerFactory;
|
|
||||||
import javax.net.ssl.SSLContext;
|
import javax.net.ssl.SSLContext;
|
||||||
import javax.net.ssl.SSLEngine;
|
import javax.net.ssl.SSLEngine;
|
||||||
import javax.net.ssl.SSLParameters;
|
import javax.net.ssl.SSLParameters;
|
||||||
import javax.net.ssl.SSLSocket;
|
import javax.net.ssl.SSLSocket;
|
||||||
import javax.net.ssl.TrustManager;
|
|
||||||
import javax.net.ssl.TrustManagerFactory;
|
|
||||||
|
|
||||||
import com.google.common.annotations.VisibleForTesting;
|
|
||||||
import com.google.common.collect.ImmutableList;
|
|
||||||
import org.slf4j.Logger;
|
import org.slf4j.Logger;
|
||||||
import org.slf4j.LoggerFactory;
|
import org.slf4j.LoggerFactory;
|
||||||
|
|
||||||
import io.netty.buffer.ByteBufAllocator;
|
import io.netty.buffer.ByteBufAllocator;
|
||||||
import io.netty.handler.ssl.CipherSuiteFilter;
|
import io.netty.handler.ssl.CipherSuiteFilter;
|
||||||
import io.netty.handler.ssl.ClientAuth;
|
|
||||||
import io.netty.handler.ssl.OpenSsl;
|
import io.netty.handler.ssl.OpenSsl;
|
||||||
import io.netty.handler.ssl.SslContext;
|
import io.netty.handler.ssl.SslContext;
|
||||||
import io.netty.handler.ssl.SslContextBuilder;
|
|
||||||
import io.netty.handler.ssl.SslProvider;
|
|
||||||
import io.netty.util.ReferenceCountUtil;
|
import io.netty.util.ReferenceCountUtil;
|
||||||
import org.apache.cassandra.concurrent.ScheduledExecutors;
|
import org.apache.cassandra.concurrent.ScheduledExecutors;
|
||||||
import org.apache.cassandra.config.Config;
|
import org.apache.cassandra.config.Config;
|
||||||
import org.apache.cassandra.config.DatabaseDescriptor;
|
import org.apache.cassandra.config.DatabaseDescriptor;
|
||||||
import org.apache.cassandra.config.EncryptionOptions;
|
import org.apache.cassandra.config.EncryptionOptions;
|
||||||
|
import org.apache.cassandra.security.ISslContextFactory.SocketType;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A Factory for providing and setting up client {@link SSLSocket}s. Also provides
|
* A Factory for providing and setting up client {@link SSLSocket}s. Also provides
|
||||||
|
|
@ -73,18 +58,6 @@ public final class SSLFactory
|
||||||
{
|
{
|
||||||
private static final Logger logger = LoggerFactory.getLogger(SSLFactory.class);
|
private static final Logger logger = LoggerFactory.getLogger(SSLFactory.class);
|
||||||
|
|
||||||
/**
|
|
||||||
* Indicates if the process holds the inbound/listening end of the socket ({@link SocketType#SERVER})), or the
|
|
||||||
* outbound side ({@link SocketType#CLIENT}).
|
|
||||||
*/
|
|
||||||
public enum SocketType
|
|
||||||
{
|
|
||||||
SERVER, CLIENT
|
|
||||||
}
|
|
||||||
|
|
||||||
@VisibleForTesting
|
|
||||||
static volatile boolean checkedExpiry = false;
|
|
||||||
|
|
||||||
// Isolate calls to OpenSsl.isAvailable to allow in-jvm dtests to disable tcnative openssl
|
// Isolate calls to OpenSsl.isAvailable to allow in-jvm dtests to disable tcnative openssl
|
||||||
// support. It creates a circular reference that prevents the instance class loader from being
|
// support. It creates a circular reference that prevents the instance class loader from being
|
||||||
// garbage collected.
|
// garbage collected.
|
||||||
|
|
@ -110,11 +83,6 @@ public final class SSLFactory
|
||||||
*/
|
*/
|
||||||
private static final ConcurrentHashMap<CacheKey, SslContext> cachedSslContexts = new ConcurrentHashMap<>();
|
private static final ConcurrentHashMap<CacheKey, SslContext> cachedSslContexts = new ConcurrentHashMap<>();
|
||||||
|
|
||||||
/**
|
|
||||||
* List of files that trigger hot reloading of SSL certificates
|
|
||||||
*/
|
|
||||||
private static volatile List<HotReloadableFile> hotReloadableFiles = ImmutableList.of();
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Default initial delay for hot reloading
|
* Default initial delay for hot reloading
|
||||||
*/
|
*/
|
||||||
|
|
@ -130,38 +98,6 @@ public final class SSLFactory
|
||||||
*/
|
*/
|
||||||
private static boolean isHotReloadingInitialized = false;
|
private static boolean isHotReloadingInitialized = false;
|
||||||
|
|
||||||
/**
|
|
||||||
* Helper class for hot reloading SSL Contexts
|
|
||||||
*/
|
|
||||||
private static class HotReloadableFile
|
|
||||||
{
|
|
||||||
private final File file;
|
|
||||||
private volatile long lastModTime;
|
|
||||||
|
|
||||||
HotReloadableFile(String path)
|
|
||||||
{
|
|
||||||
file = new File(path);
|
|
||||||
lastModTime = file.lastModified();
|
|
||||||
}
|
|
||||||
|
|
||||||
boolean shouldReload()
|
|
||||||
{
|
|
||||||
long curModTime = file.lastModified();
|
|
||||||
boolean result = curModTime != lastModTime;
|
|
||||||
lastModTime = curModTime;
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public String toString()
|
|
||||||
{
|
|
||||||
return "HotReloadableFile{" +
|
|
||||||
"file=" + file +
|
|
||||||
", lastModTime=" + lastModTime +
|
|
||||||
'}';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Provides the list of protocols that would have been supported if "TLS" was selected as the
|
/** Provides the list of protocols that would have been supported if "TLS" was selected as the
|
||||||
* protocol before the change for CASSANDRA-13325 that expects explicit protocol versions.
|
* protocol before the change for CASSANDRA-13325 that expects explicit protocol versions.
|
||||||
* @return list of enabled protocol names
|
* @return list of enabled protocol names
|
||||||
|
|
@ -185,100 +121,25 @@ public final class SSLFactory
|
||||||
/**
|
/**
|
||||||
* Create a JSSE {@link SSLContext}.
|
* Create a JSSE {@link SSLContext}.
|
||||||
*/
|
*/
|
||||||
public static SSLContext createSSLContext(EncryptionOptions options, boolean buildTruststore) throws IOException
|
public static SSLContext createSSLContext(EncryptionOptions options, boolean verifyPeerCertificate) throws IOException
|
||||||
{
|
{
|
||||||
TrustManager[] trustManagers = null;
|
return options.sslContextFactoryInstance.createJSSESslContext(verifyPeerCertificate);
|
||||||
if (buildTruststore)
|
|
||||||
trustManagers = buildTrustManagerFactory(options).getTrustManagers();
|
|
||||||
|
|
||||||
KeyManagerFactory kmf = buildKeyManagerFactory(options);
|
|
||||||
|
|
||||||
try
|
|
||||||
{
|
|
||||||
SSLContext ctx = SSLContext.getInstance("TLS");
|
|
||||||
ctx.init(kmf.getKeyManagers(), trustManagers, null);
|
|
||||||
return ctx;
|
|
||||||
}
|
|
||||||
catch (Exception e)
|
|
||||||
{
|
|
||||||
throw new IOException("Error creating/initializing the SSL Context", e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
static TrustManagerFactory buildTrustManagerFactory(EncryptionOptions options) throws IOException
|
|
||||||
{
|
|
||||||
try (InputStream tsf = Files.newInputStream(Paths.get(options.truststore)))
|
|
||||||
{
|
|
||||||
TrustManagerFactory tmf = TrustManagerFactory.getInstance(
|
|
||||||
options.algorithm == null ? TrustManagerFactory.getDefaultAlgorithm() : options.algorithm);
|
|
||||||
KeyStore ts = KeyStore.getInstance(options.store_type);
|
|
||||||
ts.load(tsf, options.truststore_password.toCharArray());
|
|
||||||
tmf.init(ts);
|
|
||||||
return tmf;
|
|
||||||
}
|
|
||||||
catch (Exception e)
|
|
||||||
{
|
|
||||||
throw new IOException("failed to build trust manager store for secure connections", e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
static KeyManagerFactory buildKeyManagerFactory(EncryptionOptions options) throws IOException
|
|
||||||
{
|
|
||||||
try (InputStream ksf = Files.newInputStream(Paths.get(options.keystore)))
|
|
||||||
{
|
|
||||||
KeyManagerFactory kmf = KeyManagerFactory.getInstance(
|
|
||||||
options.algorithm == null ? KeyManagerFactory.getDefaultAlgorithm() : options.algorithm);
|
|
||||||
KeyStore ks = KeyStore.getInstance(options.store_type);
|
|
||||||
ks.load(ksf, options.keystore_password.toCharArray());
|
|
||||||
if (!checkedExpiry)
|
|
||||||
{
|
|
||||||
for (Enumeration<String> aliases = ks.aliases(); aliases.hasMoreElements(); )
|
|
||||||
{
|
|
||||||
String alias = aliases.nextElement();
|
|
||||||
if (ks.getCertificate(alias).getType().equals("X.509"))
|
|
||||||
{
|
|
||||||
Date expires = ((X509Certificate) ks.getCertificate(alias)).getNotAfter();
|
|
||||||
if (expires.before(new Date()))
|
|
||||||
logger.warn("Certificate for {} expired on {}", alias, expires);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
checkedExpiry = true;
|
|
||||||
}
|
|
||||||
kmf.init(ks, options.keystore_password.toCharArray());
|
|
||||||
return kmf;
|
|
||||||
}
|
|
||||||
catch (Exception e)
|
|
||||||
{
|
|
||||||
throw new IOException("failed to build key manager store for secure connections", e);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* get a netty {@link SslContext} instance
|
* get a netty {@link SslContext} instance
|
||||||
*/
|
*/
|
||||||
public static SslContext getOrCreateSslContext(EncryptionOptions options, boolean buildTruststore,
|
public static SslContext getOrCreateSslContext(EncryptionOptions options, boolean verifyPeerCertificate,
|
||||||
SocketType socketType) throws IOException
|
SocketType socketType) throws IOException
|
||||||
{
|
{
|
||||||
return getOrCreateSslContext(options, buildTruststore, socketType, openSslIsAvailable());
|
CacheKey key = new CacheKey(options, socketType);
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get a netty {@link SslContext} instance.
|
|
||||||
*/
|
|
||||||
@VisibleForTesting
|
|
||||||
static SslContext getOrCreateSslContext(EncryptionOptions options,
|
|
||||||
boolean buildTruststore,
|
|
||||||
SocketType socketType,
|
|
||||||
boolean useOpenSsl) throws IOException
|
|
||||||
{
|
|
||||||
CacheKey key = new CacheKey(options, socketType, useOpenSsl);
|
|
||||||
SslContext sslContext;
|
SslContext sslContext;
|
||||||
|
|
||||||
sslContext = cachedSslContexts.get(key);
|
sslContext = cachedSslContexts.get(key);
|
||||||
if (sslContext != null)
|
if (sslContext != null)
|
||||||
return sslContext;
|
return sslContext;
|
||||||
|
|
||||||
sslContext = createNettySslContext(options, buildTruststore, socketType, useOpenSsl);
|
sslContext = createNettySslContext(options, verifyPeerCertificate, socketType);
|
||||||
|
|
||||||
SslContext previous = cachedSslContexts.putIfAbsent(key, sslContext);
|
SslContext previous = cachedSslContexts.putIfAbsent(key, sslContext);
|
||||||
if (previous == null)
|
if (previous == null)
|
||||||
|
|
@ -291,52 +152,21 @@ public final class SSLFactory
|
||||||
/**
|
/**
|
||||||
* Create a Netty {@link SslContext}
|
* Create a Netty {@link SslContext}
|
||||||
*/
|
*/
|
||||||
static SslContext createNettySslContext(EncryptionOptions options, boolean buildTruststore,
|
static SslContext createNettySslContext(EncryptionOptions options, boolean verifyPeerCertificate,
|
||||||
SocketType socketType, boolean useOpenSsl) throws IOException
|
SocketType socketType) throws IOException
|
||||||
{
|
{
|
||||||
return createNettySslContext(options, buildTruststore, socketType, useOpenSsl,
|
return createNettySslContext(options, verifyPeerCertificate, socketType,
|
||||||
LoggingCipherSuiteFilter.QUIET_FILTER);
|
LoggingCipherSuiteFilter.QUIET_FILTER);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Create a Netty {@link SslContext} with a supplied cipherFilter
|
* Create a Netty {@link SslContext} with a supplied cipherFilter
|
||||||
*/
|
*/
|
||||||
static SslContext createNettySslContext(EncryptionOptions options, boolean buildTruststore,
|
static SslContext createNettySslContext(EncryptionOptions options, boolean verifyPeerCertificate,
|
||||||
SocketType socketType, boolean useOpenSsl, CipherSuiteFilter cipherFilter) throws IOException
|
SocketType socketType, CipherSuiteFilter cipherFilter) throws IOException
|
||||||
{
|
{
|
||||||
/*
|
return options.sslContextFactoryInstance.createNettySslContext(verifyPeerCertificate, socketType,
|
||||||
There is a case where the netty/openssl combo might not support using KeyManagerFactory. specifically,
|
cipherFilter);
|
||||||
I've seen this with the netty-tcnative dynamic openssl implementation. using the netty-tcnative static-boringssl
|
|
||||||
works fine with KeyManagerFactory. If we want to support all of the netty-tcnative options, we would need
|
|
||||||
to fall back to passing in a file reference for both a x509 and PKCS#8 private key file in PEM format (see
|
|
||||||
{@link SslContextBuilder#forServer(File, File, String)}). However, we are not supporting that now to keep
|
|
||||||
the config/yaml API simple.
|
|
||||||
*/
|
|
||||||
KeyManagerFactory kmf = buildKeyManagerFactory(options);
|
|
||||||
SslContextBuilder builder;
|
|
||||||
if (socketType == SocketType.SERVER)
|
|
||||||
{
|
|
||||||
builder = SslContextBuilder.forServer(kmf);
|
|
||||||
builder.clientAuth(options.require_client_auth ? ClientAuth.REQUIRE : ClientAuth.NONE);
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
builder = SslContextBuilder.forClient().keyManager(kmf);
|
|
||||||
}
|
|
||||||
|
|
||||||
builder.sslProvider(useOpenSsl ? SslProvider.OPENSSL : SslProvider.JDK);
|
|
||||||
|
|
||||||
builder.protocols(options.acceptedProtocols());
|
|
||||||
|
|
||||||
// only set the cipher suites if the opertor has explicity configured values for it; else, use the default
|
|
||||||
// for each ssl implemention (jdk or openssl)
|
|
||||||
if (options.cipher_suites != null && !options.cipher_suites.isEmpty())
|
|
||||||
builder.ciphers(options.cipher_suites, cipherFilter);
|
|
||||||
|
|
||||||
if (buildTruststore)
|
|
||||||
builder.trustManager(buildTrustManagerFactory(options));
|
|
||||||
|
|
||||||
return builder.build();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -351,21 +181,58 @@ public final class SSLFactory
|
||||||
if (!isHotReloadingInitialized)
|
if (!isHotReloadingInitialized)
|
||||||
throw new IllegalStateException("Hot reloading functionality has not been initialized.");
|
throw new IllegalStateException("Hot reloading functionality has not been initialized.");
|
||||||
|
|
||||||
logger.debug("Checking whether certificates have been updated {}", hotReloadableFiles);
|
logger.debug("Checking whether certificates have been updated for server {} and client {}",
|
||||||
|
serverOpts.sslContextFactoryInstance.getClass().getName(), clientOpts.sslContextFactoryInstance.getClass().getName());
|
||||||
|
|
||||||
if (hotReloadableFiles.stream().anyMatch(HotReloadableFile::shouldReload))
|
if (serverOpts != null)
|
||||||
{
|
{
|
||||||
logger.info("SSL certificates have been updated. Reseting the ssl contexts for new connections.");
|
checkCertFilesForHotReloading(serverOpts, "server_encryption_options", true);
|
||||||
try
|
}
|
||||||
|
if (clientOpts != null)
|
||||||
|
{
|
||||||
|
checkCertFilesForHotReloading(clientOpts, "client_encryption_options", clientOpts.require_client_auth);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void checkCertFilesForHotReloading(EncryptionOptions options, String contextDescription,
|
||||||
|
boolean verifyPeerCertificate)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
if (options.sslContextFactoryInstance.shouldReload())
|
||||||
{
|
{
|
||||||
validateSslCerts(serverOpts, clientOpts);
|
logger.info("SSL certificates have been updated for {}. Resetting the ssl contexts for new " +
|
||||||
cachedSslContexts.clear();
|
"connections.", options.getClass().getName());
|
||||||
}
|
validateSslContext(contextDescription, options, verifyPeerCertificate, false);
|
||||||
catch(Exception e)
|
clearSslContextCache(options);
|
||||||
{
|
|
||||||
logger.error("Failed to hot reload the SSL Certificates! Please check the certificate files.", e);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
catch(Exception e)
|
||||||
|
{
|
||||||
|
logger.error("Failed to hot reload the SSL Certificates! Please check the certificate files.", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This clears the cache of Netty's SslContext objects for Client and Server sockets. This is made publically
|
||||||
|
* available so that any {@link ISslContextFactory}'s implementation can call this to handle any special scenario
|
||||||
|
* to invalidate the SslContext cache.
|
||||||
|
* This should be used with caution since the purpose of this cache is save costly creation of Netty's SslContext
|
||||||
|
* objects and this essentially results in re-creating it.
|
||||||
|
*/
|
||||||
|
public static void clearSslContextCache()
|
||||||
|
{
|
||||||
|
cachedSslContexts.clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void clearSslContextCache(EncryptionOptions options)
|
||||||
|
{
|
||||||
|
cachedSslContexts.forEachKey(1, cacheKey -> {
|
||||||
|
if (cacheKey.encryptionOptions.equals(options))
|
||||||
|
{
|
||||||
|
cachedSslContexts.remove(cacheKey);
|
||||||
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -383,22 +250,14 @@ public final class SSLFactory
|
||||||
|
|
||||||
logger.debug("Initializing hot reloading SSLContext");
|
logger.debug("Initializing hot reloading SSLContext");
|
||||||
|
|
||||||
List<HotReloadableFile> fileList = new ArrayList<>();
|
if ( serverOpts != null && serverOpts.tlsEncryptionPolicy() != EncryptionOptions.TlsEncryptionPolicy.UNENCRYPTED) {
|
||||||
|
serverOpts.sslContextFactoryInstance.initHotReloading();
|
||||||
if (serverOpts != null && serverOpts.tlsEncryptionPolicy() != EncryptionOptions.TlsEncryptionPolicy.UNENCRYPTED)
|
|
||||||
{
|
|
||||||
fileList.add(new HotReloadableFile(serverOpts.keystore));
|
|
||||||
fileList.add(new HotReloadableFile(serverOpts.truststore));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (clientOpts != null && clientOpts.tlsEncryptionPolicy() != EncryptionOptions.TlsEncryptionPolicy.UNENCRYPTED)
|
if ( clientOpts != null && clientOpts.tlsEncryptionPolicy() != EncryptionOptions.TlsEncryptionPolicy.UNENCRYPTED) {
|
||||||
{
|
clientOpts.sslContextFactoryInstance.initHotReloading();
|
||||||
fileList.add(new HotReloadableFile(clientOpts.keystore));
|
|
||||||
fileList.add(new HotReloadableFile(clientOpts.truststore));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
hotReloadableFiles = ImmutableList.copyOf(fileList);
|
|
||||||
|
|
||||||
if (!isHotReloadingInitialized)
|
if (!isHotReloadingInitialized)
|
||||||
{
|
{
|
||||||
ScheduledExecutors.scheduledTasks
|
ScheduledExecutors.scheduledTasks
|
||||||
|
|
@ -485,7 +344,7 @@ public final class SSLFactory
|
||||||
return !string.equals("SSLv2Hello");
|
return !string.equals("SSLv2Hello");
|
||||||
}
|
}
|
||||||
|
|
||||||
public static void validateSslContext(String contextDescription, EncryptionOptions options, boolean buildTrustStore, boolean logProtocolAndCiphers) throws IOException
|
public static void validateSslContext(String contextDescription, EncryptionOptions options, boolean verifyPeerCertificate, boolean logProtocolAndCiphers) throws IOException
|
||||||
{
|
{
|
||||||
if (options != null && options.tlsEncryptionPolicy() != EncryptionOptions.TlsEncryptionPolicy.UNENCRYPTED)
|
if (options != null && options.tlsEncryptionPolicy() != EncryptionOptions.TlsEncryptionPolicy.UNENCRYPTED)
|
||||||
{
|
{
|
||||||
|
|
@ -493,7 +352,7 @@ public final class SSLFactory
|
||||||
{
|
{
|
||||||
CipherSuiteFilter loggingCipherSuiteFilter = logProtocolAndCiphers ? new LoggingCipherSuiteFilter(contextDescription)
|
CipherSuiteFilter loggingCipherSuiteFilter = logProtocolAndCiphers ? new LoggingCipherSuiteFilter(contextDescription)
|
||||||
: LoggingCipherSuiteFilter.QUIET_FILTER;
|
: LoggingCipherSuiteFilter.QUIET_FILTER;
|
||||||
SslContext serverSslContext = createNettySslContext(options, buildTrustStore, SocketType.SERVER, openSslIsAvailable(), loggingCipherSuiteFilter);
|
SslContext serverSslContext = createNettySslContext(options, verifyPeerCertificate, SocketType.SERVER, loggingCipherSuiteFilter);
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
SSLEngine engine = serverSslContext.newEngine(ByteBufAllocator.DEFAULT);
|
SSLEngine engine = serverSslContext.newEngine(ByteBufAllocator.DEFAULT);
|
||||||
|
|
@ -538,7 +397,7 @@ public final class SSLFactory
|
||||||
}
|
}
|
||||||
|
|
||||||
// Make sure it is possible to build the client context too
|
// Make sure it is possible to build the client context too
|
||||||
SslContext clientSslContext = createNettySslContext(options, buildTrustStore, SocketType.CLIENT, openSslIsAvailable());
|
SslContext clientSslContext = createNettySslContext(options, verifyPeerCertificate, SocketType.CLIENT);
|
||||||
ReferenceCountUtil.release(clientSslContext);
|
ReferenceCountUtil.release(clientSslContext);
|
||||||
}
|
}
|
||||||
catch (Exception e)
|
catch (Exception e)
|
||||||
|
|
@ -561,13 +420,11 @@ public final class SSLFactory
|
||||||
{
|
{
|
||||||
private final EncryptionOptions encryptionOptions;
|
private final EncryptionOptions encryptionOptions;
|
||||||
private final SocketType socketType;
|
private final SocketType socketType;
|
||||||
private final boolean useOpenSSL;
|
|
||||||
|
|
||||||
public CacheKey(EncryptionOptions encryptionOptions, SocketType socketType, boolean useOpenSSL)
|
public CacheKey(EncryptionOptions encryptionOptions, SocketType socketType)
|
||||||
{
|
{
|
||||||
this.encryptionOptions = encryptionOptions;
|
this.encryptionOptions = encryptionOptions;
|
||||||
this.socketType = socketType;
|
this.socketType = socketType;
|
||||||
this.useOpenSSL = useOpenSSL;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public boolean equals(Object o)
|
public boolean equals(Object o)
|
||||||
|
|
@ -576,7 +433,6 @@ public final class SSLFactory
|
||||||
if (o == null || getClass() != o.getClass()) return false;
|
if (o == null || getClass() != o.getClass()) return false;
|
||||||
CacheKey cacheKey = (CacheKey) o;
|
CacheKey cacheKey = (CacheKey) o;
|
||||||
return (socketType == cacheKey.socketType &&
|
return (socketType == cacheKey.socketType &&
|
||||||
useOpenSSL == cacheKey.useOpenSSL &&
|
|
||||||
Objects.equals(encryptionOptions, cacheKey.encryptionOptions));
|
Objects.equals(encryptionOptions, cacheKey.encryptionOptions));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -585,7 +441,6 @@ public final class SSLFactory
|
||||||
int result = 0;
|
int result = 0;
|
||||||
result += 31 * socketType.hashCode();
|
result += 31 * socketType.hashCode();
|
||||||
result += 31 * encryptionOptions.hashCode();
|
result += 31 * encryptionOptions.hashCode();
|
||||||
result += 31 * Boolean.hashCode(useOpenSSL);
|
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -44,6 +44,7 @@ import io.netty.util.Version;
|
||||||
import org.apache.cassandra.config.DatabaseDescriptor;
|
import org.apache.cassandra.config.DatabaseDescriptor;
|
||||||
import org.apache.cassandra.config.EncryptionOptions;
|
import org.apache.cassandra.config.EncryptionOptions;
|
||||||
import org.apache.cassandra.net.*;
|
import org.apache.cassandra.net.*;
|
||||||
|
import org.apache.cassandra.security.ISslContextFactory;
|
||||||
import org.apache.cassandra.security.SSLFactory;
|
import org.apache.cassandra.security.SSLFactory;
|
||||||
import org.apache.cassandra.transport.messages.StartupMessage;
|
import org.apache.cassandra.transport.messages.StartupMessage;
|
||||||
|
|
||||||
|
|
@ -163,7 +164,7 @@ public class PipelineConfigurator
|
||||||
return channel -> {
|
return channel -> {
|
||||||
SslContext sslContext = SSLFactory.getOrCreateSslContext(encryptionOptions,
|
SslContext sslContext = SSLFactory.getOrCreateSslContext(encryptionOptions,
|
||||||
encryptionOptions.require_client_auth,
|
encryptionOptions.require_client_auth,
|
||||||
SSLFactory.SocketType.SERVER);
|
ISslContextFactory.SocketType.SERVER);
|
||||||
|
|
||||||
channel.pipeline().addFirst(SSL_HANDLER, new ByteToMessageDecoder()
|
channel.pipeline().addFirst(SSL_HANDLER, new ByteToMessageDecoder()
|
||||||
{
|
{
|
||||||
|
|
@ -197,7 +198,7 @@ public class PipelineConfigurator
|
||||||
return channel -> {
|
return channel -> {
|
||||||
SslContext sslContext = SSLFactory.getOrCreateSslContext(encryptionOptions,
|
SslContext sslContext = SSLFactory.getOrCreateSslContext(encryptionOptions,
|
||||||
encryptionOptions.require_client_auth,
|
encryptionOptions.require_client_auth,
|
||||||
SSLFactory.SocketType.SERVER);
|
ISslContextFactory.SocketType.SERVER);
|
||||||
channel.pipeline().addFirst(SSL_HANDLER, sslContext.newHandler(channel.alloc()));
|
channel.pipeline().addFirst(SSL_HANDLER, sslContext.newHandler(channel.alloc()));
|
||||||
};
|
};
|
||||||
default:
|
default:
|
||||||
|
|
|
||||||
|
|
@ -47,6 +47,7 @@ import org.apache.cassandra.config.EncryptionOptions;
|
||||||
import org.apache.cassandra.cql3.QueryOptions;
|
import org.apache.cassandra.cql3.QueryOptions;
|
||||||
import org.apache.cassandra.db.ConsistencyLevel;
|
import org.apache.cassandra.db.ConsistencyLevel;
|
||||||
import org.apache.cassandra.net.*;
|
import org.apache.cassandra.net.*;
|
||||||
|
import org.apache.cassandra.security.ISslContextFactory;
|
||||||
import org.apache.cassandra.security.SSLFactory;
|
import org.apache.cassandra.security.SSLFactory;
|
||||||
import org.apache.cassandra.transport.messages.*;
|
import org.apache.cassandra.transport.messages.*;
|
||||||
|
|
||||||
|
|
@ -620,7 +621,7 @@ public class SimpleClient implements Closeable
|
||||||
{
|
{
|
||||||
super.initChannel(channel);
|
super.initChannel(channel);
|
||||||
SslContext sslContext = SSLFactory.getOrCreateSslContext(encryptionOptions, encryptionOptions.require_client_auth,
|
SslContext sslContext = SSLFactory.getOrCreateSslContext(encryptionOptions, encryptionOptions.require_client_auth,
|
||||||
SSLFactory.SocketType.CLIENT);
|
ISslContextFactory.SocketType.CLIENT);
|
||||||
channel.pipeline().addFirst("ssl", sslContext.newHandler(channel.alloc()));
|
channel.pipeline().addFirst("ssl", sslContext.newHandler(channel.alloc()));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -42,8 +42,8 @@ import org.slf4j.LoggerFactory;
|
||||||
|
|
||||||
import com.fasterxml.jackson.core.JsonFactory;
|
import com.fasterxml.jackson.core.JsonFactory;
|
||||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||||
import org.apache.cassandra.auth.AllowAllNetworkAuthorizer;
|
|
||||||
import org.apache.cassandra.audit.IAuditLogger;
|
import org.apache.cassandra.audit.IAuditLogger;
|
||||||
|
import org.apache.cassandra.auth.AllowAllNetworkAuthorizer;
|
||||||
import org.apache.cassandra.auth.IAuthenticator;
|
import org.apache.cassandra.auth.IAuthenticator;
|
||||||
import org.apache.cassandra.auth.IAuthorizer;
|
import org.apache.cassandra.auth.IAuthorizer;
|
||||||
import org.apache.cassandra.auth.INetworkAuthorizer;
|
import org.apache.cassandra.auth.INetworkAuthorizer;
|
||||||
|
|
@ -66,6 +66,7 @@ import org.apache.cassandra.io.util.DataOutputBuffer;
|
||||||
import org.apache.cassandra.io.util.DataOutputBufferFixed;
|
import org.apache.cassandra.io.util.DataOutputBufferFixed;
|
||||||
import org.apache.cassandra.io.util.FileUtils;
|
import org.apache.cassandra.io.util.FileUtils;
|
||||||
import org.apache.cassandra.locator.InetAddressAndPort;
|
import org.apache.cassandra.locator.InetAddressAndPort;
|
||||||
|
import org.apache.cassandra.security.ISslContextFactory;
|
||||||
|
|
||||||
import static org.apache.cassandra.config.CassandraRelevantProperties.LINE_SEPARATOR;
|
import static org.apache.cassandra.config.CassandraRelevantProperties.LINE_SEPARATOR;
|
||||||
import static org.apache.cassandra.config.CassandraRelevantProperties.USER_HOME;
|
import static org.apache.cassandra.config.CassandraRelevantProperties.USER_HOME;
|
||||||
|
|
@ -672,7 +673,7 @@ public class FBUtilities
|
||||||
}
|
}
|
||||||
return FBUtilities.construct(className, "network authorizer");
|
return FBUtilities.construct(className, "network authorizer");
|
||||||
}
|
}
|
||||||
|
|
||||||
public static IAuditLogger newAuditLogger(String className, Map<String, String> parameters) throws ConfigurationException
|
public static IAuditLogger newAuditLogger(String className, Map<String, String> parameters) throws ConfigurationException
|
||||||
{
|
{
|
||||||
if (!className.contains("."))
|
if (!className.contains("."))
|
||||||
|
|
@ -689,6 +690,22 @@ public class FBUtilities
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static ISslContextFactory newSslContextFactory(String className, Map<String,Object> parameters) throws ConfigurationException
|
||||||
|
{
|
||||||
|
if (!className.contains("."))
|
||||||
|
className = "org.apache.cassandra.security." + className;
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
Class<?> sslContextFactoryClass = Class.forName(className);
|
||||||
|
return (ISslContextFactory) sslContextFactoryClass.getConstructor(Map.class).newInstance(parameters);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
throw new ConfigurationException("Unable to create instance of ISslContextFactory for " + className, ex);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @return The Class for the given name.
|
* @return The Class for the given name.
|
||||||
* @param classname Fully qualified classname.
|
* @param classname Fully qualified classname.
|
||||||
|
|
|
||||||
|
|
@ -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.
|
||||||
|
#
|
||||||
|
|
||||||
|
#
|
||||||
|
# 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_sync_batch_window_in_ms: 1.0
|
||||||
|
commitlog_segment_size_in_mb: 5
|
||||||
|
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_in_kb: 4
|
||||||
|
saved_caches_directory: build/test/cassandra/saved_caches
|
||||||
|
data_file_directories:
|
||||||
|
- build/test/cassandra/data
|
||||||
|
disk_access_mode: mmap
|
||||||
|
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
|
||||||
|
client_encryption_options:
|
||||||
|
ssl_context_factory:
|
||||||
|
class_name: org.apache.cassandra.security.DummySslContextFactoryImpl
|
||||||
|
parameters:
|
||||||
|
key1: "value1"
|
||||||
|
key2: "value2"
|
||||||
|
key3: "value3"
|
||||||
|
truststore: conf/.truststore
|
||||||
|
truststore_password: cassandra
|
||||||
|
keystore: conf/.keystore
|
||||||
|
keystore_password: cassandra
|
||||||
|
server_encryption_options:
|
||||||
|
internode_encryption: none
|
||||||
|
keystore: conf/.keystore
|
||||||
|
keystore_password: cassandra
|
||||||
|
truststore: conf/.truststore
|
||||||
|
truststore_password: cassandra
|
||||||
|
incremental_backups: true
|
||||||
|
concurrent_compactors: 4
|
||||||
|
compaction_throughput_mb_per_sec: 0
|
||||||
|
row_cache_class_name: org.apache.cassandra.cache.OHCProvider
|
||||||
|
row_cache_size_in_mb: 16
|
||||||
|
enable_user_defined_functions: true
|
||||||
|
enable_scripted_user_defined_functions: true
|
||||||
|
prepared_statements_cache_size_mb: 1
|
||||||
|
corrupted_tombstone_strategy: exception
|
||||||
|
stream_entire_sstables: true
|
||||||
|
stream_throughput_outbound_megabits_per_sec: 200000000
|
||||||
|
enable_sasi_indexes: true
|
||||||
|
enable_materialized_views: true
|
||||||
|
file_cache_enabled: true
|
||||||
|
|
@ -0,0 +1,85 @@
|
||||||
|
#
|
||||||
|
# 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_sync_batch_window_in_ms: 1.0
|
||||||
|
commitlog_segment_size_in_mb: 5
|
||||||
|
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_in_kb: 4
|
||||||
|
saved_caches_directory: build/test/cassandra/saved_caches
|
||||||
|
data_file_directories:
|
||||||
|
- build/test/cassandra/data
|
||||||
|
disk_access_mode: mmap
|
||||||
|
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
|
||||||
|
client_encryption_options:
|
||||||
|
ssl_context_factory:
|
||||||
|
class_name: org.apache.cassandra.security.DummySslContextFactoryImpl
|
||||||
|
parameters:
|
||||||
|
key1: "value1"
|
||||||
|
key2: "value2"
|
||||||
|
key3: "value3"
|
||||||
|
keystore: dummy-keystore
|
||||||
|
server_encryption_options:
|
||||||
|
ssl_context_factory:
|
||||||
|
class_name: org.apache.cassandra.security.MissingSslContextFactoryImpl
|
||||||
|
parameters:
|
||||||
|
key1: "value1"
|
||||||
|
key2: "value2"
|
||||||
|
key3: "value3"
|
||||||
|
internode_encryption: none
|
||||||
|
keystore: conf/.keystore
|
||||||
|
keystore_password: cassandra
|
||||||
|
truststore: conf/.truststore
|
||||||
|
truststore_password: cassandra
|
||||||
|
incremental_backups: true
|
||||||
|
concurrent_compactors: 4
|
||||||
|
compaction_throughput_mb_per_sec: 0
|
||||||
|
row_cache_class_name: org.apache.cassandra.cache.OHCProvider
|
||||||
|
row_cache_size_in_mb: 16
|
||||||
|
enable_user_defined_functions: true
|
||||||
|
enable_scripted_user_defined_functions: true
|
||||||
|
prepared_statements_cache_size_mb: 1
|
||||||
|
corrupted_tombstone_strategy: exception
|
||||||
|
stream_entire_sstables: true
|
||||||
|
stream_throughput_outbound_megabits_per_sec: 200000000
|
||||||
|
enable_sasi_indexes: true
|
||||||
|
enable_materialized_views: true
|
||||||
|
file_cache_enabled: true
|
||||||
|
|
@ -50,6 +50,7 @@ import io.netty.util.concurrent.FutureListener;
|
||||||
import org.apache.cassandra.config.EncryptionOptions;
|
import org.apache.cassandra.config.EncryptionOptions;
|
||||||
import org.apache.cassandra.distributed.Cluster;
|
import org.apache.cassandra.distributed.Cluster;
|
||||||
import org.apache.cassandra.exceptions.ConfigurationException;
|
import org.apache.cassandra.exceptions.ConfigurationException;
|
||||||
|
import org.apache.cassandra.security.ISslContextFactory;
|
||||||
import org.apache.cassandra.security.SSLFactory;
|
import org.apache.cassandra.security.SSLFactory;
|
||||||
import org.apache.cassandra.utils.concurrent.SimpleCondition;
|
import org.apache.cassandra.utils.concurrent.SimpleCondition;
|
||||||
|
|
||||||
|
|
@ -194,8 +195,8 @@ public class AbstractEncryptionOptionsImpl extends TestBaseImpl
|
||||||
setProtocolAndCipher(null, null);
|
setProtocolAndCipher(null, null);
|
||||||
|
|
||||||
SslContext sslContext = SSLFactory.getOrCreateSslContext(
|
SslContext sslContext = SSLFactory.getOrCreateSslContext(
|
||||||
encryptionOptions.withAcceptedProtocols(acceptedProtocols).withCipherSuites(cipherSuites),
|
encryptionOptions.withAcceptedProtocols(acceptedProtocols).withCipherSuites(cipherSuites),
|
||||||
true, SSLFactory.SocketType.CLIENT);
|
true, ISslContextFactory.SocketType.CLIENT);
|
||||||
|
|
||||||
EventLoopGroup workerGroup = new NioEventLoopGroup();
|
EventLoopGroup workerGroup = new NioEventLoopGroup();
|
||||||
Bootstrap b = new Bootstrap();
|
Bootstrap b = new Bootstrap();
|
||||||
|
|
|
||||||
|
|
@ -149,6 +149,7 @@ public class DatabaseDescriptorRefTest
|
||||||
"org.apache.cassandra.locator.Replica",
|
"org.apache.cassandra.locator.Replica",
|
||||||
"org.apache.cassandra.locator.SimpleSeedProvider",
|
"org.apache.cassandra.locator.SimpleSeedProvider",
|
||||||
"org.apache.cassandra.locator.SeedProvider",
|
"org.apache.cassandra.locator.SeedProvider",
|
||||||
|
"org.apache.cassandra.security.ISslContextFactory",
|
||||||
"org.apache.cassandra.security.SSLFactory",
|
"org.apache.cassandra.security.SSLFactory",
|
||||||
"org.apache.cassandra.security.EncryptionContext",
|
"org.apache.cassandra.security.EncryptionContext",
|
||||||
"org.apache.cassandra.service.CacheService$CacheType",
|
"org.apache.cassandra.service.CacheService$CacheType",
|
||||||
|
|
|
||||||
|
|
@ -265,7 +265,7 @@ public class DatabaseDescriptorTest
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void testTokensFromString()
|
public void testTokensFromString()
|
||||||
{
|
{
|
||||||
|
|
@ -337,7 +337,7 @@ public class DatabaseDescriptorTest
|
||||||
testConfig.cas_contention_timeout_in_ms = DatabaseDescriptor.LOWEST_ACCEPTED_TIMEOUT + 1;
|
testConfig.cas_contention_timeout_in_ms = DatabaseDescriptor.LOWEST_ACCEPTED_TIMEOUT + 1;
|
||||||
testConfig.counter_write_request_timeout_in_ms = DatabaseDescriptor.LOWEST_ACCEPTED_TIMEOUT + 1;
|
testConfig.counter_write_request_timeout_in_ms = DatabaseDescriptor.LOWEST_ACCEPTED_TIMEOUT + 1;
|
||||||
testConfig.request_timeout_in_ms = DatabaseDescriptor.LOWEST_ACCEPTED_TIMEOUT + 1;
|
testConfig.request_timeout_in_ms = DatabaseDescriptor.LOWEST_ACCEPTED_TIMEOUT + 1;
|
||||||
|
|
||||||
assertTrue(testConfig.read_request_timeout_in_ms > DatabaseDescriptor.LOWEST_ACCEPTED_TIMEOUT);
|
assertTrue(testConfig.read_request_timeout_in_ms > DatabaseDescriptor.LOWEST_ACCEPTED_TIMEOUT);
|
||||||
assertTrue(testConfig.range_request_timeout_in_ms > DatabaseDescriptor.LOWEST_ACCEPTED_TIMEOUT);
|
assertTrue(testConfig.range_request_timeout_in_ms > DatabaseDescriptor.LOWEST_ACCEPTED_TIMEOUT);
|
||||||
assertTrue(testConfig.write_request_timeout_in_ms > DatabaseDescriptor.LOWEST_ACCEPTED_TIMEOUT);
|
assertTrue(testConfig.write_request_timeout_in_ms > DatabaseDescriptor.LOWEST_ACCEPTED_TIMEOUT);
|
||||||
|
|
@ -741,4 +741,15 @@ public class DatabaseDescriptorTest
|
||||||
conf.track_warnings.row_index_size.abort_threshold_kb = 2;
|
conf.track_warnings.row_index_size.abort_threshold_kb = 2;
|
||||||
DatabaseDescriptor.applyTrackWarningsValidations(conf);
|
DatabaseDescriptor.applyTrackWarningsValidations(conf);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testDefaultSslContextFactoryConfiguration() {
|
||||||
|
Config config = DatabaseDescriptor.loadConfig();
|
||||||
|
Assert.assertEquals("org.apache.cassandra.security.DefaultSslContextFactory",
|
||||||
|
config.client_encryption_options.ssl_context_factory.class_name);
|
||||||
|
Assert.assertTrue(config.client_encryption_options.ssl_context_factory.parameters.isEmpty());
|
||||||
|
Assert.assertEquals("org.apache.cassandra.security.DefaultSslContextFactory",
|
||||||
|
config.server_encryption_options.ssl_context_factory.class_name);
|
||||||
|
Assert.assertTrue(config.server_encryption_options.ssl_context_factory.parameters.isEmpty());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,142 @@
|
||||||
|
/*
|
||||||
|
* 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.config;
|
||||||
|
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
import org.junit.Test;
|
||||||
|
|
||||||
|
import org.apache.cassandra.security.DefaultSslContextFactory;
|
||||||
|
import org.apache.cassandra.security.DummySslContextFactoryImpl;
|
||||||
|
|
||||||
|
import static org.junit.Assert.assertEquals;
|
||||||
|
import static org.junit.Assert.assertNotEquals;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This class tests the equals and hashCode method of {@link EncryptionOptions} in order to make sure that the
|
||||||
|
* caching done in the {@link org.apache.cassandra.security.SSLFactory} doesn't break.
|
||||||
|
*/
|
||||||
|
public class EncryptionOptionsEqualityTest
|
||||||
|
{
|
||||||
|
@Test
|
||||||
|
public void testKeystoreOptions() {
|
||||||
|
EncryptionOptions encryptionOptions1 =
|
||||||
|
new EncryptionOptions()
|
||||||
|
.withStoreType("JKS")
|
||||||
|
.withKeyStore("test/conf/cassandra.keystore")
|
||||||
|
.withKeyStorePassword("cassandra")
|
||||||
|
.withTrustStore("test/conf/cassandra_ssl_test.truststore")
|
||||||
|
.withTrustStorePassword("cassandra")
|
||||||
|
.withProtocol("TLSv1.1")
|
||||||
|
.withRequireClientAuth(true)
|
||||||
|
.withRequireEndpointVerification(false);
|
||||||
|
|
||||||
|
EncryptionOptions encryptionOptions2 =
|
||||||
|
new EncryptionOptions()
|
||||||
|
.withStoreType("JKS")
|
||||||
|
.withKeyStore("test/conf/cassandra.keystore")
|
||||||
|
.withKeyStorePassword("cassandra")
|
||||||
|
.withTrustStore("test/conf/cassandra_ssl_test.truststore")
|
||||||
|
.withTrustStorePassword("cassandra")
|
||||||
|
.withProtocol("TLSv1.1")
|
||||||
|
.withRequireClientAuth(true)
|
||||||
|
.withRequireEndpointVerification(false);
|
||||||
|
|
||||||
|
assertEquals(encryptionOptions1, encryptionOptions2);
|
||||||
|
assertEquals(encryptionOptions1.hashCode(), encryptionOptions2.hashCode());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testSameCustomSslContextFactoryImplementation() {
|
||||||
|
|
||||||
|
Map<String,String> parameters1 = new HashMap<>();
|
||||||
|
parameters1.put("key1", "value1");
|
||||||
|
parameters1.put("key2", "value2");
|
||||||
|
EncryptionOptions encryptionOptions1 =
|
||||||
|
new EncryptionOptions()
|
||||||
|
.withSslContextFactory(new ParameterizedClass(DummySslContextFactoryImpl.class.getName(), parameters1))
|
||||||
|
.withProtocol("TLSv1.1")
|
||||||
|
.withRequireClientAuth(true)
|
||||||
|
.withRequireEndpointVerification(false);
|
||||||
|
|
||||||
|
Map<String,String> parameters2 = new HashMap<>();
|
||||||
|
parameters2.put("key1", "value1");
|
||||||
|
parameters2.put("key2", "value2");
|
||||||
|
EncryptionOptions encryptionOptions2 =
|
||||||
|
new EncryptionOptions()
|
||||||
|
.withSslContextFactory(new ParameterizedClass(DummySslContextFactoryImpl.class.getName(), parameters2))
|
||||||
|
.withProtocol("TLSv1.1")
|
||||||
|
.withRequireClientAuth(true)
|
||||||
|
.withRequireEndpointVerification(false);
|
||||||
|
|
||||||
|
assertEquals(encryptionOptions1, encryptionOptions2);
|
||||||
|
assertEquals(encryptionOptions1.hashCode(), encryptionOptions2.hashCode());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testDifferentCustomSslContextFactoryImplementations() {
|
||||||
|
|
||||||
|
Map<String,String> parameters1 = new HashMap<>();
|
||||||
|
parameters1.put("key1", "value1");
|
||||||
|
parameters1.put("key2", "value2");
|
||||||
|
EncryptionOptions encryptionOptions1 =
|
||||||
|
new EncryptionOptions()
|
||||||
|
.withSslContextFactory(new ParameterizedClass(DummySslContextFactoryImpl.class.getName(), parameters1))
|
||||||
|
.withProtocol("TLSv1.1")
|
||||||
|
.withRequireClientAuth(false)
|
||||||
|
.withRequireEndpointVerification(true);
|
||||||
|
|
||||||
|
Map<String,String> parameters2 = new HashMap<>();
|
||||||
|
parameters2.put("key1", "value1");
|
||||||
|
parameters2.put("key2", "value2");
|
||||||
|
EncryptionOptions encryptionOptions2 =
|
||||||
|
new EncryptionOptions()
|
||||||
|
.withSslContextFactory(new ParameterizedClass(DefaultSslContextFactory.class.getName(), parameters2))
|
||||||
|
.withProtocol("TLSv1.1")
|
||||||
|
.withRequireClientAuth(false)
|
||||||
|
.withRequireEndpointVerification(true);
|
||||||
|
|
||||||
|
assertNotEquals(encryptionOptions1, encryptionOptions2);
|
||||||
|
assertNotEquals(encryptionOptions1.hashCode(), encryptionOptions2.hashCode());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testDifferentCustomSslContextFactoryParameters() {
|
||||||
|
|
||||||
|
Map<String,String> parameters1 = new HashMap<>();
|
||||||
|
parameters1.put("key1", "value11");
|
||||||
|
parameters1.put("key2", "value12");
|
||||||
|
EncryptionOptions encryptionOptions1 =
|
||||||
|
new EncryptionOptions()
|
||||||
|
.withSslContextFactory(new ParameterizedClass(DummySslContextFactoryImpl.class.getName(), parameters1))
|
||||||
|
.withProtocol("TLSv1.1");
|
||||||
|
|
||||||
|
Map<String,String> parameters2 = new HashMap<>();
|
||||||
|
parameters2.put("key1", "value21");
|
||||||
|
parameters2.put("key2", "value22");
|
||||||
|
EncryptionOptions encryptionOptions2 =
|
||||||
|
new EncryptionOptions()
|
||||||
|
.withSslContextFactory(new ParameterizedClass(DummySslContextFactoryImpl.class.getName(), parameters2))
|
||||||
|
.withProtocol("TLSv1.1");
|
||||||
|
|
||||||
|
assertNotEquals(encryptionOptions1, encryptionOptions2);
|
||||||
|
assertNotEquals(encryptionOptions1.hashCode(), encryptionOptions2.hashCode());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -20,6 +20,7 @@ package org.apache.cassandra.config;
|
||||||
|
|
||||||
import java.io.File;
|
import java.io.File;
|
||||||
import java.util.Collections;
|
import java.util.Collections;
|
||||||
|
import java.util.HashMap;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
|
|
||||||
import com.google.common.collect.ImmutableMap;
|
import com.google.common.collect.ImmutableMap;
|
||||||
|
|
@ -29,14 +30,15 @@ import org.junit.Test;
|
||||||
import org.apache.cassandra.exceptions.ConfigurationException;
|
import org.apache.cassandra.exceptions.ConfigurationException;
|
||||||
import org.assertj.core.api.Assertions;
|
import org.assertj.core.api.Assertions;
|
||||||
|
|
||||||
import static org.apache.cassandra.config.EncryptionOptions.TlsEncryptionPolicy.UNENCRYPTED;
|
|
||||||
import static org.apache.cassandra.config.EncryptionOptions.TlsEncryptionPolicy.OPTIONAL;
|
|
||||||
import static org.apache.cassandra.config.EncryptionOptions.TlsEncryptionPolicy.ENCRYPTED;
|
|
||||||
import static org.apache.cassandra.config.EncryptionOptions.ServerEncryptionOptions.InternodeEncryption.all;
|
import static org.apache.cassandra.config.EncryptionOptions.ServerEncryptionOptions.InternodeEncryption.all;
|
||||||
import static org.apache.cassandra.config.EncryptionOptions.ServerEncryptionOptions.InternodeEncryption.dc;
|
import static org.apache.cassandra.config.EncryptionOptions.ServerEncryptionOptions.InternodeEncryption.dc;
|
||||||
import static org.apache.cassandra.config.EncryptionOptions.ServerEncryptionOptions.InternodeEncryption.none;
|
import static org.apache.cassandra.config.EncryptionOptions.ServerEncryptionOptions.InternodeEncryption.none;
|
||||||
import static org.apache.cassandra.config.EncryptionOptions.ServerEncryptionOptions.InternodeEncryption.rack;
|
import static org.apache.cassandra.config.EncryptionOptions.ServerEncryptionOptions.InternodeEncryption.rack;
|
||||||
import static org.junit.Assert.*;
|
import static org.apache.cassandra.config.EncryptionOptions.TlsEncryptionPolicy.ENCRYPTED;
|
||||||
|
import static org.apache.cassandra.config.EncryptionOptions.TlsEncryptionPolicy.OPTIONAL;
|
||||||
|
import static org.apache.cassandra.config.EncryptionOptions.TlsEncryptionPolicy.UNENCRYPTED;
|
||||||
|
import static org.junit.Assert.assertFalse;
|
||||||
|
import static org.junit.Assert.assertTrue;
|
||||||
|
|
||||||
public class EncryptionOptionsTest
|
public class EncryptionOptionsTest
|
||||||
{
|
{
|
||||||
|
|
@ -55,7 +57,24 @@ public class EncryptionOptionsTest
|
||||||
|
|
||||||
public static EncryptionOptionsTestCase of(Boolean optional, String keystorePath, Boolean enabled, EncryptionOptions.TlsEncryptionPolicy expected)
|
public static EncryptionOptionsTestCase of(Boolean optional, String keystorePath, Boolean enabled, EncryptionOptions.TlsEncryptionPolicy expected)
|
||||||
{
|
{
|
||||||
return new EncryptionOptionsTestCase(new EncryptionOptions(keystorePath, "dummypass", "dummytruststore", "dummypass",
|
return new EncryptionOptionsTestCase(new EncryptionOptions(new ParameterizedClass("org.apache.cassandra.security.DefaultSslContextFactory",
|
||||||
|
new HashMap<>()),
|
||||||
|
keystorePath, "dummypass",
|
||||||
|
"dummytruststore", "dummypass",
|
||||||
|
Collections.emptyList(), null, null, null, "JKS", false, false, enabled, optional)
|
||||||
|
.applyConfig(),
|
||||||
|
expected,
|
||||||
|
String.format("optional=%s keystore=%s enabled=%s", optional, keystorePath, enabled));
|
||||||
|
}
|
||||||
|
|
||||||
|
public static EncryptionOptionsTestCase of(Boolean optional, String keystorePath, Boolean enabled,
|
||||||
|
Map<String,String> customSslContextFactoryParams,
|
||||||
|
EncryptionOptions.TlsEncryptionPolicy expected)
|
||||||
|
{
|
||||||
|
return new EncryptionOptionsTestCase(new EncryptionOptions(new ParameterizedClass("org.apache.cassandra.security.DefaultSslContextFactory",
|
||||||
|
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(),
|
.applyConfig(),
|
||||||
expected,
|
expected,
|
||||||
|
|
@ -105,7 +124,8 @@ public class EncryptionOptionsTest
|
||||||
EncryptionOptions.ServerEncryptionOptions.InternodeEncryption internodeEncryption,
|
EncryptionOptions.ServerEncryptionOptions.InternodeEncryption internodeEncryption,
|
||||||
EncryptionOptions.TlsEncryptionPolicy expected)
|
EncryptionOptions.TlsEncryptionPolicy expected)
|
||||||
{
|
{
|
||||||
return new ServerEncryptionOptionsTestCase(new EncryptionOptions.ServerEncryptionOptions(keystorePath, "dummypass", "dummytruststore", "dummypass",
|
return new ServerEncryptionOptionsTestCase(new EncryptionOptions.ServerEncryptionOptions(new ParameterizedClass("org.apache.cassandra.security.DefaultSslContextFactory",
|
||||||
|
new HashMap<>()), 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(),
|
.applyConfig(),
|
||||||
expected,
|
expected,
|
||||||
|
|
@ -175,4 +195,17 @@ public class EncryptionOptionsTest
|
||||||
Assert.assertSame(testCase.description, testCase.expected, testCase.encryptionOptions.tlsEncryptionPolicy());
|
Assert.assertSame(testCase.description, testCase.expected, testCase.encryptionOptions.tlsEncryptionPolicy());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test(expected = IllegalArgumentException.class)
|
||||||
|
public void testMisplacedConfigKey()
|
||||||
|
{
|
||||||
|
Map<String, String> customSslContextFactoryParams = new HashMap<>();
|
||||||
|
|
||||||
|
for(EncryptionOptions.ConfigKey configKey: EncryptionOptions.ConfigKey.values())
|
||||||
|
{
|
||||||
|
customSslContextFactoryParams.put(configKey.getKeyName(), "my-custom-value");
|
||||||
|
}
|
||||||
|
|
||||||
|
EncryptionOptionsTestCase.of(null, absentKeystore, true, customSslContextFactoryParams, ENCRYPTED);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,75 @@
|
||||||
|
/*
|
||||||
|
* 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.security;
|
||||||
|
|
||||||
|
import org.junit.AfterClass;
|
||||||
|
import org.junit.Assert;
|
||||||
|
import org.junit.BeforeClass;
|
||||||
|
import org.junit.Test;
|
||||||
|
|
||||||
|
import org.apache.cassandra.config.Config;
|
||||||
|
import org.apache.cassandra.config.DatabaseDescriptor;
|
||||||
|
import org.apache.cassandra.exceptions.ConfigurationException;
|
||||||
|
|
||||||
|
public class CustomSslContextFactoryConfigTest
|
||||||
|
{
|
||||||
|
@BeforeClass
|
||||||
|
public static void setupDatabaseDescriptor()
|
||||||
|
{
|
||||||
|
System.setProperty("cassandra.config", "cassandra-sslcontextfactory.yaml");
|
||||||
|
}
|
||||||
|
|
||||||
|
@AfterClass
|
||||||
|
public static void tearDownDatabaseDescriptor() {
|
||||||
|
System.clearProperty("cassandra.config");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testValidCustomSslContextFactoryConfiguration() {
|
||||||
|
|
||||||
|
Config config = DatabaseDescriptor.loadConfig();
|
||||||
|
config.client_encryption_options.applyConfig();
|
||||||
|
|
||||||
|
Assert.assertEquals("org.apache.cassandra.security.DummySslContextFactoryImpl",
|
||||||
|
config.client_encryption_options.ssl_context_factory.class_name);
|
||||||
|
Assert.assertEquals(config.client_encryption_options.ssl_context_factory.class_name,
|
||||||
|
config.client_encryption_options.sslContextFactoryInstance.getClass().getName());
|
||||||
|
Assert.assertEquals(3, config.client_encryption_options.ssl_context_factory.parameters.size());
|
||||||
|
Assert.assertEquals("value1", config.client_encryption_options.ssl_context_factory.parameters.get("key1"));
|
||||||
|
Assert.assertEquals("value2", config.client_encryption_options.ssl_context_factory.parameters.get("key2"));
|
||||||
|
Assert.assertEquals("value3", config.client_encryption_options.ssl_context_factory.parameters.get("key3"));
|
||||||
|
DummySslContextFactoryImpl dummySslContextFactory =
|
||||||
|
(DummySslContextFactoryImpl)config.client_encryption_options.sslContextFactoryInstance;
|
||||||
|
Assert.assertEquals("dummy-keystore",dummySslContextFactory.getStringValueFor("keystore"));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testInvalidCustomSslContextFactoryConfiguration() {
|
||||||
|
Config config = DatabaseDescriptor.loadConfig();
|
||||||
|
try {
|
||||||
|
config.server_encryption_options.applyConfig();
|
||||||
|
} catch(ConfigurationException ce) {
|
||||||
|
Assert.assertEquals("Unable to create instance of ISslContextFactory for org.apache.cassandra.security" +
|
||||||
|
".MissingSslContextFactoryImpl", ce.getMessage());
|
||||||
|
Assert.assertNotNull("Unable to find root cause of pluggable ISslContextFactory loading failure",
|
||||||
|
ce.getCause());
|
||||||
|
Assert.assertTrue(ce.getCause() instanceof ClassNotFoundException);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,46 @@
|
||||||
|
/*
|
||||||
|
* 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.security;
|
||||||
|
|
||||||
|
import org.junit.AfterClass;
|
||||||
|
import org.junit.BeforeClass;
|
||||||
|
import org.junit.Test;
|
||||||
|
|
||||||
|
import org.apache.cassandra.config.Config;
|
||||||
|
import org.apache.cassandra.config.DatabaseDescriptor;
|
||||||
|
|
||||||
|
public class CustomSslContextFactoryInvalidConfigTest
|
||||||
|
{
|
||||||
|
@BeforeClass
|
||||||
|
public static void setupDatabaseDescriptor()
|
||||||
|
{
|
||||||
|
System.setProperty("cassandra.config", "cassandra-sslcontextfactory-invalidconfiguration.yaml");
|
||||||
|
}
|
||||||
|
|
||||||
|
@AfterClass
|
||||||
|
public static void tearDownDatabaseDescriptor() {
|
||||||
|
System.clearProperty("cassandra.config");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test(expected = IllegalArgumentException.class)
|
||||||
|
public void testValidCustomSslContextFactoryConfiguration() {
|
||||||
|
Config config = DatabaseDescriptor.loadConfig();
|
||||||
|
config.client_encryption_options.applyConfig();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,169 @@
|
||||||
|
/*
|
||||||
|
* 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.security;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.util.Arrays;
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.Map;
|
||||||
|
import javax.net.ssl.TrustManagerFactory;
|
||||||
|
|
||||||
|
import org.junit.Assert;
|
||||||
|
import org.junit.Before;
|
||||||
|
import org.junit.Test;
|
||||||
|
|
||||||
|
import io.netty.handler.ssl.OpenSsl;
|
||||||
|
import io.netty.handler.ssl.OpenSslContext;
|
||||||
|
import io.netty.handler.ssl.SslContext;
|
||||||
|
import io.netty.handler.ssl.SslProvider;
|
||||||
|
import org.apache.cassandra.config.EncryptionOptions;
|
||||||
|
|
||||||
|
public class DefaultSslContextFactoryTest
|
||||||
|
{
|
||||||
|
private Map<String,Object> commonConfig = new HashMap<>();
|
||||||
|
|
||||||
|
@Before
|
||||||
|
public void setup()
|
||||||
|
{
|
||||||
|
commonConfig.put("truststore", "test/conf/cassandra_ssl_test.truststore");
|
||||||
|
commonConfig.put("truststore_password", "cassandra");
|
||||||
|
commonConfig.put("require_client_auth", Boolean.FALSE);
|
||||||
|
commonConfig.put("cipher_suites", Arrays.asList("TLS_RSA_WITH_AES_128_CBC_SHA"));
|
||||||
|
}
|
||||||
|
|
||||||
|
private void addKeystoreOptions(Map<String,Object> config)
|
||||||
|
{
|
||||||
|
config.put("keystore", "test/conf/cassandra_ssl_test.keystore");
|
||||||
|
config.put("keystore_password", "cassandra");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void getSslContextOpenSSL() throws IOException
|
||||||
|
{
|
||||||
|
EncryptionOptions options = new EncryptionOptions().withTrustStore("test/conf/cassandra_ssl_test.truststore")
|
||||||
|
.withTrustStorePassword("cassandra")
|
||||||
|
.withKeyStore("test/conf/cassandra_ssl_test.keystore")
|
||||||
|
.withKeyStorePassword("cassandra")
|
||||||
|
.withRequireClientAuth(false)
|
||||||
|
.withCipherSuites("TLS_RSA_WITH_AES_128_CBC_SHA");
|
||||||
|
SslContext sslContext = SSLFactory.getOrCreateSslContext(options, true, ISslContextFactory.SocketType.CLIENT);
|
||||||
|
Assert.assertNotNull(sslContext);
|
||||||
|
if (OpenSsl.isAvailable())
|
||||||
|
Assert.assertTrue(sslContext instanceof OpenSslContext);
|
||||||
|
else
|
||||||
|
Assert.assertTrue(sslContext instanceof SslContext);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test(expected = IOException.class)
|
||||||
|
public void buildTrustManagerFactoryWithInvalidTruststoreFile() throws IOException
|
||||||
|
{
|
||||||
|
Map<String,Object> config = new HashMap<>();
|
||||||
|
config.putAll(commonConfig);
|
||||||
|
config.put("truststore", "/this/is/probably/not/a/file/on/your/test/machine");
|
||||||
|
|
||||||
|
DefaultSslContextFactory defaultSslContextFactoryImpl = new DefaultSslContextFactory(config);
|
||||||
|
defaultSslContextFactoryImpl.checkedExpiry = false;
|
||||||
|
defaultSslContextFactoryImpl.buildTrustManagerFactory();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test(expected = IOException.class)
|
||||||
|
public void buildTrustManagerFactoryWithBadPassword() throws IOException
|
||||||
|
{
|
||||||
|
Map<String,Object> config = new HashMap<>();
|
||||||
|
config.putAll(commonConfig);
|
||||||
|
config.put("truststore_password", "HomeOfBadPasswords");
|
||||||
|
|
||||||
|
DefaultSslContextFactory defaultSslContextFactoryImpl = new DefaultSslContextFactory(config);
|
||||||
|
defaultSslContextFactoryImpl.checkedExpiry = false;
|
||||||
|
defaultSslContextFactoryImpl.buildTrustManagerFactory();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void buildTrustManagerFactoryHappyPath() throws IOException
|
||||||
|
{
|
||||||
|
Map<String,Object> config = new HashMap<>();
|
||||||
|
config.putAll(commonConfig);
|
||||||
|
|
||||||
|
DefaultSslContextFactory defaultSslContextFactoryImpl = new DefaultSslContextFactory(config);
|
||||||
|
defaultSslContextFactoryImpl.checkedExpiry = false;
|
||||||
|
TrustManagerFactory trustManagerFactory = defaultSslContextFactoryImpl.buildTrustManagerFactory();
|
||||||
|
Assert.assertNotNull(trustManagerFactory);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test(expected = IOException.class)
|
||||||
|
public void buildKeyManagerFactoryWithInvalidKeystoreFile() throws IOException
|
||||||
|
{
|
||||||
|
Map<String,Object> config = new HashMap<>();
|
||||||
|
config.putAll(commonConfig);
|
||||||
|
config.put("keystore", "/this/is/probably/not/a/file/on/your/test/machine");
|
||||||
|
|
||||||
|
DefaultSslContextFactory defaultSslContextFactoryImpl = new DefaultSslContextFactory(config);
|
||||||
|
defaultSslContextFactoryImpl.checkedExpiry = false;
|
||||||
|
defaultSslContextFactoryImpl.buildKeyManagerFactory();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test(expected = IOException.class)
|
||||||
|
public void buildKeyManagerFactoryWithBadPassword() throws IOException
|
||||||
|
{
|
||||||
|
Map<String,Object> config = new HashMap<>();
|
||||||
|
config.putAll(commonConfig);
|
||||||
|
addKeystoreOptions(config);
|
||||||
|
config.put("keystore_password", "HomeOfBadPasswords");
|
||||||
|
|
||||||
|
DefaultSslContextFactory defaultSslContextFactoryImpl = new DefaultSslContextFactory(config);
|
||||||
|
defaultSslContextFactoryImpl.buildKeyManagerFactory();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void buildKeyManagerFactoryHappyPath() throws IOException
|
||||||
|
{
|
||||||
|
Map<String,Object> config = new HashMap<>();
|
||||||
|
config.putAll(commonConfig);
|
||||||
|
|
||||||
|
DefaultSslContextFactory defaultSslContextFactoryImpl = new DefaultSslContextFactory(config);
|
||||||
|
// Make sure the exiry check didn't happen so far for the private key
|
||||||
|
Assert.assertFalse(defaultSslContextFactoryImpl.checkedExpiry);
|
||||||
|
|
||||||
|
addKeystoreOptions(config);
|
||||||
|
DefaultSslContextFactory defaultSslContextFactoryImpl2 = new DefaultSslContextFactory(config);
|
||||||
|
// Trigger the private key loading. That will also check for expired private key
|
||||||
|
defaultSslContextFactoryImpl2.buildKeyManagerFactory();
|
||||||
|
// Now we should have checked the private key's expiry
|
||||||
|
Assert.assertTrue(defaultSslContextFactoryImpl2.checkedExpiry);
|
||||||
|
|
||||||
|
// Make sure that new factory object preforms the fresh private key expiry check
|
||||||
|
DefaultSslContextFactory defaultSslContextFactoryImpl3 = new DefaultSslContextFactory(config);
|
||||||
|
Assert.assertFalse(defaultSslContextFactoryImpl3.checkedExpiry);
|
||||||
|
defaultSslContextFactoryImpl3.buildKeyManagerFactory();
|
||||||
|
Assert.assertTrue(defaultSslContextFactoryImpl3.checkedExpiry);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testDisableOpenSslForInJvmDtests() {
|
||||||
|
// The configuration name below is hard-coded intentionally to make sure we don't break the contract without
|
||||||
|
// changing the documentation appropriately
|
||||||
|
System.setProperty("cassandra.disable_tcactive_openssl","true");
|
||||||
|
Map<String,Object> config = new HashMap<>();
|
||||||
|
config.putAll(commonConfig);
|
||||||
|
|
||||||
|
DefaultSslContextFactory defaultSslContextFactoryImpl = new DefaultSslContextFactory(config);
|
||||||
|
Assert.assertEquals(SslProvider.JDK, defaultSslContextFactoryImpl.getSslProvider());
|
||||||
|
System.clearProperty("cassandra.disable_tcactive_openssl");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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.security;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
import javax.net.ssl.SSLContext;
|
||||||
|
import javax.net.ssl.SSLException;
|
||||||
|
|
||||||
|
import io.netty.handler.ssl.CipherSuiteFilter;
|
||||||
|
import io.netty.handler.ssl.SslContext;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* TEST ONLY Class. DON'T use it for anything else.
|
||||||
|
*/
|
||||||
|
public class DummySslContextFactoryImpl implements ISslContextFactory
|
||||||
|
{
|
||||||
|
private Map<String,Object> parameters;
|
||||||
|
public DummySslContextFactoryImpl(Map<String,Object> parameters) {
|
||||||
|
this.parameters=parameters;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public SSLContext createJSSESslContext(boolean verifyPeerCertificate) throws SSLException
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public SslContext createNettySslContext(boolean verifyPeerCertificate, SocketType socketType,
|
||||||
|
CipherSuiteFilter cipherFilter) throws SSLException
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void initHotReloading() throws SSLException
|
||||||
|
{
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean shouldReload()
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public List<String> getAcceptedProtocols()
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public List<String> getCipherSuites()
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* For testing only
|
||||||
|
*/
|
||||||
|
public String getStringValueFor(String configKey) {
|
||||||
|
return parameters.containsKey(configKey) ? parameters.get(configKey).toString() : null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -21,7 +21,8 @@ package org.apache.cassandra.security;
|
||||||
import java.io.File;
|
import java.io.File;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.security.cert.CertificateException;
|
import java.security.cert.CertificateException;
|
||||||
import javax.net.ssl.TrustManagerFactory;
|
import java.util.HashMap;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
import org.apache.commons.io.FileUtils;
|
import org.apache.commons.io.FileUtils;
|
||||||
import org.junit.Assert;
|
import org.junit.Assert;
|
||||||
|
|
@ -30,16 +31,12 @@ import org.junit.Test;
|
||||||
import org.slf4j.Logger;
|
import org.slf4j.Logger;
|
||||||
import org.slf4j.LoggerFactory;
|
import org.slf4j.LoggerFactory;
|
||||||
|
|
||||||
import io.netty.handler.ssl.JdkSslContext;
|
|
||||||
import io.netty.handler.ssl.OpenSsl;
|
|
||||||
import io.netty.handler.ssl.OpenSslContext;
|
|
||||||
import io.netty.handler.ssl.SslContext;
|
import io.netty.handler.ssl.SslContext;
|
||||||
import io.netty.handler.ssl.util.SelfSignedCertificate;
|
import io.netty.handler.ssl.util.SelfSignedCertificate;
|
||||||
import org.apache.cassandra.config.DatabaseDescriptor;
|
import org.apache.cassandra.config.DatabaseDescriptor;
|
||||||
import org.apache.cassandra.config.EncryptionOptions;
|
import org.apache.cassandra.config.EncryptionOptions;
|
||||||
import org.apache.cassandra.config.EncryptionOptions.ServerEncryptionOptions;
|
import org.apache.cassandra.config.EncryptionOptions.ServerEncryptionOptions;
|
||||||
|
import org.apache.cassandra.config.ParameterizedClass;
|
||||||
import static org.junit.Assert.assertArrayEquals;
|
|
||||||
|
|
||||||
public class SSLFactoryTest
|
public class SSLFactoryTest
|
||||||
{
|
{
|
||||||
|
|
@ -69,34 +66,6 @@ public class SSLFactoryTest
|
||||||
.withTrustStorePassword("cassandra")
|
.withTrustStorePassword("cassandra")
|
||||||
.withRequireClientAuth(false)
|
.withRequireClientAuth(false)
|
||||||
.withCipherSuites("TLS_RSA_WITH_AES_128_CBC_SHA");
|
.withCipherSuites("TLS_RSA_WITH_AES_128_CBC_SHA");
|
||||||
|
|
||||||
SSLFactory.checkedExpiry = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
public void getSslContext_OpenSSL() throws IOException
|
|
||||||
{
|
|
||||||
// only try this test if OpenSsl is available
|
|
||||||
if (!OpenSsl.isAvailable())
|
|
||||||
{
|
|
||||||
logger.warn("OpenSSL not available in this application, so not testing the netty-openssl code paths");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
EncryptionOptions options = addKeystoreOptions(encryptionOptions);
|
|
||||||
SslContext sslContext = SSLFactory.getOrCreateSslContext(options, true, SSLFactory.SocketType.CLIENT, true);
|
|
||||||
Assert.assertNotNull(sslContext);
|
|
||||||
Assert.assertTrue(sslContext instanceof OpenSslContext);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
public void getSslContext_JdkSsl() throws IOException
|
|
||||||
{
|
|
||||||
EncryptionOptions options = addKeystoreOptions(encryptionOptions);
|
|
||||||
SslContext sslContext = SSLFactory.getOrCreateSslContext(options, true, SSLFactory.SocketType.CLIENT, false);
|
|
||||||
Assert.assertNotNull(sslContext);
|
|
||||||
Assert.assertTrue(sslContext instanceof JdkSslContext);
|
|
||||||
Assert.assertEquals(encryptionOptions.cipher_suites, sslContext.cipherSuites());
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private ServerEncryptionOptions addKeystoreOptions(ServerEncryptionOptions options)
|
private ServerEncryptionOptions addKeystoreOptions(ServerEncryptionOptions options)
|
||||||
|
|
@ -105,50 +74,6 @@ public class SSLFactoryTest
|
||||||
.withKeyStorePassword("cassandra");
|
.withKeyStorePassword("cassandra");
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test(expected = IOException.class)
|
|
||||||
public void buildTrustManagerFactory_NoFile() throws IOException
|
|
||||||
{
|
|
||||||
SSLFactory.buildTrustManagerFactory(encryptionOptions.withTrustStore("/this/is/probably/not/a/file/on/your/test/machine"));
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test(expected = IOException.class)
|
|
||||||
public void buildTrustManagerFactory_BadPassword() throws IOException
|
|
||||||
{
|
|
||||||
SSLFactory.buildTrustManagerFactory(encryptionOptions.withTrustStorePassword("HomeOfBadPasswords"));
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
public void buildTrustManagerFactory_HappyPath() throws IOException
|
|
||||||
{
|
|
||||||
TrustManagerFactory trustManagerFactory = SSLFactory.buildTrustManagerFactory(encryptionOptions);
|
|
||||||
Assert.assertNotNull(trustManagerFactory);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test(expected = IOException.class)
|
|
||||||
public void buildKeyManagerFactory_NoFile() throws IOException
|
|
||||||
{
|
|
||||||
EncryptionOptions options = addKeystoreOptions(encryptionOptions)
|
|
||||||
.withKeyStore("/this/is/probably/not/a/file/on/your/test/machine");
|
|
||||||
SSLFactory.buildKeyManagerFactory(options);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test(expected = IOException.class)
|
|
||||||
public void buildKeyManagerFactory_BadPassword() throws IOException
|
|
||||||
{
|
|
||||||
EncryptionOptions options = addKeystoreOptions(encryptionOptions)
|
|
||||||
.withKeyStorePassword("HomeOfBadPasswords");
|
|
||||||
SSLFactory.buildKeyManagerFactory(options);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
public void buildKeyManagerFactory_HappyPath() throws IOException
|
|
||||||
{
|
|
||||||
Assert.assertFalse(SSLFactory.checkedExpiry);
|
|
||||||
EncryptionOptions options = addKeystoreOptions(encryptionOptions);
|
|
||||||
SSLFactory.buildKeyManagerFactory(options);
|
|
||||||
Assert.assertTrue(SSLFactory.checkedExpiry);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void testSslContextReload_HappyPath() throws IOException, InterruptedException
|
public void testSslContextReload_HappyPath() throws IOException, InterruptedException
|
||||||
{
|
{
|
||||||
|
|
@ -159,8 +84,7 @@ public class SSLFactoryTest
|
||||||
|
|
||||||
SSLFactory.initHotReloading(options, options, true);
|
SSLFactory.initHotReloading(options, options, true);
|
||||||
|
|
||||||
SslContext oldCtx = SSLFactory.getOrCreateSslContext(options, true, SSLFactory.SocketType.CLIENT, OpenSsl
|
SslContext oldCtx = SSLFactory.getOrCreateSslContext(options, true, ISslContextFactory.SocketType.CLIENT);
|
||||||
.isAvailable());
|
|
||||||
File keystoreFile = new File(options.keystore);
|
File keystoreFile = new File(options.keystore);
|
||||||
|
|
||||||
SSLFactory.checkCertFilesForHotReloading(options, options);
|
SSLFactory.checkCertFilesForHotReloading(options, options);
|
||||||
|
|
@ -168,8 +92,7 @@ public class SSLFactoryTest
|
||||||
keystoreFile.setLastModified(System.currentTimeMillis() + 15000);
|
keystoreFile.setLastModified(System.currentTimeMillis() + 15000);
|
||||||
|
|
||||||
SSLFactory.checkCertFilesForHotReloading(options, options);
|
SSLFactory.checkCertFilesForHotReloading(options, options);
|
||||||
SslContext newCtx = SSLFactory.getOrCreateSslContext(options, true, SSLFactory.SocketType.CLIENT, OpenSsl
|
SslContext newCtx = SSLFactory.getOrCreateSslContext(options, true, ISslContextFactory.SocketType.CLIENT);
|
||||||
.isAvailable());
|
|
||||||
|
|
||||||
Assert.assertNotSame(oldCtx, newCtx);
|
Assert.assertNotSame(oldCtx, newCtx);
|
||||||
}
|
}
|
||||||
|
|
@ -201,8 +124,7 @@ public class SSLFactoryTest
|
||||||
ServerEncryptionOptions options = addKeystoreOptions(encryptionOptions);
|
ServerEncryptionOptions options = addKeystoreOptions(encryptionOptions);
|
||||||
|
|
||||||
SSLFactory.initHotReloading(options, options, true);
|
SSLFactory.initHotReloading(options, options, true);
|
||||||
SslContext oldCtx = SSLFactory.getOrCreateSslContext(options, true, SSLFactory.SocketType.CLIENT, OpenSsl
|
SslContext oldCtx = SSLFactory.getOrCreateSslContext(options, true, ISslContextFactory.SocketType.CLIENT);
|
||||||
.isAvailable());
|
|
||||||
File keystoreFile = new File(options.keystore);
|
File keystoreFile = new File(options.keystore);
|
||||||
|
|
||||||
SSLFactory.checkCertFilesForHotReloading(options, options);
|
SSLFactory.checkCertFilesForHotReloading(options, options);
|
||||||
|
|
@ -211,8 +133,7 @@ public class SSLFactoryTest
|
||||||
ServerEncryptionOptions modOptions = new ServerEncryptionOptions(options)
|
ServerEncryptionOptions modOptions = new ServerEncryptionOptions(options)
|
||||||
.withKeyStorePassword("bad password");
|
.withKeyStorePassword("bad password");
|
||||||
SSLFactory.checkCertFilesForHotReloading(modOptions, modOptions);
|
SSLFactory.checkCertFilesForHotReloading(modOptions, modOptions);
|
||||||
SslContext newCtx = SSLFactory.getOrCreateSslContext(options, true, SSLFactory.SocketType.CLIENT, OpenSsl
|
SslContext newCtx = SSLFactory.getOrCreateSslContext(options, true, ISslContextFactory.SocketType.CLIENT);
|
||||||
.isAvailable());
|
|
||||||
|
|
||||||
Assert.assertSame(oldCtx, newCtx);
|
Assert.assertSame(oldCtx, newCtx);
|
||||||
}
|
}
|
||||||
|
|
@ -235,16 +156,14 @@ public class SSLFactoryTest
|
||||||
|
|
||||||
|
|
||||||
SSLFactory.initHotReloading(options, options, true);
|
SSLFactory.initHotReloading(options, options, true);
|
||||||
SslContext oldCtx = SSLFactory.getOrCreateSslContext(options, true, SSLFactory.SocketType.CLIENT, OpenSsl
|
SslContext oldCtx = SSLFactory.getOrCreateSslContext(options, true, ISslContextFactory.SocketType.CLIENT);
|
||||||
.isAvailable());
|
|
||||||
SSLFactory.checkCertFilesForHotReloading(options, options);
|
SSLFactory.checkCertFilesForHotReloading(options, options);
|
||||||
|
|
||||||
testKeystoreFile.setLastModified(System.currentTimeMillis() + 15000);
|
testKeystoreFile.setLastModified(System.currentTimeMillis() + 15000);
|
||||||
FileUtils.forceDelete(testKeystoreFile);
|
FileUtils.forceDelete(testKeystoreFile);
|
||||||
|
|
||||||
SSLFactory.checkCertFilesForHotReloading(options, options);
|
SSLFactory.checkCertFilesForHotReloading(options, options);
|
||||||
SslContext newCtx = SSLFactory.getOrCreateSslContext(options, true, SSLFactory.SocketType.CLIENT, OpenSsl
|
SslContext newCtx = SSLFactory.getOrCreateSslContext(options, true, ISslContextFactory.SocketType.CLIENT);
|
||||||
.isAvailable());
|
|
||||||
|
|
||||||
Assert.assertSame(oldCtx, newCtx);
|
Assert.assertSame(oldCtx, newCtx);
|
||||||
}
|
}
|
||||||
|
|
@ -267,7 +186,7 @@ public class SSLFactoryTest
|
||||||
.withCipherSuites("TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256");
|
.withCipherSuites("TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256");
|
||||||
|
|
||||||
SslContext ctx1 = SSLFactory.getOrCreateSslContext(options, true,
|
SslContext ctx1 = SSLFactory.getOrCreateSslContext(options, true,
|
||||||
SSLFactory.SocketType.SERVER, OpenSsl.isAvailable());
|
ISslContextFactory.SocketType.SERVER);
|
||||||
|
|
||||||
Assert.assertTrue(ctx1.isServer());
|
Assert.assertTrue(ctx1.isServer());
|
||||||
Assert.assertEquals(ctx1.cipherSuites(), options.cipher_suites);
|
Assert.assertEquals(ctx1.cipherSuites(), options.cipher_suites);
|
||||||
|
|
@ -275,9 +194,69 @@ public class SSLFactoryTest
|
||||||
options = options.withCipherSuites("TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256");
|
options = options.withCipherSuites("TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256");
|
||||||
|
|
||||||
SslContext ctx2 = SSLFactory.getOrCreateSslContext(options, true,
|
SslContext ctx2 = SSLFactory.getOrCreateSslContext(options, true,
|
||||||
SSLFactory.SocketType.CLIENT, OpenSsl.isAvailable());
|
ISslContextFactory.SocketType.CLIENT);
|
||||||
|
|
||||||
Assert.assertTrue(ctx2.isClient());
|
Assert.assertTrue(ctx2.isClient());
|
||||||
Assert.assertEquals(ctx2.cipherSuites(), options.cipher_suites);
|
Assert.assertEquals(ctx2.cipherSuites(), options.cipher_suites);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testCacheKeyEqualityForCustomSslContextFactory() {
|
||||||
|
|
||||||
|
Map<String,String> parameters1 = new HashMap<>();
|
||||||
|
parameters1.put("key1", "value1");
|
||||||
|
parameters1.put("key2", "value2");
|
||||||
|
EncryptionOptions encryptionOptions1 =
|
||||||
|
new EncryptionOptions()
|
||||||
|
.withSslContextFactory(new ParameterizedClass(DummySslContextFactoryImpl.class.getName(), parameters1))
|
||||||
|
.withProtocol("TLSv1.1")
|
||||||
|
.withRequireClientAuth(true)
|
||||||
|
.withRequireEndpointVerification(false);
|
||||||
|
|
||||||
|
SSLFactory.CacheKey cacheKey1 = new SSLFactory.CacheKey(encryptionOptions1, ISslContextFactory.SocketType.SERVER
|
||||||
|
);
|
||||||
|
|
||||||
|
Map<String,String> parameters2 = new HashMap<>();
|
||||||
|
parameters2.put("key1", "value1");
|
||||||
|
parameters2.put("key2", "value2");
|
||||||
|
EncryptionOptions encryptionOptions2 =
|
||||||
|
new EncryptionOptions()
|
||||||
|
.withSslContextFactory(new ParameterizedClass(DummySslContextFactoryImpl.class.getName(), parameters2))
|
||||||
|
.withProtocol("TLSv1.1")
|
||||||
|
.withRequireClientAuth(true)
|
||||||
|
.withRequireEndpointVerification(false);
|
||||||
|
|
||||||
|
SSLFactory.CacheKey cacheKey2 = new SSLFactory.CacheKey(encryptionOptions2, ISslContextFactory.SocketType.SERVER
|
||||||
|
);
|
||||||
|
|
||||||
|
Assert.assertEquals(cacheKey1, cacheKey2);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testCacheKeyInequalityForCustomSslContextFactory() {
|
||||||
|
|
||||||
|
Map<String,String> parameters1 = new HashMap<>();
|
||||||
|
parameters1.put("key1", "value11");
|
||||||
|
parameters1.put("key2", "value12");
|
||||||
|
EncryptionOptions encryptionOptions1 =
|
||||||
|
new EncryptionOptions()
|
||||||
|
.withSslContextFactory(new ParameterizedClass(DummySslContextFactoryImpl.class.getName(), parameters1))
|
||||||
|
.withProtocol("TLSv1.1");
|
||||||
|
|
||||||
|
SSLFactory.CacheKey cacheKey1 = new SSLFactory.CacheKey(encryptionOptions1, ISslContextFactory.SocketType.SERVER
|
||||||
|
);
|
||||||
|
|
||||||
|
Map<String,String> parameters2 = new HashMap<>();
|
||||||
|
parameters2.put("key1", "value21");
|
||||||
|
parameters2.put("key2", "value22");
|
||||||
|
EncryptionOptions encryptionOptions2 =
|
||||||
|
new EncryptionOptions()
|
||||||
|
.withSslContextFactory(new ParameterizedClass(DummySslContextFactoryImpl.class.getName(), parameters2))
|
||||||
|
.withProtocol("TLSv1.1");
|
||||||
|
|
||||||
|
SSLFactory.CacheKey cacheKey2 = new SSLFactory.CacheKey(encryptionOptions2, ISslContextFactory.SocketType.SERVER
|
||||||
|
);
|
||||||
|
|
||||||
|
Assert.assertNotEquals(cacheKey1, cacheKey2);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue