Cassandra SSL certificates rotation

December 3, 2022
Sergio Rua

Introduction

Security is one of the most important considerations when running any database. But it is also one of the most challenging.

You don’t just want to encrypt the data between nodes and clients but also to ensure the SSL certificates are not used for a long time.

The safer thing to do is to renew certificates frequently. If for some reason they are leaked, they will not be valid for too long restoring security to the data.

There are multiple solutions to this problem. Mine is to use HashiCorp Vault as a CA and consul-templates for the rotation.

HashiCorp Vault

I configure my vault cluster using terraform, it’s the easier way to manage the configuration. If you don’t use terraform, you can find instructions on how to set it up manually from HashiCorp’s website.

The code below performs all necessary configs to allow clients to issue certs for a domain called here example.com and all its subdomains.

locals {
  dns_domains = [
    "example.com",
  ]
}

resource "vault_mount" "pki" {
  path                      = "pki"
  type                      = "pki"
  default_lease_ttl_seconds = 691200  # 8 days
  max_lease_ttl_seconds     = 2764800 # 30 days
}

resource "vault_pki_secret_backend_role" "pki" {
  backend            = vault_mount.pki.path
  name               = "vdp"
  ttl                = 2419200
  allow_ip_sans      = true
  key_type           = "rsa"
  key_bits           = 4096
  allowed_domains    = local.dns_domains
  allow_subdomains   = true
  allow_glob_domains = true
}

resource "vault_pki_secret_backend_intermediate_cert_request" "axonops" {
  depends_on  = [vault_mount.pki]
  backend     = vault_mount.pki.path
  type        = "internal"
  common_name = local.dns_domain[0]
}

resource "vault_pki_secret_backend_root_cert" "axonops" {
  depends_on           = [vault_mount.pki]
  backend              = vault_mount.pki.path
  type                 = "internal"
  common_name          = "Root CA"
  ttl                  = "315360000"
  format               = "pem"
  private_key_format   = "der"
  key_type             = "rsa"
  key_bits             = 4096
  exclude_cn_from_sans = true
  ou                   = "Cassandra"
  organization         = "AxonOps"
}

resource "vault_pki_secret_backend_root_sign_intermediate" "root" {
  depends_on           = [vault_pki_secret_backend_intermediate_cert_request.axonops]
  backend              = vault_mount.pki.path
  csr                  = vault_pki_secret_backend_intermediate_cert_request.axonops.csr
  common_name          = "Intermediate CA"
  exclude_cn_from_sans = true
  ou                   = "Cassandra"
  organization         = "AxonOps"
}

resource "vault_pki_secret_backend_config_urls" "axonops" {
  backend = vault_mount.pki.path
  issuing_certificates = [
    "https://vault:8200/v1/pki/ca",
  ]
  crl_distribution_points = [
    "https://vault:8200/v1/pki/crt",
  ]
}

Consul Template

There are many ways to create SSL certs from Vault and configure them in Cassandra. You can use the following:

  • a simple cron job with a script
  • orchestration such as Ansible
  • remote execution using tools such as Rundeck
  • long etc here

I have chosen to use consul-templates. The consul-template daemon can query Vault to retrieve the SSL cert with two added bonuses: it will update the cert when it expires and it can run an arbitrary command (a script here) that I will use to reload the certificates.

You can save the template definition (contents in the example) into different files but I prefer to use it all in one as it reduces the number of config files I need to copy over to each Cassandra node.

vault {
  address = "https://vault:8200"
  token = "TOKEN"

  # must be false if using root token
  renew_token = false
  ssl {
    enabled = true
    verify = false
  }
}
template {
    contents = "{{ with secret \"pki/issue/vdp\" \"common_name=cass000.domain.com\" \"ip_sans=1.1.1.1\"}}{{ .Data.private_key }}{{ end }}"
    destination = "/opt/ssl/cassandra-certs.key"
    create_dest_dirs = true
    command = "/opt/consul_template/cassandra-certs/restart.sh"
}
template {
    contents = "{{ with secret \"pki/issue/vdp\" \"common_name=cass001.domain.com\" \"ip_sans=1.1.1.1\"}}{{ .Data.certificate }}{{ end }}"
    destination = "/opt/ssl/cassandra-certs.crt"
    create_dest_dirs = true
    command = "/opt/consul_template/cassandra-certs/restart.sh"
}
template {
    contents = "{{ with secret \"pki/issue/vdp\" \"common_name=cass002.domain.com\" \"ip_sans=1.1.1.1\"}}{{ .Data.issuing_ca }}{{ end }}"
    destination = "/opt/ssl/cassandra-certs.ca"
    create_dest_dirs = true
    command = "/opt/consul_template/cassandra-certs/restart.sh"
}

Finally, you need something to convert the PEM files into keystore.jks This is the function of the restart.sh script invoked from consul-templates:

#!/bin/bash

CA=/opt/ssl/cassandra-certs.ca
CRT=/opt/ssl/cassandra-certs.crt
KEY=/opt/ssl/cassandra-certs.key
NODE=cass001.example.com
TRUST_STORE_PASSWORD="CHANGEME"
JKS_PASSWD="CHANGEME"
TMPDIR=$(mktemp -d /tmp/key.XXXXX)
TMPKEYSTORE="$TMPDIR/keystore.jks"
TMPTRUSTSTORE="$TMPDIR/truststore.jks"
P12TMP="$TMPDIR/keystore.p12"

trap "rm -rf ${TMPDIR}" EXIT

cd /opt/ssl
openssl verify -CAfile $CA $CRT

# Generate PKCS12 keystore from private key, host cert and intermediates
openssl pkcs12 -export -in "$CRT" -inkey "$KEY" -name $NODE -out "$P12TMP" -passout pass:"$JKS_PASSWD"

# Import the PKCS12 into a JKS keystore
keytool -noprompt -importkeystore -deststorepass "$JKS_PASSWD" -destkeystore "$TMPKEYSTORE" -srckeystore "$P12TMP" -srcstoretype PKCS12 -srcstorepass "$JKS_PASSWD"
# Add the CA certificate to the keystore
keytool -noprompt -import -alias caroot -keystore "$TMPKEYSTORE" -keyalg RSA -storepass "$JKS_PASSWD" -file "$CA"

# Generate the truststore containing just the CA certificate
keytool -noprompt -import -alias caroot -keystore "$TMPTRUSTSTORE" -keyalg RSA -storepass "$TRUST_STORE_PASSWORD" -file "$CA"

cp -f $TMPKEYSTORE /opt/cassandra/ssl/keystore.jks
cp -f $TMPTRUSTSTORE /opt/cassandra/ssl/truststore.jks

/opt/cassandra/bin/nodetool -Dcom.sun.jndi.rmiURLParsing=legacy reloadssl

Note the last command in the script, this is the most important. It causes Cassandra to reload the SSL without restarting. Pretty cool!

/opt/cassandra/bin/nodetool -Dcom.sun.jndi.rmiURLParsing=legacy reloadssl

Conclusion

Improve your security with strong SSL certificates and make sure you rotate them often.

Subscribe to newsletter

Subscribe to receive the latest blog posts to your inbox every week.

By subscribing you agree to with our Privacy Policy.
Thank you! Your submission has been received!
Oops! Something went wrong while submitting the form.

Ready to Transform 

Your Business?