mirror of https://github.com/apache/cassandra
Adding endpoint verification option to client_encryption_options
patch by Jyothsna Konisa; reviewed by Jon Meredith, Yifan Cai for CASSANDRA-18034
This commit is contained in:
parent
d5fd0dceee
commit
36e16ee3c9
|
|
@ -1,4 +1,5 @@
|
|||
4.2
|
||||
* Adding endpoint verification option to client_encryption_options (CASSANDRA-18034)
|
||||
* Replace 'wcwidth.py' with pypi module (CASSANDRA-17287)
|
||||
* Add nodetool forcecompact to remove tombstoned or ttl'd data ignoring GC grace for given table and partition keys (CASSANDRA-17711)
|
||||
* Offer IF (NOT) EXISTS in cqlsh completion for CREATE TYPE, DROP TYPE, CREATE ROLE and DROP ROLE (CASSANDRA-16640)
|
||||
|
|
|
|||
|
|
@ -1394,6 +1394,7 @@ client_encryption_options:
|
|||
keystore_password: cassandra
|
||||
# Verify client certificates
|
||||
require_client_auth: false
|
||||
# require_endpoint_verification: false
|
||||
# Set trustore and truststore_password if require_client_auth is true
|
||||
# truststore: conf/.truststore
|
||||
# truststore_password: cassandra
|
||||
|
|
|
|||
|
|
@ -215,7 +215,7 @@ public final class SocketFactory
|
|||
* Creates a new {@link SslHandler} from provided SslContext.
|
||||
* @param peer enables endpoint verification for remote address when not null
|
||||
*/
|
||||
static SslHandler newSslHandler(Channel channel, SslContext sslContext, @Nullable InetSocketAddress peer)
|
||||
public static SslHandler newSslHandler(Channel channel, SslContext sslContext, @Nullable InetSocketAddress peer)
|
||||
{
|
||||
if (peer == null)
|
||||
return sslContext.newHandler(channel.alloc());
|
||||
|
|
|
|||
|
|
@ -48,6 +48,8 @@ import org.apache.cassandra.security.ISslContextFactory;
|
|||
import org.apache.cassandra.security.SSLFactory;
|
||||
import org.apache.cassandra.transport.messages.StartupMessage;
|
||||
|
||||
import static org.apache.cassandra.net.SocketFactory.newSslHandler;
|
||||
|
||||
/**
|
||||
* Takes care of intializing a Netty Channel and Pipeline for client protocol connections.
|
||||
* The pipeline is first set up with some common handlers for connection limiting, dropping
|
||||
|
|
@ -181,7 +183,8 @@ public class PipelineConfigurator
|
|||
{
|
||||
// Connection uses SSL/TLS, replace the detection handler with a SslHandler and so use
|
||||
// encryption.
|
||||
SslHandler sslHandler = sslContext.newHandler(channel.alloc());
|
||||
InetSocketAddress peer = encryptionOptions.require_endpoint_verification ? (InetSocketAddress) channel.remoteAddress() : null;
|
||||
SslHandler sslHandler = newSslHandler(channel, sslContext, peer);
|
||||
channelHandlerContext.pipeline().replace(SSL_HANDLER, SSL_HANDLER, sslHandler);
|
||||
}
|
||||
else
|
||||
|
|
@ -199,7 +202,8 @@ public class PipelineConfigurator
|
|||
SslContext sslContext = SSLFactory.getOrCreateSslContext(encryptionOptions,
|
||||
encryptionOptions.require_client_auth,
|
||||
ISslContextFactory.SocketType.SERVER);
|
||||
channel.pipeline().addFirst(SSL_HANDLER, sslContext.newHandler(channel.alloc()));
|
||||
InetSocketAddress peer = encryptionOptions.require_endpoint_verification ? (InetSocketAddress) channel.remoteAddress() : null;
|
||||
channel.pipeline().addFirst(SSL_HANDLER, newSslHandler(channel, sslContext, peer));
|
||||
};
|
||||
default:
|
||||
throw new IllegalStateException("Unrecognized TLS encryption policy: " + this.tlsEncryptionPolicy);
|
||||
|
|
|
|||
|
|
@ -52,6 +52,7 @@ import org.apache.cassandra.security.SSLFactory;
|
|||
import org.apache.cassandra.transport.messages.*;
|
||||
import org.apache.cassandra.utils.concurrent.UncheckedInterruptedException;
|
||||
|
||||
import static org.apache.cassandra.net.SocketFactory.newSslHandler;
|
||||
import static org.apache.cassandra.transport.CQLMessageHandler.envelopeSize;
|
||||
import static org.apache.cassandra.transport.Flusher.MAX_FRAMED_PAYLOAD_SIZE;
|
||||
import static org.apache.cassandra.utils.concurrent.NonBlockingRateLimiter.NO_OP_LIMITER;
|
||||
|
|
@ -624,7 +625,8 @@ public class SimpleClient implements Closeable
|
|||
super.initChannel(channel);
|
||||
SslContext sslContext = SSLFactory.getOrCreateSslContext(encryptionOptions, encryptionOptions.require_client_auth,
|
||||
ISslContextFactory.SocketType.CLIENT);
|
||||
channel.pipeline().addFirst("ssl", sslContext.newHandler(channel.alloc()));
|
||||
InetSocketAddress peer = encryptionOptions.require_endpoint_verification ? new InetSocketAddress(host, port) : null;
|
||||
channel.pipeline().addFirst("ssl", newSslHandler(channel, sslContext, peer));
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
Binary file not shown.
Binary file not shown.
|
|
@ -18,18 +18,33 @@
|
|||
|
||||
package org.apache.cassandra.distributed.test;
|
||||
|
||||
import java.io.FileInputStream;
|
||||
import java.io.InputStream;
|
||||
import java.net.InetAddress;
|
||||
import java.security.KeyStore;
|
||||
import java.util.Collections;
|
||||
|
||||
import javax.net.ssl.KeyManagerFactory;
|
||||
import javax.net.ssl.TrustManagerFactory;
|
||||
|
||||
import com.google.common.collect.ImmutableMap;
|
||||
import org.junit.Assert;
|
||||
import org.junit.Rule;
|
||||
import org.junit.Test;
|
||||
import org.junit.rules.ExpectedException;
|
||||
|
||||
import com.datastax.driver.core.SSLOptions;
|
||||
import com.datastax.driver.core.exceptions.NoHostAvailableException;
|
||||
import com.datastax.shaded.netty.handler.ssl.SslContext;
|
||||
import com.datastax.shaded.netty.handler.ssl.SslContextBuilder;
|
||||
import org.apache.cassandra.distributed.Cluster;
|
||||
import org.apache.cassandra.distributed.api.Feature;
|
||||
|
||||
public class NativeTransportEncryptionOptionsTest extends AbstractEncryptionOptionsImpl
|
||||
{
|
||||
@Rule
|
||||
public ExpectedException expectedException = ExpectedException.none();
|
||||
|
||||
@Test
|
||||
public void nodeWillNotStartWithBadKeystore() throws Throwable
|
||||
{
|
||||
|
|
@ -219,4 +234,90 @@ public class NativeTransportEncryptionOptionsTest extends AbstractEncryptionOpti
|
|||
assertCannotStartDueToConfigurationException(cluster);
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testEndpointVerificationDisabledIpNotInSAN() throws Throwable
|
||||
{
|
||||
// When required_endpoint_verification is set to false, client certificate Ip/hostname should be validated
|
||||
// The certificate in cassandra_ssl_test_outbound.keystore does not have IP/hostname embeded, so when
|
||||
// require_endpoint_verification is false, the connection should be established
|
||||
testEndpointVerification(false, true);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testEndpointVerificationEnabledIpNotInSAN() throws Throwable
|
||||
{
|
||||
// When required_endpoint_verification is set to true, client certificate Ip/hostname should be validated
|
||||
// The certificate in cassandra_ssl_test_outbound.keystore does not have IP/hostname emebeded, so when
|
||||
// require_endpoint_verification is true, the connection should not be established
|
||||
testEndpointVerification(true, false);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testEndpointVerificationEnabledWithIPInSan() throws Throwable
|
||||
{
|
||||
// When required_endpoint_verification is set to true, client certificate Ip/hostname should be validated
|
||||
// The certificate in cassandra_ssl_test_outbound.keystore have IP/hostname emebeded, so when
|
||||
// require_endpoint_verification is true, the connection should be established
|
||||
testEndpointVerification(true, true);
|
||||
}
|
||||
|
||||
private void testEndpointVerification(boolean requireEndpointVerification, boolean ipInSAN) throws Throwable
|
||||
{
|
||||
try (Cluster cluster = builder().withNodes(1).withConfig(c -> {
|
||||
c.with(Feature.NATIVE_PROTOCOL);
|
||||
c.set("client_encryption_options",
|
||||
ImmutableMap.builder().putAll(validKeystore)
|
||||
.put("enabled", true)
|
||||
.put("require_client_auth", true)
|
||||
.put("require_endpoint_verification", requireEndpointVerification)
|
||||
.build());
|
||||
}).start())
|
||||
{
|
||||
InetAddress address = cluster.get(1).config().broadcastAddress().getAddress();
|
||||
SslContextBuilder sslContextBuilder = SslContextBuilder.forClient();
|
||||
if (ipInSAN)
|
||||
sslContextBuilder.keyManager(createKeyManagerFactory("test/conf/cassandra_ssl_test_endpoint_verify.keystore", "cassandra"));
|
||||
else
|
||||
sslContextBuilder.keyManager(createKeyManagerFactory("test/conf/cassandra_ssl_test_outbound.keystore", "cassandra"));
|
||||
|
||||
SslContext sslContext = sslContextBuilder.trustManager(createTrustManagerFactory("test/conf/cassandra_ssl_test.truststore", "cassandra"))
|
||||
.build();
|
||||
final SSLOptions sslOptions = socketChannel -> sslContext.newHandler(socketChannel.alloc());
|
||||
com.datastax.driver.core.Cluster driverCluster = com.datastax.driver.core.Cluster.builder()
|
||||
.addContactPoint(address.getHostAddress())
|
||||
.withSSL(sslOptions)
|
||||
.build();
|
||||
|
||||
if (!ipInSAN)
|
||||
{
|
||||
expectedException.expect(NoHostAvailableException.class);
|
||||
}
|
||||
|
||||
driverCluster.connect();
|
||||
}
|
||||
}
|
||||
|
||||
private KeyManagerFactory createKeyManagerFactory(final String keyStorePath,
|
||||
final String keyStorePassword) throws Exception
|
||||
{
|
||||
final InputStream stream = new FileInputStream(keyStorePath);
|
||||
final KeyStore ks = KeyStore.getInstance("JKS");
|
||||
ks.load(stream, keyStorePassword.toCharArray());
|
||||
final KeyManagerFactory kmf = KeyManagerFactory.getInstance(KeyManagerFactory.getDefaultAlgorithm());
|
||||
kmf.init(ks, keyStorePassword.toCharArray());
|
||||
return kmf;
|
||||
}
|
||||
|
||||
private TrustManagerFactory createTrustManagerFactory(final String trustStorePath,
|
||||
final String trustStorePassword) throws Exception
|
||||
{
|
||||
final InputStream stream = new FileInputStream(trustStorePath);
|
||||
final KeyStore ts = KeyStore.getInstance("JKS");
|
||||
ts.load(stream, trustStorePassword.toCharArray());
|
||||
final TrustManagerFactory tmf = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm());
|
||||
tmf.init(ts);
|
||||
return tmf;
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue