Smallstep Certificate Manager | Your Hosted Private CA

Securing MongoDB With TLS (Part 1 of 3)

Carl-Tashian.jpg

Carl Tashian

Follow Smallstep

In the intro post on mongodb.com, we covered why mutual TLS is such a good fit for securing MongoDB. In this post, we're going deploy a Certificate Authority that can issue TLS certificates to MongoDB.

Step-by-step TLS Deployment of MongoDB

Here are the steps required to secure MongoDB with TLS:

  • Set up a step-ca Certificate Authority server.
  • For Mongo server validation, issue a certificate and private key to the MongoDB server and configure server TLS.
  • For Mongo client validation, issue certificates and private keys to clients and configure client-side TLS.
  • For Mongo cluster member validation, issue cluster TLS certificates and private keys to MongoDB nodes.
  • Note: It’s possible to stage the migration of an existing MongoDB database to TLS: You can make TLS connections to MongoDB optional, and only require client certificates once you’ve migrated all of your clients.

Part One: Setting up a Certificate Authority

Create a Certificate Authority

I've written a system init script that sets up a CA, configureing it to issue MongoDB client, server, and cluster TLS certificates. Change the configuration values at the top of this script, run it on an Ubuntu 20.04 (Focal) Linux machine, and boom! You now have an online Certificate Authority up and running.

I'm not going to walk through the entire CA script here. Instead, let's step through the parts of the script that configure the Certificate Authority to issue certificates for MongoDB.

