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
|
||||
* 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)
|
||||
* Avoid token cache invalidation for removing a non-member node (CASSANDRA-15290)
|
||||
* 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``
|
||||
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:
|
||||
|
||||
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;
|
||||
|
||||
import java.io.File;
|
||||
import java.util.Arrays;
|
||||
import java.util.HashMap;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
import java.util.Set;
|
||||
|
||||
import com.google.common.annotations.VisibleForTesting;
|
||||
import com.google.common.collect.ImmutableList;
|
||||
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import org.apache.cassandra.locator.IEndpointSnitch;
|
||||
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
|
||||
{
|
||||
Logger logger = LoggerFactory.getLogger(EncryptionOptions.class);
|
||||
|
||||
public enum TlsEncryptionPolicy
|
||||
{
|
||||
UNENCRYPTED("unencrypted"), OPTIONAL("optionally encrypted"), ENCRYPTED("encrypted");
|
||||
UNENCRYPTED("unencrypted"),
|
||||
OPTIONAL("optionally encrypted"),
|
||||
ENCRYPTED("encrypted");
|
||||
|
||||
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_password;
|
||||
public final String truststore;
|
||||
|
|
@ -73,11 +90,58 @@ public class EncryptionOptions
|
|||
protected Boolean optional;
|
||||
|
||||
// Calculated by calling applyConfig() after populating/parsing
|
||||
protected Boolean isEnabled = null;
|
||||
protected Boolean isOptional = null;
|
||||
protected Boolean isEnabled;
|
||||
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()
|
||||
{
|
||||
ssl_context_factory = new ParameterizedClass("org.apache.cassandra.security.DefaultSslContextFactory",
|
||||
new HashMap<>());
|
||||
keystore = "conf/.keystore";
|
||||
keystore_password = "cassandra";
|
||||
truststore = "conf/.truststore";
|
||||
|
|
@ -93,8 +157,13 @@ public class EncryptionOptions
|
|||
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_password = keystore_password;
|
||||
this.truststore = truststore;
|
||||
|
|
@ -112,6 +181,7 @@ public class EncryptionOptions
|
|||
|
||||
public EncryptionOptions(EncryptionOptions options)
|
||||
{
|
||||
ssl_context_factory = options.ssl_context_factory;
|
||||
keystore = options.keystore;
|
||||
keystore_password = options.keystore_password;
|
||||
truststore = options.truststore;
|
||||
|
|
@ -130,11 +200,15 @@ public class EncryptionOptions
|
|||
/* 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
|
||||
* enabled and optional are set correctly.
|
||||
*
|
||||
* It also initializes the ISslContextFactory's instance
|
||||
*/
|
||||
public EncryptionOptions applyConfig()
|
||||
{
|
||||
ensureConfigNotApplied();
|
||||
|
||||
initializeSslContextFactory();
|
||||
|
||||
isEnabled = this.enabled != null && enabled;
|
||||
|
||||
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
|
||||
// encrypted connections AND they have a keystore file, we assume they would like to be able
|
||||
// to transition to encrypted connections in the future.
|
||||
else if (new File(keystore).exists())
|
||||
else if (sslContextFactoryInstance.hasKeystore())
|
||||
{
|
||||
isOptional = !isEnabled;
|
||||
}
|
||||
|
|
@ -156,6 +230,63 @@ public class EncryptionOptions
|
|||
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()
|
||||
{
|
||||
if (isEnabled == null || isOptional == null)
|
||||
|
|
@ -236,48 +367,9 @@ public class EncryptionOptions
|
|||
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()
|
||||
{
|
||||
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 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;
|
||||
}
|
||||
return sslContextFactoryInstance.getAcceptedProtocols();
|
||||
}
|
||||
|
||||
public String[] acceptedProtocolsArray()
|
||||
|
|
@ -286,11 +378,6 @@ public class EncryptionOptions
|
|||
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()
|
||||
{
|
||||
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)
|
||||
{
|
||||
return new EncryptionOptions(keystore, keystore_password, truststore, truststore_password, cipher_suites,
|
||||
protocol, accepted_protocols, algorithm, store_type, require_client_auth, require_endpoint_verification,
|
||||
enabled, optional).applyConfig();
|
||||
return new EncryptionOptions(ssl_context_factory, keystore, keystore_password, truststore,
|
||||
truststore_password, cipher_suites,protocol, accepted_protocols, algorithm,
|
||||
store_type, require_client_auth, require_endpoint_verification, enabled,
|
||||
optional).applyConfig();
|
||||
}
|
||||
|
||||
public EncryptionOptions withKeyStorePassword(String keystore_password)
|
||||
{
|
||||
return new EncryptionOptions(keystore, keystore_password, truststore, truststore_password, cipher_suites,
|
||||
protocol, accepted_protocols, algorithm, store_type, require_client_auth, require_endpoint_verification,
|
||||
enabled, optional).applyConfig();
|
||||
return new EncryptionOptions(ssl_context_factory, keystore, keystore_password, truststore,
|
||||
truststore_password, cipher_suites,protocol, accepted_protocols, algorithm,
|
||||
store_type, require_client_auth, require_endpoint_verification, enabled,
|
||||
optional).applyConfig();
|
||||
}
|
||||
|
||||
public EncryptionOptions withTrustStore(String truststore)
|
||||
{
|
||||
return new EncryptionOptions(keystore, keystore_password, truststore, truststore_password, cipher_suites,
|
||||
protocol, accepted_protocols, algorithm, store_type, require_client_auth, require_endpoint_verification,
|
||||
enabled, optional).applyConfig();
|
||||
return new EncryptionOptions(ssl_context_factory, keystore, keystore_password, truststore,
|
||||
truststore_password, cipher_suites, protocol, accepted_protocols, algorithm,
|
||||
store_type, require_client_auth, require_endpoint_verification, enabled,
|
||||
optional).applyConfig();
|
||||
}
|
||||
|
||||
public EncryptionOptions withTrustStorePassword(String truststore_password)
|
||||
{
|
||||
return new EncryptionOptions(keystore, keystore_password, truststore, truststore_password, cipher_suites,
|
||||
protocol, accepted_protocols, algorithm, store_type, require_client_auth, require_endpoint_verification,
|
||||
enabled, optional).applyConfig();
|
||||
return new EncryptionOptions(ssl_context_factory, keystore, keystore_password, truststore,
|
||||
truststore_password, cipher_suites, protocol, accepted_protocols, algorithm,
|
||||
store_type, require_client_auth, require_endpoint_verification, enabled,
|
||||
optional).applyConfig();
|
||||
}
|
||||
|
||||
public EncryptionOptions withCipherSuites(List<String> cipher_suites)
|
||||
{
|
||||
return new EncryptionOptions(keystore, keystore_password, truststore, truststore_password, cipher_suites,
|
||||
protocol, accepted_protocols, algorithm, store_type, require_client_auth, require_endpoint_verification,
|
||||
enabled, optional).applyConfig();
|
||||
return new EncryptionOptions(ssl_context_factory, keystore, keystore_password, truststore,
|
||||
truststore_password, cipher_suites, protocol, accepted_protocols, algorithm,
|
||||
store_type, require_client_auth, require_endpoint_verification, enabled,
|
||||
optional).applyConfig();
|
||||
}
|
||||
|
||||
public EncryptionOptions withCipherSuites(String ... cipher_suites)
|
||||
{
|
||||
return new EncryptionOptions(keystore, keystore_password, truststore, truststore_password, ImmutableList.copyOf(cipher_suites),
|
||||
protocol, accepted_protocols, algorithm, store_type, require_client_auth, require_endpoint_verification,
|
||||
enabled, optional).applyConfig();
|
||||
return new EncryptionOptions(ssl_context_factory, keystore, keystore_password, truststore,
|
||||
truststore_password, ImmutableList.copyOf(cipher_suites), protocol,
|
||||
accepted_protocols, algorithm, store_type, require_client_auth,
|
||||
require_endpoint_verification, enabled, optional).applyConfig();
|
||||
}
|
||||
|
||||
public EncryptionOptions withProtocol(String protocol)
|
||||
{
|
||||
return new EncryptionOptions(keystore, keystore_password, truststore, truststore_password, cipher_suites,
|
||||
protocol, accepted_protocols, algorithm, store_type, require_client_auth, require_endpoint_verification,
|
||||
enabled, optional).applyConfig();
|
||||
return new EncryptionOptions(ssl_context_factory, keystore, keystore_password, truststore,
|
||||
truststore_password, cipher_suites, protocol, accepted_protocols, algorithm,
|
||||
store_type, require_client_auth, require_endpoint_verification, enabled,
|
||||
optional).applyConfig();
|
||||
}
|
||||
|
||||
|
||||
public EncryptionOptions withAcceptedProtocols(List<String> accepted_protocols)
|
||||
{
|
||||
return new EncryptionOptions(keystore, keystore_password, truststore, truststore_password, cipher_suites, protocol,
|
||||
accepted_protocols == null ? null : ImmutableList.copyOf(accepted_protocols),
|
||||
algorithm, store_type, require_client_auth, require_endpoint_verification, enabled, optional).applyConfig();
|
||||
return new EncryptionOptions(ssl_context_factory, keystore, keystore_password, truststore,
|
||||
truststore_password, cipher_suites,protocol, accepted_protocols == null ? null :
|
||||
ImmutableList.copyOf(accepted_protocols),
|
||||
algorithm, store_type, require_client_auth, require_endpoint_verification,
|
||||
enabled, optional).applyConfig();
|
||||
}
|
||||
|
||||
|
||||
public EncryptionOptions withAlgorithm(String algorithm)
|
||||
{
|
||||
return new EncryptionOptions(keystore, keystore_password, truststore, truststore_password, cipher_suites,
|
||||
protocol, accepted_protocols, algorithm, store_type, require_client_auth, require_endpoint_verification,
|
||||
enabled, optional).applyConfig();
|
||||
return new EncryptionOptions(ssl_context_factory, keystore, keystore_password, truststore,
|
||||
truststore_password, cipher_suites, protocol, accepted_protocols, algorithm,
|
||||
store_type, require_client_auth, require_endpoint_verification, enabled,
|
||||
optional).applyConfig();
|
||||
}
|
||||
|
||||
public EncryptionOptions withStoreType(String store_type)
|
||||
{
|
||||
return new EncryptionOptions(keystore, keystore_password, truststore, truststore_password, cipher_suites,
|
||||
protocol, accepted_protocols, algorithm, store_type, require_client_auth, require_endpoint_verification,
|
||||
enabled, optional).applyConfig();
|
||||
return new EncryptionOptions(ssl_context_factory, keystore, keystore_password, truststore,
|
||||
truststore_password, cipher_suites, protocol, accepted_protocols, algorithm,
|
||||
store_type, require_client_auth, require_endpoint_verification, enabled,
|
||||
optional).applyConfig();
|
||||
}
|
||||
|
||||
public EncryptionOptions withRequireClientAuth(boolean require_client_auth)
|
||||
{
|
||||
return new EncryptionOptions(keystore, keystore_password, truststore, truststore_password, cipher_suites,
|
||||
protocol, accepted_protocols, algorithm, store_type, require_client_auth, require_endpoint_verification,
|
||||
enabled, optional).applyConfig();
|
||||
return new EncryptionOptions(ssl_context_factory, keystore, keystore_password, truststore,
|
||||
truststore_password, cipher_suites, protocol, accepted_protocols, algorithm,
|
||||
store_type, require_client_auth, require_endpoint_verification, enabled,
|
||||
optional).applyConfig();
|
||||
}
|
||||
|
||||
public EncryptionOptions withRequireEndpointVerification(boolean require_endpoint_verification)
|
||||
{
|
||||
return new EncryptionOptions(keystore, keystore_password, truststore, truststore_password, cipher_suites,
|
||||
protocol, accepted_protocols, algorithm, store_type, require_client_auth, require_endpoint_verification,
|
||||
enabled, optional).applyConfig();
|
||||
return new EncryptionOptions(ssl_context_factory, keystore, keystore_password, truststore,
|
||||
truststore_password, cipher_suites, protocol, accepted_protocols, algorithm,
|
||||
store_type, require_client_auth, require_endpoint_verification, enabled,
|
||||
optional).applyConfig();
|
||||
}
|
||||
|
||||
public EncryptionOptions withEnabled(boolean enabled)
|
||||
{
|
||||
return new EncryptionOptions(keystore, keystore_password, truststore, truststore_password, cipher_suites,
|
||||
protocol, accepted_protocols, algorithm, store_type, require_client_auth, require_endpoint_verification,
|
||||
enabled, optional).applyConfig();
|
||||
return new EncryptionOptions(ssl_context_factory, keystore, keystore_password, truststore,
|
||||
truststore_password, cipher_suites, protocol, accepted_protocols, algorithm,
|
||||
store_type, require_client_auth, require_endpoint_verification, enabled,
|
||||
optional).applyConfig();
|
||||
}
|
||||
|
||||
public EncryptionOptions withOptional(Boolean optional)
|
||||
{
|
||||
return new EncryptionOptions(keystore, keystore_password, truststore, truststore_password, cipher_suites,
|
||||
protocol, accepted_protocols, algorithm, store_type, require_client_auth, require_endpoint_verification,
|
||||
enabled, optional).applyConfig();
|
||||
return new EncryptionOptions(ssl_context_factory, keystore, keystore_password, truststore,
|
||||
truststore_password, cipher_suites, protocol, accepted_protocols, algorithm,
|
||||
store_type, require_client_auth, require_endpoint_verification, enabled,
|
||||
optional).applyConfig();
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -432,7 +541,8 @@ public class EncryptionOptions
|
|||
Objects.equals(accepted_protocols, opt.accepted_protocols) &&
|
||||
Objects.equals(algorithm, opt.algorithm) &&
|
||||
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 * Boolean.hashCode(require_client_auth);
|
||||
result += 31 * Boolean.hashCode(require_endpoint_verification);
|
||||
result += 31 * (ssl_context_factory == null ? 0 : ssl_context_factory.hashCode());
|
||||
return result;
|
||||
}
|
||||
|
||||
|
|
@ -475,16 +586,16 @@ public class EncryptionOptions
|
|||
this.enable_legacy_ssl_storage_port = false;
|
||||
}
|
||||
|
||||
public ServerEncryptionOptions(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 optional, InternodeEncryption internode_encryption,
|
||||
boolean enable_legacy_ssl_storage_port)
|
||||
public ServerEncryptionOptions(ParameterizedClass sslContextFactoryClass, 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 optional,
|
||||
InternodeEncryption internode_encryption, boolean enable_legacy_ssl_storage_port)
|
||||
{
|
||||
super(keystore, keystore_password, truststore, truststore_password, cipher_suites, protocol,
|
||||
accepted_protocols, algorithm, store_type, require_client_auth, require_endpoint_verification,
|
||||
null, optional);
|
||||
super(sslContextFactoryClass, keystore, keystore_password, truststore, truststore_password, cipher_suites,
|
||||
protocol, accepted_protocols, algorithm, store_type, require_client_auth, require_endpoint_verification,
|
||||
null, optional);
|
||||
this.internode_encryption = internode_encryption;
|
||||
this.enable_legacy_ssl_storage_port = enable_legacy_ssl_storage_port;
|
||||
}
|
||||
|
|
@ -562,110 +673,148 @@ public class EncryptionOptions
|
|||
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)
|
||||
{
|
||||
return new ServerEncryptionOptions(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();
|
||||
return new ServerEncryptionOptions(ssl_context_factory, 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 withKeyStorePassword(String keystore_password)
|
||||
{
|
||||
return new ServerEncryptionOptions(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();
|
||||
return new ServerEncryptionOptions(ssl_context_factory, 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 withTrustStore(String truststore)
|
||||
{
|
||||
return new ServerEncryptionOptions(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();
|
||||
return new ServerEncryptionOptions(ssl_context_factory, 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 withTrustStorePassword(String truststore_password)
|
||||
{
|
||||
return new ServerEncryptionOptions(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();
|
||||
return new ServerEncryptionOptions(ssl_context_factory, 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 withCipherSuites(List<String> cipher_suites)
|
||||
{
|
||||
return new ServerEncryptionOptions(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();
|
||||
return new ServerEncryptionOptions(ssl_context_factory, 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 withCipherSuites(String ... cipher_suites)
|
||||
{
|
||||
return new ServerEncryptionOptions(keystore, keystore_password, truststore, truststore_password, ImmutableList.copyOf(cipher_suites),
|
||||
protocol, accepted_protocols, algorithm, store_type, require_client_auth, require_endpoint_verification,
|
||||
optional, internode_encryption, enable_legacy_ssl_storage_port).applyConfigInternal();
|
||||
return new ServerEncryptionOptions(ssl_context_factory, keystore, keystore_password, truststore,
|
||||
truststore_password, Arrays.asList(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 withProtocol(String protocol)
|
||||
{
|
||||
return new ServerEncryptionOptions(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();
|
||||
return new ServerEncryptionOptions(ssl_context_factory, 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 withAcceptedProtocols(List<String> accepted_protocols)
|
||||
{
|
||||
return new ServerEncryptionOptions(keystore, keystore_password, truststore, truststore_password, cipher_suites,
|
||||
protocol, accepted_protocols == null ? null : ImmutableList.copyOf(accepted_protocols),
|
||||
algorithm, store_type, require_client_auth, require_endpoint_verification,
|
||||
optional, internode_encryption, enable_legacy_ssl_storage_port).applyConfigInternal();
|
||||
return new ServerEncryptionOptions(ssl_context_factory, 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 withAlgorithm(String algorithm)
|
||||
{
|
||||
return new ServerEncryptionOptions(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();
|
||||
return new ServerEncryptionOptions(ssl_context_factory, 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 withStoreType(String store_type)
|
||||
{
|
||||
return new ServerEncryptionOptions(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();
|
||||
return new ServerEncryptionOptions(ssl_context_factory, 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 withRequireClientAuth(boolean require_client_auth)
|
||||
{
|
||||
return new ServerEncryptionOptions(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();
|
||||
return new ServerEncryptionOptions(ssl_context_factory, 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 withRequireEndpointVerification(boolean require_endpoint_verification)
|
||||
{
|
||||
return new ServerEncryptionOptions(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();
|
||||
return new ServerEncryptionOptions(ssl_context_factory, 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 withOptional(boolean optional)
|
||||
{
|
||||
return new ServerEncryptionOptions(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();
|
||||
return new ServerEncryptionOptions(ssl_context_factory, 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 withInternodeEncryption(InternodeEncryption internode_encryption)
|
||||
{
|
||||
return new ServerEncryptionOptions(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();
|
||||
return new ServerEncryptionOptions(ssl_context_factory, 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 withLegacySslStoragePort(boolean enable_legacy_ssl_storage_port)
|
||||
{
|
||||
return new ServerEncryptionOptions(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();
|
||||
return new ServerEncryptionOptions(ssl_context_factory, 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();
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -59,6 +59,12 @@ public class ParameterizedClass
|
|||
return Objects.equal(class_name, that.class_name) && Objects.equal(parameters, that.parameters);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode()
|
||||
{
|
||||
return Objects.hashCode(class_name, parameters);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString()
|
||||
{
|
||||
|
|
|
|||
|
|
@ -50,6 +50,7 @@ import org.apache.cassandra.config.EncryptionOptions;
|
|||
import org.apache.cassandra.exceptions.ConfigurationException;
|
||||
import org.apache.cassandra.locator.InetAddressAndPort;
|
||||
import org.apache.cassandra.net.OutboundConnectionSettings.Framing;
|
||||
import org.apache.cassandra.security.ISslContextFactory;
|
||||
import org.apache.cassandra.security.SSLFactory;
|
||||
import org.apache.cassandra.streaming.async.StreamingInboundHandler;
|
||||
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
|
||||
{
|
||||
final boolean buildTrustStore = true;
|
||||
SslContext sslContext = SSLFactory.getOrCreateSslContext(encryptionOptions, buildTrustStore, SSLFactory.SocketType.SERVER);
|
||||
final boolean verifyPeerCertificate = true;
|
||||
SslContext sslContext = SSLFactory.getOrCreateSslContext(encryptionOptions, verifyPeerCertificate,
|
||||
ISslContextFactory.SocketType.SERVER);
|
||||
InetSocketAddress peer = encryptionOptions.require_endpoint_verification ? (InetSocketAddress) channel.remoteAddress() : null;
|
||||
SslHandler sslHandler = newSslHandler(channel, sslContext, peer);
|
||||
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.OutboundConnectionInitiator.Result.MessagingSuccess;
|
||||
import org.apache.cassandra.net.OutboundConnectionInitiator.Result.StreamingSuccess;
|
||||
import org.apache.cassandra.security.ISslContextFactory;
|
||||
import org.apache.cassandra.security.SSLFactory;
|
||||
import org.apache.cassandra.utils.JVMStabilityInspector;
|
||||
import org.apache.cassandra.utils.memory.BufferPools;
|
||||
|
|
@ -201,7 +202,8 @@ public class OutboundConnectionInitiator<SuccessType extends OutboundConnectionI
|
|||
if (settings.withEncryption())
|
||||
{
|
||||
// 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
|
||||
InetAddressAndPort address = settings.to;
|
||||
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;
|
||||
|
||||
|
||||
import java.io.File;
|
||||
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.Arrays;
|
||||
import java.util.Date;
|
||||
import java.util.Enumeration;
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
import java.util.Set;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.stream.Collectors;
|
||||
import javax.net.ssl.KeyManagerFactory;
|
||||
import javax.net.ssl.SSLContext;
|
||||
import javax.net.ssl.SSLEngine;
|
||||
import javax.net.ssl.SSLParameters;
|
||||
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.LoggerFactory;
|
||||
|
||||
import io.netty.buffer.ByteBufAllocator;
|
||||
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 io.netty.util.ReferenceCountUtil;
|
||||
import org.apache.cassandra.concurrent.ScheduledExecutors;
|
||||
import org.apache.cassandra.config.Config;
|
||||
import org.apache.cassandra.config.DatabaseDescriptor;
|
||||
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
|
||||
|
|
@ -73,18 +58,6 @@ public final class SSLFactory
|
|||
{
|
||||
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
|
||||
// support. It creates a circular reference that prevents the instance class loader from being
|
||||
// garbage collected.
|
||||
|
|
@ -110,11 +83,6 @@ public final class SSLFactory
|
|||
*/
|
||||
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
|
||||
*/
|
||||
|
|
@ -130,38 +98,6 @@ public final class SSLFactory
|
|||
*/
|
||||
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
|
||||
* protocol before the change for CASSANDRA-13325 that expects explicit protocol versions.
|
||||
* @return list of enabled protocol names
|
||||
|
|
@ -185,100 +121,25 @@ public final class SSLFactory
|
|||
/**
|
||||
* 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;
|
||||
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);
|
||||
}
|
||||
return options.sslContextFactoryInstance.createJSSESslContext(verifyPeerCertificate);
|
||||
}
|
||||
|
||||
/**
|
||||
* 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
|
||||
{
|
||||
return getOrCreateSslContext(options, buildTruststore, socketType, openSslIsAvailable());
|
||||
}
|
||||
|
||||
/**
|
||||
* 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);
|
||||
CacheKey key = new CacheKey(options, socketType);
|
||||
SslContext sslContext;
|
||||
|
||||
sslContext = cachedSslContexts.get(key);
|
||||
if (sslContext != null)
|
||||
return sslContext;
|
||||
|
||||
sslContext = createNettySslContext(options, buildTruststore, socketType, useOpenSsl);
|
||||
sslContext = createNettySslContext(options, verifyPeerCertificate, socketType);
|
||||
|
||||
SslContext previous = cachedSslContexts.putIfAbsent(key, sslContext);
|
||||
if (previous == null)
|
||||
|
|
@ -291,52 +152,21 @@ public final class SSLFactory
|
|||
/**
|
||||
* Create a Netty {@link SslContext}
|
||||
*/
|
||||
static SslContext createNettySslContext(EncryptionOptions options, boolean buildTruststore,
|
||||
SocketType socketType, boolean useOpenSsl) throws IOException
|
||||
static SslContext createNettySslContext(EncryptionOptions options, boolean verifyPeerCertificate,
|
||||
SocketType socketType) throws IOException
|
||||
{
|
||||
return createNettySslContext(options, buildTruststore, socketType, useOpenSsl,
|
||||
return createNettySslContext(options, verifyPeerCertificate, socketType,
|
||||
LoggingCipherSuiteFilter.QUIET_FILTER);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a Netty {@link SslContext} with a supplied cipherFilter
|
||||
*/
|
||||
static SslContext createNettySslContext(EncryptionOptions options, boolean buildTruststore,
|
||||
SocketType socketType, boolean useOpenSsl, CipherSuiteFilter cipherFilter) throws IOException
|
||||
static SslContext createNettySslContext(EncryptionOptions options, boolean verifyPeerCertificate,
|
||||
SocketType socketType, CipherSuiteFilter cipherFilter) throws IOException
|
||||
{
|
||||
/*
|
||||
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(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();
|
||||
return options.sslContextFactoryInstance.createNettySslContext(verifyPeerCertificate, socketType,
|
||||
cipherFilter);
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -351,21 +181,58 @@ public final class SSLFactory
|
|||
if (!isHotReloadingInitialized)
|
||||
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.");
|
||||
try
|
||||
checkCertFilesForHotReloading(serverOpts, "server_encryption_options", true);
|
||||
}
|
||||
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);
|
||||
cachedSslContexts.clear();
|
||||
}
|
||||
catch(Exception e)
|
||||
{
|
||||
logger.error("Failed to hot reload the SSL Certificates! Please check the certificate files.", e);
|
||||
logger.info("SSL certificates have been updated for {}. Resetting the ssl contexts for new " +
|
||||
"connections.", options.getClass().getName());
|
||||
validateSslContext(contextDescription, options, verifyPeerCertificate, false);
|
||||
clearSslContextCache(options);
|
||||
}
|
||||
}
|
||||
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");
|
||||
|
||||
List<HotReloadableFile> fileList = new ArrayList<>();
|
||||
|
||||
if (serverOpts != null && serverOpts.tlsEncryptionPolicy() != EncryptionOptions.TlsEncryptionPolicy.UNENCRYPTED)
|
||||
{
|
||||
fileList.add(new HotReloadableFile(serverOpts.keystore));
|
||||
fileList.add(new HotReloadableFile(serverOpts.truststore));
|
||||
if ( serverOpts != null && serverOpts.tlsEncryptionPolicy() != EncryptionOptions.TlsEncryptionPolicy.UNENCRYPTED) {
|
||||
serverOpts.sslContextFactoryInstance.initHotReloading();
|
||||
}
|
||||
|
||||
if (clientOpts != null && clientOpts.tlsEncryptionPolicy() != EncryptionOptions.TlsEncryptionPolicy.UNENCRYPTED)
|
||||
{
|
||||
fileList.add(new HotReloadableFile(clientOpts.keystore));
|
||||
fileList.add(new HotReloadableFile(clientOpts.truststore));
|
||||
if ( clientOpts != null && clientOpts.tlsEncryptionPolicy() != EncryptionOptions.TlsEncryptionPolicy.UNENCRYPTED) {
|
||||
clientOpts.sslContextFactoryInstance.initHotReloading();
|
||||
}
|
||||
|
||||
hotReloadableFiles = ImmutableList.copyOf(fileList);
|
||||
|
||||
if (!isHotReloadingInitialized)
|
||||
{
|
||||
ScheduledExecutors.scheduledTasks
|
||||
|
|
@ -485,7 +344,7 @@ public final class SSLFactory
|
|||
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)
|
||||
{
|
||||
|
|
@ -493,7 +352,7 @@ public final class SSLFactory
|
|||
{
|
||||
CipherSuiteFilter loggingCipherSuiteFilter = logProtocolAndCiphers ? new LoggingCipherSuiteFilter(contextDescription)
|
||||
: LoggingCipherSuiteFilter.QUIET_FILTER;
|
||||
SslContext serverSslContext = createNettySslContext(options, buildTrustStore, SocketType.SERVER, openSslIsAvailable(), loggingCipherSuiteFilter);
|
||||
SslContext serverSslContext = createNettySslContext(options, verifyPeerCertificate, SocketType.SERVER, loggingCipherSuiteFilter);
|
||||
try
|
||||
{
|
||||
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
|
||||
SslContext clientSslContext = createNettySslContext(options, buildTrustStore, SocketType.CLIENT, openSslIsAvailable());
|
||||
SslContext clientSslContext = createNettySslContext(options, verifyPeerCertificate, SocketType.CLIENT);
|
||||
ReferenceCountUtil.release(clientSslContext);
|
||||
}
|
||||
catch (Exception e)
|
||||
|
|
@ -561,13 +420,11 @@ public final class SSLFactory
|
|||
{
|
||||
private final EncryptionOptions encryptionOptions;
|
||||
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.socketType = socketType;
|
||||
this.useOpenSSL = useOpenSSL;
|
||||
}
|
||||
|
||||
public boolean equals(Object o)
|
||||
|
|
@ -576,7 +433,6 @@ public final class SSLFactory
|
|||
if (o == null || getClass() != o.getClass()) return false;
|
||||
CacheKey cacheKey = (CacheKey) o;
|
||||
return (socketType == cacheKey.socketType &&
|
||||
useOpenSSL == cacheKey.useOpenSSL &&
|
||||
Objects.equals(encryptionOptions, cacheKey.encryptionOptions));
|
||||
}
|
||||
|
||||
|
|
@ -585,7 +441,6 @@ public final class SSLFactory
|
|||
int result = 0;
|
||||
result += 31 * socketType.hashCode();
|
||||
result += 31 * encryptionOptions.hashCode();
|
||||
result += 31 * Boolean.hashCode(useOpenSSL);
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -44,6 +44,7 @@ import io.netty.util.Version;
|
|||
import org.apache.cassandra.config.DatabaseDescriptor;
|
||||
import org.apache.cassandra.config.EncryptionOptions;
|
||||
import org.apache.cassandra.net.*;
|
||||
import org.apache.cassandra.security.ISslContextFactory;
|
||||
import org.apache.cassandra.security.SSLFactory;
|
||||
import org.apache.cassandra.transport.messages.StartupMessage;
|
||||
|
||||
|
|
@ -163,7 +164,7 @@ public class PipelineConfigurator
|
|||
return channel -> {
|
||||
SslContext sslContext = SSLFactory.getOrCreateSslContext(encryptionOptions,
|
||||
encryptionOptions.require_client_auth,
|
||||
SSLFactory.SocketType.SERVER);
|
||||
ISslContextFactory.SocketType.SERVER);
|
||||
|
||||
channel.pipeline().addFirst(SSL_HANDLER, new ByteToMessageDecoder()
|
||||
{
|
||||
|
|
@ -197,7 +198,7 @@ public class PipelineConfigurator
|
|||
return channel -> {
|
||||
SslContext sslContext = SSLFactory.getOrCreateSslContext(encryptionOptions,
|
||||
encryptionOptions.require_client_auth,
|
||||
SSLFactory.SocketType.SERVER);
|
||||
ISslContextFactory.SocketType.SERVER);
|
||||
channel.pipeline().addFirst(SSL_HANDLER, sslContext.newHandler(channel.alloc()));
|
||||
};
|
||||
default:
|
||||
|
|
|
|||
|
|
@ -47,6 +47,7 @@ import org.apache.cassandra.config.EncryptionOptions;
|
|||
import org.apache.cassandra.cql3.QueryOptions;
|
||||
import org.apache.cassandra.db.ConsistencyLevel;
|
||||
import org.apache.cassandra.net.*;
|
||||
import org.apache.cassandra.security.ISslContextFactory;
|
||||
import org.apache.cassandra.security.SSLFactory;
|
||||
import org.apache.cassandra.transport.messages.*;
|
||||
|
||||
|
|
@ -620,7 +621,7 @@ public class SimpleClient implements Closeable
|
|||
{
|
||||
super.initChannel(channel);
|
||||
SslContext sslContext = SSLFactory.getOrCreateSslContext(encryptionOptions, encryptionOptions.require_client_auth,
|
||||
SSLFactory.SocketType.CLIENT);
|
||||
ISslContextFactory.SocketType.CLIENT);
|
||||
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.databind.ObjectMapper;
|
||||
import org.apache.cassandra.auth.AllowAllNetworkAuthorizer;
|
||||
import org.apache.cassandra.audit.IAuditLogger;
|
||||
import org.apache.cassandra.auth.AllowAllNetworkAuthorizer;
|
||||
import org.apache.cassandra.auth.IAuthenticator;
|
||||
import org.apache.cassandra.auth.IAuthorizer;
|
||||
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.FileUtils;
|
||||
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.USER_HOME;
|
||||
|
|
@ -672,7 +673,7 @@ public class FBUtilities
|
|||
}
|
||||
return FBUtilities.construct(className, "network authorizer");
|
||||
}
|
||||
|
||||
|
||||
public static IAuditLogger newAuditLogger(String className, Map<String, String> parameters) throws ConfigurationException
|
||||
{
|
||||
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.
|
||||
* @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.distributed.Cluster;
|
||||
import org.apache.cassandra.exceptions.ConfigurationException;
|
||||
import org.apache.cassandra.security.ISslContextFactory;
|
||||
import org.apache.cassandra.security.SSLFactory;
|
||||
import org.apache.cassandra.utils.concurrent.SimpleCondition;
|
||||
|
||||
|
|
@ -194,8 +195,8 @@ public class AbstractEncryptionOptionsImpl extends TestBaseImpl
|
|||
setProtocolAndCipher(null, null);
|
||||
|
||||
SslContext sslContext = SSLFactory.getOrCreateSslContext(
|
||||
encryptionOptions.withAcceptedProtocols(acceptedProtocols).withCipherSuites(cipherSuites),
|
||||
true, SSLFactory.SocketType.CLIENT);
|
||||
encryptionOptions.withAcceptedProtocols(acceptedProtocols).withCipherSuites(cipherSuites),
|
||||
true, ISslContextFactory.SocketType.CLIENT);
|
||||
|
||||
EventLoopGroup workerGroup = new NioEventLoopGroup();
|
||||
Bootstrap b = new Bootstrap();
|
||||
|
|
|
|||
|
|
@ -149,6 +149,7 @@ public class DatabaseDescriptorRefTest
|
|||
"org.apache.cassandra.locator.Replica",
|
||||
"org.apache.cassandra.locator.SimpleSeedProvider",
|
||||
"org.apache.cassandra.locator.SeedProvider",
|
||||
"org.apache.cassandra.security.ISslContextFactory",
|
||||
"org.apache.cassandra.security.SSLFactory",
|
||||
"org.apache.cassandra.security.EncryptionContext",
|
||||
"org.apache.cassandra.service.CacheService$CacheType",
|
||||
|
|
|
|||
|
|
@ -265,7 +265,7 @@ public class DatabaseDescriptorTest
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@Test
|
||||
public void testTokensFromString()
|
||||
{
|
||||
|
|
@ -337,7 +337,7 @@ public class DatabaseDescriptorTest
|
|||
testConfig.cas_contention_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;
|
||||
|
||||
|
||||
assertTrue(testConfig.read_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);
|
||||
|
|
@ -741,4 +741,15 @@ public class DatabaseDescriptorTest
|
|||
conf.track_warnings.row_index_size.abort_threshold_kb = 2;
|
||||
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.util.Collections;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
import com.google.common.collect.ImmutableMap;
|
||||
|
|
@ -29,14 +30,15 @@ import org.junit.Test;
|
|||
import org.apache.cassandra.exceptions.ConfigurationException;
|
||||
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.dc;
|
||||
import static org.apache.cassandra.config.EncryptionOptions.ServerEncryptionOptions.InternodeEncryption.none;
|
||||
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
|
||||
{
|
||||
|
|
@ -55,7 +57,24 @@ public class EncryptionOptionsTest
|
|||
|
||||
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)
|
||||
.applyConfig(),
|
||||
expected,
|
||||
|
|
@ -105,7 +124,8 @@ public class EncryptionOptionsTest
|
|||
EncryptionOptions.ServerEncryptionOptions.InternodeEncryption internodeEncryption,
|
||||
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)
|
||||
.applyConfig(),
|
||||
expected,
|
||||
|
|
@ -175,4 +195,17 @@ public class EncryptionOptionsTest
|
|||
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.IOException;
|
||||
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.junit.Assert;
|
||||
|
|
@ -30,16 +31,12 @@ import org.junit.Test;
|
|||
import org.slf4j.Logger;
|
||||
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.util.SelfSignedCertificate;
|
||||
import org.apache.cassandra.config.DatabaseDescriptor;
|
||||
import org.apache.cassandra.config.EncryptionOptions;
|
||||
import org.apache.cassandra.config.EncryptionOptions.ServerEncryptionOptions;
|
||||
|
||||
import static org.junit.Assert.assertArrayEquals;
|
||||
import org.apache.cassandra.config.ParameterizedClass;
|
||||
|
||||
public class SSLFactoryTest
|
||||
{
|
||||
|
|
@ -69,34 +66,6 @@ public class SSLFactoryTest
|
|||
.withTrustStorePassword("cassandra")
|
||||
.withRequireClientAuth(false)
|
||||
.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)
|
||||
|
|
@ -105,50 +74,6 @@ public class SSLFactoryTest
|
|||
.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
|
||||
public void testSslContextReload_HappyPath() throws IOException, InterruptedException
|
||||
{
|
||||
|
|
@ -159,8 +84,7 @@ public class SSLFactoryTest
|
|||
|
||||
SSLFactory.initHotReloading(options, options, true);
|
||||
|
||||
SslContext oldCtx = SSLFactory.getOrCreateSslContext(options, true, SSLFactory.SocketType.CLIENT, OpenSsl
|
||||
.isAvailable());
|
||||
SslContext oldCtx = SSLFactory.getOrCreateSslContext(options, true, ISslContextFactory.SocketType.CLIENT);
|
||||
File keystoreFile = new File(options.keystore);
|
||||
|
||||
SSLFactory.checkCertFilesForHotReloading(options, options);
|
||||
|
|
@ -168,8 +92,7 @@ public class SSLFactoryTest
|
|||
keystoreFile.setLastModified(System.currentTimeMillis() + 15000);
|
||||
|
||||
SSLFactory.checkCertFilesForHotReloading(options, options);
|
||||
SslContext newCtx = SSLFactory.getOrCreateSslContext(options, true, SSLFactory.SocketType.CLIENT, OpenSsl
|
||||
.isAvailable());
|
||||
SslContext newCtx = SSLFactory.getOrCreateSslContext(options, true, ISslContextFactory.SocketType.CLIENT);
|
||||
|
||||
Assert.assertNotSame(oldCtx, newCtx);
|
||||
}
|
||||
|
|
@ -201,8 +124,7 @@ public class SSLFactoryTest
|
|||
ServerEncryptionOptions options = addKeystoreOptions(encryptionOptions);
|
||||
|
||||
SSLFactory.initHotReloading(options, options, true);
|
||||
SslContext oldCtx = SSLFactory.getOrCreateSslContext(options, true, SSLFactory.SocketType.CLIENT, OpenSsl
|
||||
.isAvailable());
|
||||
SslContext oldCtx = SSLFactory.getOrCreateSslContext(options, true, ISslContextFactory.SocketType.CLIENT);
|
||||
File keystoreFile = new File(options.keystore);
|
||||
|
||||
SSLFactory.checkCertFilesForHotReloading(options, options);
|
||||
|
|
@ -211,8 +133,7 @@ public class SSLFactoryTest
|
|||
ServerEncryptionOptions modOptions = new ServerEncryptionOptions(options)
|
||||
.withKeyStorePassword("bad password");
|
||||
SSLFactory.checkCertFilesForHotReloading(modOptions, modOptions);
|
||||
SslContext newCtx = SSLFactory.getOrCreateSslContext(options, true, SSLFactory.SocketType.CLIENT, OpenSsl
|
||||
.isAvailable());
|
||||
SslContext newCtx = SSLFactory.getOrCreateSslContext(options, true, ISslContextFactory.SocketType.CLIENT);
|
||||
|
||||
Assert.assertSame(oldCtx, newCtx);
|
||||
}
|
||||
|
|
@ -235,16 +156,14 @@ public class SSLFactoryTest
|
|||
|
||||
|
||||
SSLFactory.initHotReloading(options, options, true);
|
||||
SslContext oldCtx = SSLFactory.getOrCreateSslContext(options, true, SSLFactory.SocketType.CLIENT, OpenSsl
|
||||
.isAvailable());
|
||||
SslContext oldCtx = SSLFactory.getOrCreateSslContext(options, true, ISslContextFactory.SocketType.CLIENT);
|
||||
SSLFactory.checkCertFilesForHotReloading(options, options);
|
||||
|
||||
testKeystoreFile.setLastModified(System.currentTimeMillis() + 15000);
|
||||
FileUtils.forceDelete(testKeystoreFile);
|
||||
|
||||
SSLFactory.checkCertFilesForHotReloading(options, options);
|
||||
SslContext newCtx = SSLFactory.getOrCreateSslContext(options, true, SSLFactory.SocketType.CLIENT, OpenSsl
|
||||
.isAvailable());
|
||||
SslContext newCtx = SSLFactory.getOrCreateSslContext(options, true, ISslContextFactory.SocketType.CLIENT);
|
||||
|
||||
Assert.assertSame(oldCtx, newCtx);
|
||||
}
|
||||
|
|
@ -267,7 +186,7 @@ public class SSLFactoryTest
|
|||
.withCipherSuites("TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256");
|
||||
|
||||
SslContext ctx1 = SSLFactory.getOrCreateSslContext(options, true,
|
||||
SSLFactory.SocketType.SERVER, OpenSsl.isAvailable());
|
||||
ISslContextFactory.SocketType.SERVER);
|
||||
|
||||
Assert.assertTrue(ctx1.isServer());
|
||||
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");
|
||||
|
||||
SslContext ctx2 = SSLFactory.getOrCreateSslContext(options, true,
|
||||
SSLFactory.SocketType.CLIENT, OpenSsl.isAvailable());
|
||||
ISslContextFactory.SocketType.CLIENT);
|
||||
|
||||
Assert.assertTrue(ctx2.isClient());
|
||||
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