MongoDB SSL/TLS with X509 Authentication

Jacob Bills

March 07, 2020

Hello and welcome to the first tutorial in my blog. For this tutorial I am setting up a 3 node mongodb replica set with TLS and enabling X509 client authentication.

I created 3 ec2 instances running a custom AMI I built for this demo. Heres a blog post that goes into more detail how that AMI was built. TLDR; its a fresh installation of CentOS-7-x86_64-Minimal-1908.iso with a preloaded ssh public key in roots authorized keys


In this blog


Create ec2 instances

See here for a complete specification of the ec2 run-instances command
#!/bin/bash
aws ec2 run-instances --image-id ami-00446be8a2dcad866 --count 1 --instance-type t2.micro --key-name id_rsa_vlog --security-group-ids sg-2f055c55 --subnet-id subnet-1ba30952 --tag-specifications 'ResourceType=instance,Tags=[{Key=Name,Value=mongo-rs0-00}]'
aws ec2 run-instances --image-id ami-00446be8a2dcad866 --count 1 --instance-type t2.micro --key-name id_rsa_vlog --security-group-ids sg-2f055c55 --subnet-id subnet-1ba30952 --tag-specifications 'ResourceType=instance,Tags=[{Key=Name,Value=mongo-rs0-01}]'
aws ec2 run-instances --image-id ami-00446be8a2dcad866 --count 1 --instance-type t2.micro --key-name id_rsa_vlog --security-group-ids sg-2f055c55 --subnet-id subnet-1ba30952 --tag-specifications 'ResourceType=instance,Tags=[{Key=Name,Value=mongo-rs0-02}]'

Provision server for MongoDB

#!/bin/bash
yum install wget libcurl openssl telnet -y
firewall-cmd --permanent --add-port=27017/tcp
firewall-cmd --reload
wget https://fastdl.mongodb.org/linux/mongodb-linux-x86_64-rhel70-4.2.3.tgz
tar -xvf mongodb-linux-x86_64-rhel70-4.2.3.tgz
mv mongodb-linux-x86_64-rhel70-4.2.3 /opt
ln -sf /etc/alternatives/mongo /bin/mongo
ln -sf /etc/alternatives/mongod /bin/mongod
ln -sf /opt/mongodb-linux-x86_64-rhel70-4.2.3/bin/mongo /etc/alternatives/mongo
ln -sf /opt/mongodb-linux-x86_64-rhel70-4.2.3/bin/mongod /etc/alternatives/mongod
mkdir /data

MongoDB certificate subject requirements


Create certificate authority (CA)

Linux
#!/bin/bash
openssl req -passout pass:password -new -x509 -days 3650 -extensions v3_ca -keyout ca_private.pem -out ca.pem -subj "/CN=CA/OU=MONGO/O=BUSTEDWARE/L=PAINESVILLE/ST=OH/C=US"
OSX
I had to include the -config parameter to use the /usr/local/etc/openssl@1.1/openssl.cnf file when creating the CA on OSX
#!/bin/bash
openssl req -passout pass:password -new -x509 -days 3650 -extensions v3_ca -keyout ca_private.pem -out ca.pem -subj "/CN=CA/OU=MONGO/O=BUSTEDWARE/L=PAINESVILLE/ST=OH/C=US" -config /usr/local/etc/openssl@1.1/openssl.cnf

Create key and certificate signing requests (CSR)

Note that the OU is different between the client and members of the replica set

Clients of replica set
#!/bin/bash
openssl req -newkey rsa:4096 -nodes -out client.csr -keyout client.key -subj '/CN=JacobTBills/OU=MONGO_CLIENTS/O=BUSTEDWARE/L=PAINESVILLE/ST=OH/C=US'
Members of replica set
#!/bin/bash
openssl req -newkey rsa:4096 -nodes -out node1.csr -keyout node1.key -subj '/CN=ip-172-31-7-201.ec2.internal/OU=MONGO/O=BUSTEDWARE/L=PAINESVILLE/ST=OH/C=US'
openssl req -newkey rsa:4096 -nodes -out node2.csr -keyout node2.key -subj '/CN=ip-172-31-5-30.ec2.internal/OU=MONGO/O=BUSTEDWARE/L=PAINESVILLE/ST=OH/C=US'
openssl req -newkey rsa:4096 -nodes -out node3.csr -keyout node3.key -subj '/CN=ip-172-31-4-24.ec2.internal/OU=MONGO/O=BUSTEDWARE/L=PAINESVILLE/ST=OH/C=US'

Sign the certificate signing requests with CA

