Using SSL for Phantom connections to Apache Cassandra

Thomas Bach
ash_blog
Published in
3 min readFeb 22, 2018

At Midas we have a security first approach. We try to enable authentication and encryption at all stages. Often, backend-communication is an overlooked piece of the stack when it comes to applying this principle. The backend should be your realm and you are in control what is running there and what is not, right? This argument is definitely not valid per se, and even less when deploying your code in a microservice architecture to a cloud-platform like AWS.

We are happily using the phantom driver to interact with Apache Cassandra. When I had a first look into securing the connection between our backend and the database, I was scared not to see any documentation for this. I almost came to the conclusion that we would have to drop phantom and go for a driver which properly supports encryption.

But, I stepped back a bit, looked into phantom’s underlying Datastax Java Driver and found a test for SSL authenticated encryption. Hence, when Datastax supports SSL, phantom must support SSL as well. In these tests they mainly hook into Java CAPS for SSL. So, we first need key- and truststores.

Setting up Key- and Truststore

I go quickly through the needed steps here. Read up on this tutorial for further information.

First, create a root CA, export its certificate and import it into your truststore:

keytool -genkey -keyalg RSA -keysize 8192 \
-dname "cn=CA, ou=Backend, o=Midas, c=CH" \
-keystore ca.jks -storetype pkcs12 -alias ca \
-validity $((365 * 5))
chmod 600 ca.jks
keytool -export -keystore ca.jks -alias ca -rfc -file ca.cer
keytool -v -import -keystore truststore.jks -file ca.cer

Create a certificate for the first Cassandra node and import the root CA:

keytool -genkey -keyalg RSA -keysize 4096 \
-dname "cn=*, ou=Backend, o=Midas, c=" \
-keystore node01_client.jks -storetype pkcs12 \
-alias "node01" -validity 365
chmod 600 node01_client.jks
keytool -v -import -keystore node01_client.jks -alias ca \
-file ca.cer

Create a signing request, sign it and re-import the result:

keytool -certreq -keystore node01_client.jks -alias node01 \
-file node01_client.csr
keytool -gencert -rfc -infile node01_client.csr \
-validity 365 -keystore ca.jks -alias ca \
-outfile node01_client.cer
keytool -import -keystore node01_client.jks -alias node01 \
-file node01_client.cer

Repeat the steps above for your services:

keytool -genkey -keyalg RSA -keysize 4096 \
-dname "cn=*, ou=Backend, o=Midas, c=" \
-keystore service01.jks -storetype pkcs12 \
-alias service01 -validity 365
chmod 600 service01.jks
keytool -import -keystore service01.jks -alias root_ca \
-file root_ca.cer
keytool -certreq -keystore service01.jks -alias service01 \
-file service01.csr
keytool -gencert -rfc -infile service01.csr \
-validity 365 -keystore ca.jks -alias ca \
-outfile service01.cer
keytool -import -keystore service01.jks -alias service01 \
-file service01.cer

Configure Cassandra

We have to tell Cassandra to use our truststore truststore.jks and the keystore node01_client.jks:

native_transport_port: 9042
native_transport_port_ssl: 9142
client_encryption_options:
enabled: true
optional: false
keystore: /var/lib/cassandra/node01_client.jks
keystore_password: <secret>
require_client_auth: true
truststore: /var/lib/cassandra/truststore.jks
truststore_password: <secret>

Ensure that only Cassandra can read these files!

With the configuration above you have two ports to connect to Cassandra. You can either connect without encryption on port 9042 or on port 9142 where encryption and authentication is a must-have. We firewall port 9042. But it comes handy when we want to troubleshoot locally using cqlsh.

Configure Phantom

Now, lets have a look at the phantom site again. As said above, we will have to use Java CAPS for SSL. So, you either need something along these lines in your code

def setSSLProperties(): Unit = {
System.setProperty(
"javax.net.ssl.keyStore", "/path/to/service01.jks"
)
System.setProperty(
"javax.net.ssl.keyStorePassword", "<secret>"
)
System.setProperty(
"javax.net.ssl.trustStore", "/path/to/truststore.jks"
)
System.setProperty(
"javax.net.ssl.trustStorePassword", "<secret>"
)
}

or you set these properties via the command line. Now comes the interesting part: phantom provides you the factories ContactPoint and ContactPoints. Both factories give you back an instance of KeySpaceBuilder which provides this cute little helper:

def withClusterBuilder(builder: ClusterBuilder): KeySpaceBuilder =
new KeySpaceBuilder(clusterBuilder andThen builder)

Now, what’s this ClusterBuilder thing? It's a type synonym:

type ClusterBuilder = (Cluster.Builder => Cluster.Builder)

where Cluster.Builder comes from the Datastax driver. Cluster.Builder provides a withSSL function which returns a Cluster.Builder, i.e. _.withSSL() is a function of type Cluster.Builder => Cluster.Builder. So, all we have to do is something like this:

def connection: CassandraConnection = {
ContactPoints(contactPoints, port)
.withClusterBuilder(_.withSSL())
.keySpace(keySpace)
}

We then pass the CassandraConnection to our Database and have a fully encrypted client-to-server connection with authentication on both sides.

--

--

Thomas Bach
ash_blog

DevOp using functional programming where possible.