with Spring Boot and NodeJS examples
By Michael van Niekerk
Why
What to expect in this document
Small notes
What you will need
Setting up the keys and certificates
Making your own certificate authority (CA)
A note on proper CA and key usage
Private key
Self-signed root certificate
Server key and certificate
Private key
CA signed certificate
TLS PEM key
Client private key and certificate
Private key
CA signed certificate
Java Keystore
PKCS#12 Keystore
Convert to a Java Keystore
Java Truststore
TLS PEM Key
TLS-enabled MongoDB in Docker
Spring Boot application
Initial setup
Java files
sb/src/main/java/com/example/demo/DbEntry.java
sb/src/main/java/com/example/demo/DbEntryDao.java
sb/src/main/java/com/example/demo/DbEntryController.java
Running the Spring Boot application
Does it work?
Payload / NodeJS example
Files to be edited / added
payload/.env
payload/src/payload.config.ts
Running Payload
Does it work?
Closing remarks
When your application connects to a MongoDB instance, it needs to authenticate itself using some sort of authentication method.
As standard, using a username and password, the credentials are sent over the wire in plain sight. That means, your credentials can be intercepted (a man-in-the-middle attack).
A step further - there is no way for you to know that the MongoDB server you are sending your credentials to is in fact the MongoDB you should be sending it to. Conversely, the MongoDB cannot verify that the application connecting to it should be connected to it (that it is not someone that stole the credentials).
You are thus sitting with a plain text sent over the wire problem, with no client and server verification.
TLS helps with this.
First off, we will be encrypting the communications between the client and the MongoDB server. That takes care of the plain text over the wire problem.
For verifying the server, the client application needs to trust the certificate sent by the MongoDB instance. Conversely, the server can require that the client application also sends a certificate that is to be trusted by the server.
- Setting up a certificate authority (CA) to self-sign your server and client certificates.
- Setting up the server private key and creating a signed certificate using your CA.
- Setting up the client's private key and creating a signed certificate.
- For the Spring Boot example application, we will be creating a Java Keystore / Truststore combination.
- For the NodeJS example application, we will be creating a PEM file.
- Start up the MongoDB instance with TLS enabled.
- A sample Spring Boot application, using Spring Boot SSLBundles to configure the keys.
- A sample NodeJS application using Payload CMS.
I have used the same password everywhere (“Eendje0!”) when confronted with a prompt to give a password. It is up to the reader to differentiate when to use which password. It is never good practice to use the same password everywhere.
For files, every file noted will have the full path as seen from the project folder’s root.
Commands in the terminal are marked as $ here be command
, that means you will need to type everything after the $.
- Docker and Docker Compose
- OpenSSL
- curl
- A 17+ version of the JDK (Java Development Kit, used in the Spring Boot example) installed.
- NodeJS and NPM (used in the NodeJS/Payload example)
- Git, if you are downloading the Payload blank template
- A POSIX compliant terminal
Create a project folder for yourself. For a folder structure for the keys and certificates we will be creating CA, server, client and JKS sub-folders.
$ mkdir -p ca server client jks
For security, you should not have your root CA be “live”. That is, it should not be installed or ran anywhere on your production environment. It should be “offline”. It is better that you have a subordinate CA certificate signed by the root CA and have that installed into your production environment. This guide is a how-to, but for more information have a look at this link from the OpenSSL cookbook on creating a private CA. From the same OpenSSL cookbook, there’s also some more reading you can do on keys and certificate management.
Start by creating a private key that will be used to sign the certificates:
$ cd ca
$ openssl genpkey -algorithm RSA -out ca.key -aes256
The above will prompt you to enter a passphrase for the key – write it down as we are going to use it later. After this it will make a new file ca/ca.key. For further reading, go to the genpkey documentation
$ openssl req -x509 -new -nodes -key ca.key -sha256 -days 3650 -out ca.crt
The –days 3650 part makes the root certificate valid for 3650 days (about 10 years). You can omit this but then it will not expire (which is not considered good practice).
The command will prompt you to add more information about the root certificate. When you are finished with the command it will create a new file in ca/ca.crt.
Generate a private key in the server folder
$ cd ../server
$ openssl genpkey -algorithm RSA -out server.key -aes256
Like with the CA we will be prompted for a passphrase. Write this down for later use. This creates a file server/server.key.
Create a certificate signing request (CSR)
$ openssl req -new -key server.key -out server.csr
The above creates a file server/server.csr
Next up is to create an extensions configuration file, that includes the specification for the hostname(s) from which this certificate file is to be used with the MongoDB. Let’s call it ext.cfg and place it in the server folder. Here is what the contents should be:
authorityKeyIdentifier=keyid,issuer
basicConstraints=CA:FALSE
keyUsage = digitalSignature, nonRepudiation, keyEncipherment, dataEncipherment
subjectAltName = @alt_names
[alt_names]
DNS.1 = mongodb1.example.com
DNS.2 = mongodb2.example.com
DNS.3 = localhost
The entries DNS.1, DNS.2, DNS.3 are important when you are to verify the hostname of the server you are connecting to as a client application. On the client application side, you can optionally remove hostname verification. However, this is not considered good practice. At least here you can see where to specify it.
$ openssl x509 -req -in server.csr -CA ../ca/ca.crt -CAkey ../ca/ca.key -CAcreateserial -out server.crt -days 365 -sha256 -extfile ext.cfg
The above will prompt you for the CA’s private key password. It will produce a file server/server.crt.
We will be using this PEM key in the MongoDB instance to present the application clients a certificate for verification.
$ cat server.crt server.key > server.pem
Generate a private key in the client folder
$ cd ../client
$ openssl genpkey -algorithm RSA -out client.key -aes256
Like with the CA we will be prompted for a passphrase. Write it down, we will be using it in later steps. This creates a file client/client.key.
Create a certificate signing request (CSR)
$ openssl req -new -key client.key -out client.csr
The above creates a file client/client.csr
$ openssl x509 -req -in client.csr -CA ../ca/ca.crt -CAkey ../ca/ca.key -CAcreateserial -out client.crt -days 365 -sha256
The above will prompt you for the CA’s private key password. It will produce a file client/client.crt.
These steps are needed for the Spring Boot application. This file is used by Spring Boot and its SSLBundles sub-library to present a certificate to the MongoDB to verify the client application. You can follow this link for more reading on Java keystores.
$ cd ../jks
$ openssl pkcs12 -export -in ../client/client.crt -inkey ../client/client.key -out keystore.p12 -name client-alias -CAfile ../ca/ca.crt -caname root-ca -chain
This will create a keystore file jks/keystore.p12 that includes the private key, its signed certifcate, and the chain of trust CA certificate file into one keystore. You will be prompted for a password, write it down (you will be using it in the next steps)
$ keytool -importkeystore -srckeystore keystore.p12 -srcstoretype pkcs12 -destkeystore keystore.jks -alias client-alias
The above command will create a jks/keystore.jks Java keystore file. It is going to prompt you for a password, write it down as you will be using it for later.
This step is needed for the Spring Boot application to trust the self-signed MongoDB server. You are just adding the CA certificate to a separate keystore. What is important in all this setup is that it uses the same alias as the Java Keystore’s alias. In the example it is “client-alias”.
$ keytool -import -alias client-alias -file ../ca/ca.crt -keystore truststore.jks
It is going to prompt you for a password. Write it down, we are going to use it later. This will create a file jks/truststore.jks
This step is used on the Payload / NodeJS example. In short, we are concatenating the certificate with the encrypted private key.
$ cd ../client
$ cat client.crt client.key > client.pem
In the root folder, create a mongo folder
$ cd ../
$ mkdir mongo
In the mongo folder, add a file etc-mongod.conf, with the following contents (mongo/etc-mongod.conf):
net:
ssl:
PEMKeyPassword: Eendje0!
tls:
mode: requireTLS
certificateKeyFile: /etc/ssl/certs/mongodb.pem
CAFile: /etc/ssl/certs/mongo_ca.crt
Here you will see we are giving extra directives to MongoDB. mode: requireTLS forces all connections to be always using TLS. certificateKeyFile is the file presented to clients to verify the MongoDB server. PEMKeyPassword is the PEM’s password (when you’ve created the server private key). CAFile is used by the MongoDB to verify the client’s certificate.
Also add a file mongo/mongo-init.js. This is not really needed in the TLS scheme of things, but it ensures that there is an application database user created on the MongoDB’s startup. Here are the contents:
db.auth('admin', 'password')
payloadcmsdb = db.getSiblingDB('payloadcmsdb')
payloadcmsdb.createUser({
user: 'payloadcmsdb',
pwd: 'polo12345',
roles: [
{
role: 'readWrite',
db: 'payloadcmsdb',
},
],
});
In the root folder, create a docker-compose.yaml file:
version: "3"
services:
mongodb:
image: mongo
environment:
- PUID=${UID:-1000}
- PGID=${GID:-1000}
- MONGO_INITDB_ROOT_USERNAME=admin
- MONGO_INITDB_ROOT_PASSWORD=password
- MONGO_INITDB_DATABASE=admin
command:
- --config
- /etc/mongod.conf
ports:
- "27017-27019:27017-27019"
volumes:
- ./mongo/mongo-init.js:/docker-entrypoint-initdb.d/mongo-init.js
- ./mongo/etc-mongod.conf:/etc/mongod.conf
- ./server/server.pem:/etc/ssl/certs/mongodb.pem
- ./ca/ca.crt:/etc/ssl/certs/mongo_ca.crt
You should be set up for a TLS enabled MongoDB. Start it up with:
$ docker compose up
After a few seconds you should see something like this:
{"t":{"$date":"2024-09-03T12:21:37.839+00:00"},"s":"I", "c":"NETWORK", "id":23016, "ctx":"listener","msg":"Waiting for connections","attr":{"port":27017,"ssl":"on"}}
That means that the service is waiting on TLS/SSL enabled connections.
Head over to https://start.springboot.io . Select the options you want, just make sure that MongoDB and Spring Web are one of the options you select.
Generate the project – it will download a Zip file with your Spring Boot application in it.
When you extract it, at its root it will have only one folder (named the same as the project name you have given at the Spring Boot start website).
Move this folder to your certificate project folder and rename it sb.
First remove the example application.properties file
$ cd sb
$ rm src/main/resources/application.properties
Then create a new sb/src/main/resources/application.yaml file:
spring:
ssl:
bundle:
jks:
MONGO:
key:
alias: "client-alias"
keystore:
location: ${KEYSTORE_PATH}
password: ${KEYSTORE_PASS}
type: "PKCS12"
truststore:
location: ${TRUSTSTORE_PATH}
password: ${TRUSTSTORE_PASS}
type: "PKCS12"
data:
mongodb:
ssl:
enabled: true
bundle: MONGO
uri: mongodb://payloadcmsdb:polo12345@localhost:27017/payloadcmsdb?tls=true&tlsAllowInvalidHostnames=true
A few interesting notes:
- KEYSTORE_PASS, KEYSTORES_PASS, TRUSTSTORE_PATH, and TRUSTSTORE_PASS, will be looking for their environmental variables upon startup.
- The key alias is re-used in both keystore and truststore (as in, their entries should match in these two keystores).
- MongoDB has SSL as enabled with its SSLBundle pointing to spring.ssl.bundle.jks.MONGO => MONGO
- tlsAllowInvalidHostnames =true ignores the hostname vs certificate provided by the server. For production environments this should be avoided.
A simple record to save a document in MongoDB that just contains the time.
package com.example.demo;
import org.springframework.data.mongodb.core.mapping.Document;
@Document("db_entry")
public record DbEntry(String time) { }
The Data Access Object (DAO) is a repository that saves and lists DbEntry objects
package com.example.demo;
import org.springframework.data.mongodb.repository.MongoRepository;
import org.springframework.transaction.annotation.Transactional;
@Transactional
public interface DbEntryDao extends MongoRepository<DbEntry, String> { }
This is a Rest controller. Everytime you do a GET on its endpoint, it adds a new DbEntry into the database, and returns all the other saved DBEntry objects as a list.
package com.example.demo;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.util.List;
@RestController
public class DbEntryController {
@Autowired DbEntryDao dao;
@GetMapping
public List<DbEntry> getEntries() {
var now = LocalDateTime.now();
var isoDateTime = now.format(DateTimeFormatter.ISO_DATE_TIME);
var newEntry = new DbEntry(isoDateTime);
dao.save(newEntry);
return dao.findAll();
}
}
To run the application, go to project root folder and export the paths of the keystore and the truststore.
$ cd ../
$ export KEYSTORE_PATH=$(pwd)/jks/keystore.jks
$ export TRUSTSTORE_PATH=$(pwd)/jks/truststore.jks
Also, export your keystore and truststore passwords
$ export KEYSTORE_PASS='Eendje0!'
$ export TRUSTSTORE_PASS='Eendje0!'
Then, go to the Spring Boot app root and start it up
$ cd sb
$ ./mvnw spring-boot:run
.
Rename 'Eendje0!' with your chosen passwords for the keystore and truststore.
This will start up the Spring Boot application and binds its port to 8080.
$ curl localhost:8080/
This returns [{"time":"2024-09-03T16:07:11.091044"}]. Running the curl command a few extra times will result in more entries being returned.
Payload CMS is built upon ExpressJS. On default it has a MongoDB specific data provider (@payloadcms/db-mongodb, other options are available like Postgres), and that uses the Mongoose library. The Mongoose library, in turn, uses the standard MongoDB JS client. Thus, for other frameworks and platforms on NodeJs, the configuration will be similar.
Let us start with the blank template hosted by Payload and install its dependencies.
$ git clone [email protected]:payloadcms/payload.git payloadgit
$ mv payloadgit/templates/blank payload
$ rm -rf payloadgit
$ cd payload
$ yarn
The environment file.
DATABASE_URI=mongodb://payloadcmsdb:polo12345@localhost:27017/payloadcmsdb?tls=true&tlsAllowInvalidHostnames=true
PAYLOAD_SECRET=YOUR_SECRET_HERE
import _path_ from 'path'
import { payloadCloud } from '@payloadcms/plugin-cloud'
import { mongooseAdapter } from '@payloadcms/db-mongodb' // database-adapter-import
import { webpackBundler } from '@payloadcms/bundler-webpack' // bundler-import
import { slateEditor } from '@payloadcms/richtext-slate' // editor-import
import { buildConfig } from 'payload/config'
import Users from './collections/Users'
export default buildConfig({
admin: {
user: Users.slug,
bundler: webpackBundler(), // bundler-config
},
editor: slateEditor({}), // editor-config
collections: [Users],
typescript: {
outputFile: path.resolve(__dirname, 'payload-types.ts'),
},
graphQL: {
schemaOutputFile: path.resolve(__dirname, 'generated-schema.graphql'),
},
plugins: [payloadCloud()],
// database-adapter-config-start
db: mongooseAdapter({
url: _process_.env.DATABASE_URI,
connectOptions: {
tls: process.env.TLS_ENABLED === 'true',
tlsCertificateKeyFile: process.env.TLS_CERTIFICATE_KEY_FILE,
tlsCertificateKeyFilePassword: process.env.TLS_CERTIFICATE_KEY_FILE_PASSWORD,
tlsCAFile: process.env.TLS_CA_FILE,
tlsAllowInvalidHostnames: process.env.TLS_ALLOW_INVALID_HOSTNAMES === 'true',
}
}),
// database-adapter-config-end
})
What is extra from the default blank template is this:
connectOptions: {
tls: process.env.TLS_ENABLED === 'true',
tlsCertificateKeyFile: process.env.TLS_CERTIFICATE_KEY_FILE,
tlsCertificateKeyFilePassword: process.env.TLS_CERTIFICATE_KEY_FILE_PASSWORD,
tlsCAFile: process.env.TLS_CA_FILE,
tlsAllowInvalidHostnames: process.env.TLS_ALLOW_INVALID_HOSTNAMES === 'true',
}
Here you can see that the service will look for environment variables (like the Spring Boot application) to set up its TLS connectivity. This object is from the MongoDB JS client library.
That means that these process.env.* entries should be in the environment variable list, or in the .env file.
To run the application, go to project root folder and export the paths of the client TLS PEM key and the CA certificate:
$ cd ../
$ export TLS_CERTIFICATE_KEY_FILE=$(pwd)/client/client.pem
$ export TLS_CA_FILE=$(pwd)/ca/ca.crt
Also, export the client password, allowing of invalid hostnames, and whether TLS should be enabled:
$ export TLS_ENABLED=true
$ export TLS_CERTIFICATE_KEY_FILE_PASSWORD='Eendje0!'
$ export TLS_ALLOW_INVALID_HOSTNAMES=true
Note that these variables could have been persisted in your .env file.
Lastly, to run it:
$ cd payload
$ yarn dev
After a while, you will see: webpack compiled successfully
Go to http://localhost:3000 . You will be greeted by a startup page. Create your own details to create the admin user, then add another user. You will see that the data persists.
Although this is not in-depth, this is sufficient for anyone to get TLS connectivity set up for their own MongoDBs. Also, this would be illustrative as to what is needed to get TLS client connectivity running from commercial MongoDB providers.