Using SSL for Phantom connections to Apache Cassandra
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.