Client
You cannot use a single client certificate to authenticate more than one MongoDB user.
#!/bin/bash
openssl x509 -passin pass:password -sha256 -req -days 365 -in client.csr -CA ca.pem -CAkey ca_private.pem -CAcreateserial -out client-signed.crt
Server
For the mongod server instances I will specify the subject alternative name to be included in the signed certificate extensions.
#!/bin/bash
# sign node 1 csr
openssl x509 -passin pass:password -sha256 -req -days 365 -in node1.csr -CA ca.pem -CAkey ca_private.pem -CAcreateserial -out node1-signed.crt -extensions v3_req -extfile <(
cat << EOF
[ v3_req ]
subjectAltName = @alt_names

[ alt_names ]
DNS.1 = 127.0.0.1
DNS.2 = localhost
DNS.3 = ip-172-31-7-201.ec2.internal
EOF
)

# sign node 2 csr
openssl x509 -passin pass:password -sha256 -req -days 365 -in node2.csr -CA ca.pem -CAkey ca_private.pem -CAcreateserial -out node2-signed.crt -extensions v3_req -extfile <(
cat << EOF
[ v3_req ]
subjectAltName = @alt_names

[ alt_names ]
DNS.1 = 127.0.0.1
DNS.2 = localhost
DNS.3 = ip-172-31-5-30.ec2.internal
EOF
)

# sign node 3 csr
openssl x509 -passin pass:password -sha256 -req -days 365 -in node3.csr -CA ca.pem -CAkey ca_private.pem -CAcreateserial -out node3-signed.crt -extensions v3_req -extfile <(
cat << EOF
[ v3_req ]
subjectAltName = @alt_names

[ alt_names ]
DNS.1 = 127.0.0.1
DNS.2 = localhost
DNS.3 = ip-172-31-4-24.ec2.internal
EOF
)


Create the privacy enhanced mail (PEM) file for mongod

The PEM file is a combination of the signed certificate and private key. I create the PEM file for the client and each member of the replica set, then transfer each file to its respective server we have running in AWS for this demo
#!/bin/bash
cat client-signed.crt client.key > client.pem
cat node1-signed.crt node1.key > node1.pem
cat node2-signed.crt node2.key > node2.pem
cat node3-signed.crt node3.key > node3.pem

scp node1.pem ca.pem client.pem root@X.X.X.X:/root
scp node2.pem ca.pem root@Y.Y.Y.Y:/root
scp node3.pem ca.pem root@Z.Z.Z.Z:/root
Note that I also copied the CA certificate (ca.pem) to each server. This is a requirement for hostname verification when starting the mongod instance. I also copied the client.pem file to node1 so that I can connect and run administrative commands after creating the user in the x509 authentication database.

Create the replica set

Make sure to bind to both local interface and IPv4 interfaces with mongod. You will need to use the localhost exception to create the first user. After creating the first user you may restart mongod and only specify the interfaces you need

Node1
#!/bin/bash
mongod --bind_ip 127.0.0.1,172.31.7.201 --replSet rs0 --dbpath /data --logpath /data/mongod.log --port 27017 --fork --wiredTigerCacheSizeGB 1 --sslMode requireSSL --sslPEMKeyFile /root/node1.pem --sslCAFile /root/ca.pem --clusterAuthMode x509
Node2
#!/bin/bash
mongod --bind_ip 127.0.0.1,172.31.5.30 --replSet rs0 --dbpath /data --logpath /data/mongod.log --port 27017 --fork --wiredTigerCacheSizeGB 1 --sslMode requireSSL --sslPEMKeyFile /root/node2.pem --sslCAFile /root/ca.pem --clusterAuthMode x509
Node3
#!/bin/bash
mongod --bind_ip 127.0.0.1,172.31.4.24 --replSet rs0 --dbpath /data --logpath /data/mongod.log --port 27017 --fork --wiredTigerCacheSizeGB 1 --sslMode requireSSL --sslPEMKeyFile /root/node3.pem --sslCAFile /root/ca.pem --clusterAuthMode x509
Connect with the following command
#!/bin/bash
mongo localhost --ssl --sslPEMKeyFile client.pem --sslCAFile ca.pem
And run initiate to create the replica set
> rs.initiate(
   {
      _id: "rs0",
      version: 1,
      members: [
         { _id: 0, host : "ip-172-31-7-201.ec2.internal:27017" },
         { _id: 1, host : "ip-172-31-5-30.ec2.internal:27017" },
         { _id: 2, host : "ip-172-31-4-24.ec2.internal:27017" }
      ]
   }
)

Create user on x509 authentication database

> db.getSiblingDB("$external").runCommand({createUser:"C=US,ST=OH,L=PAINESVILLE,O=BUSTEDWARE,OU=MONGO_CLIENTS,CN=JacobTBills",roles:[{role:"root",db:"admin"}]})
Connect using x509 authentication
#!/bin/bash
mongo ip-172-31-7-201.ec2.internal --ssl --sslPEMKeyFile client.pem --sslCAFile ca.pem --username "C=US,ST=OH,L=PAINESVILLE,O=BUSTEDWARE,OU=MONGO_CLIENTS,CN=JacobTBills" --authenticationMechanism "MONGODB-X509" --authenticationDatabase '$external'

VLOG