The first section of the script installs the step-ca server and the [step CLI tool](https://github.com/smallstep/cli) that controls it. (If you want to do this part manually, you can follow our installation docs for step-ca.)

The next section is where we initialize our Certificate Authority:

# Set up our basic CA configuration and generate root keys
step ca init --name="$CA_NAME"
     --dns="$LOCAL_IP,$LOCAL_HOSTNAME,$PUBLIC_IP,$PUBLIC_HOSTNAME"
     --address=":443" --provisioner="$CA_EMAIL"
     --password-file="$STEPPATH/password.txt"

This step ca init command creates a basic configuration and CA private keys and certificates so that our Certificate Authority can run. The CA server will run on 0.0.0.0:443 and its TLS certificate will have all of the values specified in --dns listed as Subjects. Note that this script pulls the _IP and _HOSTNAME variables from the AWS metadata API. But if you're not using AWS, you can set these values manually.

The --password-file points to a file containing the password that will be used both to encrypt the CA private keys, and to secure the CA's admin provisioner. Provisioners are the CA's methods for issuing certificates. The admin provisioner lets you issue any certificate (using the step ca certificate subcommand)—so keep the password safe!

Before you continue, make a note of the hostname of your CA, and the CA fingerprint (a hex value). You'll need both of these to access the CA remotely. If you ran the script using EC2 User Data, SSH into the CA VM and run step certificate fingerprint /etc/step-ca/certs/root_ca.crt as root to get the fingerprint of your CA.

Add MongoDB certificate templates

The next step is to add two certificate templates to step-ca for MongoDB client and server certificates, so that the certificate subject and key usages on our certificates will match MongoDB's stringent requirements:

mkdir -p /etc/step-ca/templates/x509
# Server cert template.
cat <<EOF > /etc/step-ca/templates/x509/server.tpl
{
    "subject": {
        "organization": {{ toJson .Organization }},
        "commonName": {{ toJson .Subject.CommonName }},
{{- if .OrganizationalUnit }}
        "organizationalUnit": {{ toJson .OrganizationalUnit }}
{{- end }}
    },
    "sans": {{ toJson .SANs }},
    "keyUsage": ["digitalSignature"],
    "extKeyUsage": ["serverAuth", "clientAuth"]
}
EOF
## Client (and cluster) cert template
cat <<EOF > /etc/step-ca/templates/x509/client.tpl
{
    "subject": {
        "organization": {{ toJson .Organization }},
{{- if .OrganizationalUnit }}
        "organizationalUnit": {{ toJson .OrganizationalUnit }},
{{- end }}
        "commonName": {{ toJson .Subject.CommonName }}
    },
    "sans": {{ toJson .SANs }},
    "keyUsage": ["digitalSignature"],
    "extKeyUsage": ["clientAuth"]
}
EOF

In MongoDB, client certificates and cluster member certificates are similar enough that they can share a single template. The only difference is that the value of the subject Organizational Unit (OU=) must differ between client certificates and cluster certificates, and must be the same between server certificates and cluster member certificates.

Add MongoDB certificate provisioners

The next step in the script is to create certificate provisioners specific to MongoDB. The script creates three provisioners:

  • For MongoDB server certificates, an ACME provisioner. The ACME protocol (RFC8555) is the protocol that Let’s Encrypt uses to automate certificate management for websites. We'll use this in part 2, when we set up a single-node MongoDB server.
  • For MongoDB cluster member certificates, a second ACME provisioner. We'll use this in Part 3 when we secure a MongoDB replication cluster.
  • For MongoDB client certificates, a JWK "Service User" provisioner. The JWK provisioner uses token authentication to issue client certificates to bots, service accounts, or humans using a signed one-time-use token that contains the certificate request.

MongoDB server certificate provisioner (ACME)

The script creates an ACME provisioner for MongoDB server certificates:

step ca provisioner add "MongoDB Server" --type=acme

Finally, the script will configure the provisioner to use the server.tpl template, and to issue 90 day TLS certificates by default:

cat <<< $(jq '(.authority.provisioners[] | select(.name == "MongoDB Server")) += {
            "claims": {
               "maxTLSCertDuration": "2160h",
               "defaultTLSCertDuration": "2160h"
        },
        "options": {
                "x509": {
                        "templateFile": "templates/x509/server.tpl",
                        "templateData": {
                                "Organization": "'${DN_ORG_NAME}'",
                                "OrganizationalUnit": "'${SERVER_DN_ORG_UNIT}'"
                        }
                }
        }
    }' /etc/step-ca/config/ca.json) > /etc/step-ca/config/ca.jso

MongoDB Cluster Certificate Provisioner (ACME)

The cluster certificate provisioner will also use ACME.

step ca provisioner add "MongoDB Cluster" --type=acme

It uses the client.tpl template but is otherwise the same as the server provisioner above.

cat <<< $(jq '(.authority.provisioners[] | select(.name == "MongoDB Cluster")) += {
            "claims": {
               "maxTLSCertDuration": "2160h",
               "defaultTLSCertDuration": "2160h"
        },
        "options": {
                "x509": {
                        "templateFile": "templates/x509/client.tpl",
                        "templateData": {
                                "Organization": "'${DN_ORG_NAME}'",
                                "OrganizationalUnit": "'${SERVER_DN_ORG_UNIT}'"
                        }
                }
        }
    }' /etc/step-ca/config/ca.json) > /etc/step-ca/config/ca.json

MongoDB Service User Certificate Provisioner

We also need to get certificates to MongoDB clients. The script adds a JWK provisioner for this purpose, and configures it to use the client CA template and a Subject Organizational Unit (OU=) value reserved for MongoDB clients:

echo "$MONGO_SERVICE_USER_CA_PASSWORD" > /etc/step-ca/client-password.txt
step ca provisioner add "MongoDB Service User" --create --password-file /etc/step-ca/client-password.txt
cat <<< $(jq '(.authority.provisioners[] | select(.name == "MongoDB Service User")) += {
            "claims": {
               "maxTLSCertDuration": "2160h",
               "defaultTLSCertDuration": "2160h"
        },
        "options": {
                "x509": {
                        "templateFile": "templates/x509/client.tpl",
                        "templateData": {
                                "Organization": "'${DN_ORG_NAME}'",
                                "OrganizationalUnit": "'${CLIENT_DN_ORG_UNIT}'"
                        }
                }
        }
    }' /etc/step-ca/config/ca.json) > /etc/step-ca/config/ca.json

Start the Certificate Authority

Finally, the script sets up step-ca to run as a daemon, and starts it running in the background. To do this part manually, follow Running step-ca As A Daemon.

curl -sL https://raw.githubusercontent.com/smallstep/certificates/master/systemd/step-ca.service -o /etc/systemd/system/step-ca.service
systemctl daemon-reload
chown -R step:step $(step path)
systemctl enable --now step-ca

After running the script, you should see that step-ca is running. You can run step ca health to check it, or look at the logs (journalctl -fu step-ca).

Now that you have a CA running, it's time to configure the MongoDB server to use TLS certificates.

In part 2, we'll create a simple single-node MongoDB server that requires mutual TLS, and we'll use Mutual TLS to connect to it with a client certificate.

Further reading

MongoDB: Client certificate requirements

MongoDB: Client authentication manual page

MongoDB: Use x.509 Certificate for Membership Authentication

Carl Tashian (Website, LinkedIn) is an engineer, writer, exec coach, and startup all-rounder. He's currently an Offroad Engineer at Smallstep. He co-founded and built the engineering team at Trove, and he wrote the code that opens your Zipcar. He lives in San Francisco with his wife Siobhan and he loves to play the modular synthesizer 🎛️🎚️