Dockerizing a MongoDB Replica Set With TLS/SSL
Tags: mongodb, docker, security, productivity
Published: 2022-11-07 16:50:06

## Introduction

The goal of this guide is to create a local SSL-enabled [MongoDB Replica Set](https://www.mongodb.com/docs/manual/replication/) using [docker-compose](https://docs.docker.com/compose/).

TL;DR: [Here](https://github.com/prestonvasquez/mongodb-rs-demo) is a demo repository.

### Prerequisites

[Docker](https://docs.docker.com/get-docker/) is the only pre-requisite, but if you want to connect to the mongo shell locally (recommended) you will need [MongoDB](https://www.mongodb.com/docs/manual/installation/).

## File Structure

Here is the file structure used in this tutorial:
```
.db/
config/
├─ mongodb.conf
ssl/
├─ ca.pem
├─ server.pem
docker-compose.yml
init.sh
run.sh
wait-for-mongo.sh
``` 

### .db/*

This is the [database path](https://www.mongodb.com/docs/manual/reference/configuration-options/#mongodb-setting-storage.dbPath) for the mongod configuration and should be generated automatically by docker-compose.

### ssl/*

The files `ssl/ca.pem` and `ssl/server.pem` are the [TLS CA file](https://www.mongodb.com/docs/manual/reference/program/mongos/#std-option-mongos.--tlsCAFile) and [TLS certificate key file](https://www.mongodb.com/docs/manual/reference/program/mongos/#std-option-mongos.--tlsCertificateKeyFile), respectively. The data in the ssl/ directory is used by the configuration file to initialize a MongoDB server with SSL enabled. This directory will be volumized on the Docker container for the primary replicant.

This tutorial assumes prior knowledge of TLS/SSL as well as access to valid certificates. For more information on generating certificates using `openssl`, see [this](https://rajanmaharjan.medium.com/secure-your-mongodb-connections-ssl-tls-92e2addb3c89) guide.

### config/mongodb.conf

This is the [configuration file](https://www.mongodb.com/docs/manual/reference/configuration-options/) used to configure the mongod instance at startup and will be be volumized on the Docker container for the primary replicant:

```yaml
net:
  ssl:
    mode: requireSSL
    PEMKeyFile: "/data/ssl/server.pem"
    CAFile: "/data/ssl/ca.pem"
storage:
  dbPath: "/data/db"
net:
  bindIp: 127.0.0.1
  port: 27017
```

### docker-compose.yml

In our docker-compose file, we create three nodes: `mongo1` (primary), `mongo2`, and `mongo3`. These replicant containers use the [bridge network](https://docs.docker.com/network/bridge/) `mongo_network`.

```yaml
version: '3'

networks:
  mongo_network:
    driver: bridge

services:
  mongo1:
    hostname: mongo1
    image: mongo
    ports:
      - 27017:27017
    restart: always
    entrypoint: [ "/usr/bin/mongod", "--config", "/data/config/ssl.conf", "--bind_ip_all", "--replSet", "dbrs" ]
    networks:
      - mongo_network
    volumes:
      - ./.db/mongo1:/data/db
      - ./wait-for-mongodb.sh:/scripts/wait-for-mongodb.sh
      - ./init.sh:/scripts/init.sh
      - ./ssl:/data/ssl
      - ./config:/data/config
    links:
      - mongo2
      - mongo3

  mongo2:
    hostname: mongo2
    image: mongo
    ports:
      - 27018:27017
    restart: always
    entrypoint: [ "/usr/bin/mongod", "--bind_ip_all", "--replSet", "dbrs" ]
    networks:
      - mongo_network
    volumes:
      - ./.db/mongo2:/data/db
      - ./wait-for-mongodb.sh:/scripts/wait-for-mongodb.sh

  mongo3:
    hostname: mongo3
    image: mongo
    ports:
      - 27019:27017
    restart: always
    entrypoint: [ "/usr/bin/mongod", "--bind_ip_all", "--replSet", "dbrs" ]
    networks:
      - mongo_network
    volumes:
      - ./.db/mongo3:/data/db
      - ./wait-for-mongodb.sh:/scripts/wait-for-mongodb.sh
```

Before composing, it is recommended to update the `/etc/hosts` file to include the replicant mappings:

```hosts
# Replica set mappings
127.0.0.1 mongo1
127.0.0.1 mongo2
127.0.0.1 mongo3
```

### init.sh

This shell file is used to initialize the replica set and will be ran in the docker container for the primary node (i.e. `mongo1`, defined in the "docker-compose.yml" section).

```sh
#!/bin/bash

mongosh --tls \
  --tlsCAFile /data/ssl/ca.pem \
  --tlsCertificateKeyFile /data/ssl/server.pem <<EOF
var config = {
    "_id": "dbrs",
    "version": 1,
    "members": [
        {
            "_id": 1,
            "host": "mongo1:27017",
            "priority": 3
        }
    ]
};
rs.initiate(config, { force: true });
rs.status();

var config = {
    "_id": "dbrs",
    "version": 1,
    "members": [
        {
            "_id": 2,
            "host": "mongo2:27017",
            "priority": 2
        },
        {
            "_id": 3,
            "host": "mongo3:27017",
            "priority": 1
        }
    ]
};
rs.initiate(config, { force: true });
rs.status();
EOF
```

### wait-for-mongo.sh

This script will wait for a mongo server to come up

```sh
#!/bin/bash

NAME=$1
OPTS=$2

while [ -z `mongosh --eval 'db.runCommand("ping").ok' --quiet --host $OPTS 2>/dev/null` ]; do
    echo "Waiting for MongoDB $NAME to start..."
    sleep 1
done

echo "MongoDB $NAME is up and running"
```

### run.sh

`run.sh` will (1) start the replicants, (2) wait for the replicants to come up, and (3) define primary and secondary replicants on `mongo1`.

```sh
#!/bin/bash

set -e

# remove volumes
rm -rf .db

# drop existing containers
docker compose -f "docker-compose.yml" down

# prune containers
docker system prune --force

docker-compose -f "docker-compose.yml" up -d \
	--remove-orphans \
	--force-recreate \
	--build mongo1

docker-compose -f "docker-compose.yml" up -d \
	--remove-orphans \
	--force-recreate \
	--build mongo2

docker-compose -f "docker-compose.yml" up -d \
	--remove-orphans \
	--force-recreate \
	--build mongo3

docker-compose -f "docker-compose.yml" exec -T mongo3 /scripts/wait-for-mongodb.sh "mongo3:27017"
docker-compose -f "docker-compose.yml" exec -T mongo2 /scripts/wait-for-mongodb.sh "mongo2:27017"

M1_OPTS="--tls --tlsCAFile /data/ssl/ca.pem --tlsCertificateKeyFile /data/ssl/server.pem --tlsAllowInvalidCertificates"
docker-compose -f "docker-compose.yml" exec -T mongo1 /scripts/wait-for-mongodb.sh "mongo1:27017" "$M1_OPTS"

echo "Creating replica set..."
docker-compose -f "docker-compose.yml" exec -T mongo1 /scripts/init.sh
```

Putting everything together, we run the above shell script and then test our setup by opening a mongo shell:

```
mongosh --tls --tlsCAFile <path-to-ca-file> --tlsCertificateKeyFile <path-to-client-file>
```