Securing MongoDB With TLS (Part 2 of 3)
Carl Tashian
This is a series showing how to secure MongoDB using TLS.
In the intro post on mongodb.com, we covered why mutual TLS is such a good fit for securing MongoDB.
In part one, we created and configured a Certificate Authority that can issue TLS certificates for MongoDB servers, cluster members, and clients.
In this post, we'll set up a simple single-node MongoDB server that uses mutual TLS to encrypt traffic between all clients and servers. We'll get server certificates from our CA, and we'll set up automated renewal of the server certificate. Then we'll get a client certificate so we can enforce client validation and require TLS for all inbound MongoDB connections.
Part 2: Securing a single-node MongoDB server
Create your server
I've written a server init script that will create the MongoDB server on a stock Ubuntu 20.10 (Focal) machine. To spin up my server, I configured this script as User Data on a new EC2 instance. Let's walk through the most important parts of the script.
At the top we have the CA_URL
, CA_FINGERPRINT
, and MONGO_SERVICE_USER_CA_PASSWORD
. Configure these based on your CA setup from part 1.
The first part of the script installs Docker and Docker Compose, the step
CLI tool, and the mongo
CLI client (for testing).
Next, it configures the step
client to connect with our CA for certificates:
step ca bootstrap --ca-url "$CA_URL" --fingerprint "$CA_FINGERPRINT"
The /var/lib/mongo/compose.yml
sets up the MongoDB server itself:
services:
mongo:
image: mongo
command: ["--bind_ip_all", "--tlsMode", "requireTLS", "--tlsCAFile", "/usr/local/share/ca-certificates/root_ca.crt", "--tlsCertificateKeyFile", "/run/secrets/server-certificate"]
volumes:
- ca-certs:/usr/local/share/ca-certificates
- $PWD/db:/data/db
secrets:
- server-certificate
ports:
- '27017-27019:27017-27019'
secrets:
server-certificate:
file: mongo.pem
This MongoDB server will require all inbound connections to use TLS and to present a client certificate signed by our CA. This setting will effectively keep your MongoDB instance out of reach from would-be attackers. And it's independent of user authentication: Even if your MongoDB installation has no user authentication enabled, you will still
The combined server certificate and private key PEM file is injected into the Docker container using a Docker Compose secret, and the CA's root certificate is imported via a volume mount. Both are needed to configure MongoDB for TLS. There's no configuration file for MongoDB here—all configuration is done via the flags shown in command:
.
Next, the script gets an initial certificate for the MongoDB server. MongoDB requires the certificate and private key to be concatenated into one PEM file, so we do that too:
step ca certificate $LOCAL_HOSTNAME mongo.crt mongo.key \
--provisioner "MongoDB Server" --san $LOCAL_HOSTNAME --san $PUBLIC_HOSTNAME
cat mongo.crt mongo.key > mongo.pem
chmod 600 mongo.pem
chown 999 mongo.pem
That chown 999
is a bit strange. Docker requires secrets to have the same ownership on the host as they do in the container, and the mongo
Docker container runs everything with a service user with UID 999. So, we have to make sure the mongo.pem
on the host is going to be accessible by that service user.
You can see the certificate details using step certificate inspect
:
$ step certificate inspect /var/lib/mongo/mongo.crt --short
X.509v3 TLS Certificate (ECDSA P-256) [Serial: 3070...8623]
Subject: ec2-18-example.us-east-2.compute.amazonaws.com
Issuer: Smallstep Intermediate CA
Provisioner: MongoDB Server [ID: 9kU2...Yhmw]
Valid from: 2021-06-23T20:53:43Z
to: 2021-09-21T20:54:43Z
Automate TLS certificate renewal
Next, the script will installs our systemd
-based certificate renewal timer. I strongly recommend you read our documentation on Renewal using systemd timers for background on how these timers work.
The timer for our server certificate will renew the certificate after two-thirds of its lifetime has elapsed.
For the MongoDB server certificate, the script sets up a special renewal workflow. I won't get into the details of that here, but this workflow will renew the certificate, put the combined PEM file together, then restart Docker Compose. Here's the systemd service unit override that drives renewal:
[Service]
Environment=STEPPATH=/root/.step \
CERT_LOCATION=/var/lib/mongo/mongo.crt \
KEY_LOCATION=/var/lib/mongo/mongo.key
WorkingDirectory=/var/lib/mongo
; Restart Docker containers after the certificate is successfully renewed.
ExecStartPost=/usr/bin/env bash -c 'cat \${CERT_LOCATION} \${KEY_LOCATION} > mongo.pem'
ExecStartPost=/usr/local/bin/docker-compose restart
Finally, the script starts up the MongoDB server docker container.
Testing your TLS connection:
You should now be able to connect to your database using TLS. As a user that has bootstrapped to the CA and installed mongosh
, run the following:
$ EMAIL="carl@example.com"
$ step ca certificate $EMAIL carl.crt carl.key \
--provisioner "MongoDB Service User" \
--provisioner-password-file /var/lib/mongo/ca-password.txt
$ cat carl.crt carl.key > carl.pem
$ mongosh --tls --tlsCertificateKeyFile carl.pem --tlsCAFile ~/.step/certs/root_ca.crt --host ip-172-example.us-east-2.compute.internal
If you want all programs to trust your internal CA (not just
mongosh
) you can runstep certificate install ~/.step/certs/root_ca.crt
as root to install your CA root certificate into the system-wide trust store. You can then remove then--tlsCAFile
argument when runningmongosh
. You now have a single-node MongoDB server that's secured with mutual TLS.
In part three, we'll create a MongoDB replication cluster and secure both cluster and client/server traffic with TLS.
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 🎛️🎚️