mTLS with NGINX and NodeJS

An example on two parties authenticating each other

Judith Freiberger
Geek Culture
6 min readJun 10, 2021

--

Usually identity verification is handled by SSL/TLS when communicating securely over the internet or another network. It only verifies the server’s identity. Clients are authenticated at a different level by the server, in most cases on the application layer.

Photo by Bernard Hermant on Unsplash

My goal in writing this article is to introduce you to the world of mutual authentication. This tutorial walks you through the steps of configuring two-way security using a NGINX server and a simple NodeJS application.
Therefore, I assume you have some familiarity with the above technologies as well as using Bash and Docker.

Before we dig in and start with configuring the solution, let me guide you through the basics of mTLS authentication.

Understand mTLS authentication

To enable mutual SSL authentication based on certificates, both parties need to accept the other’s authority by providing a valid certificate. A certification authority (CA) verifies these digital keys, from the server, and from the client.

This includes some overhead and is not applicable to common user applications. That’s why mTLS authentication is much more widespread in B2B applications, where security requirements are usually more strict and the number of clients connecting to specific services is limited.

The scenario below points out the request flow and describes the order of certification exchange between client and server.

mTLS authentication flow

The client requests a resource on the server side [1] which will be answered with the certificate of the server [2]. After receiving the certificate, the client verifies if it is valid [3]. If the validation has been successful, the client sends its certificate to the server [4]. The server on his side does also a verification of the incoming certificate [5]. When everything is fine and the client proved its identity, the resource is ready for request and information can be exchanged from both parties [6].

Please note that a server’s SSL certificate does not necessarily have to be signed by the same authority as the one from the client. Any authority for instance the most familiar one, LetsEncrypt could be used. However, both parties should be aware of the respective CA.

Generate Certificates

In order for certificates to be exchanged, they must be created and signed by an authority. A CA is created, which in this example is relying upon by both the server and client. The output is a key pair ca.key and ca.crt that can be used to sign the actors’ certificates.

Next, create the server key and certificate: thus creating a Certificate Signing Request (CSR) with the Common Name (CN) localhost (any other name can be used). The CSR is used along with the ca.key and ca.crt to create the signed certificate.
Every client who connects to the server has access to the server certificate; the private key on the other hand is a secure entity. It should be stored with restricted access. However, it has to be available to the NGINX later.

The process of creating the client’s key and certificate is self-explanatory, it is the same as the one for the server. Using the above steps, replace the CSR with the arbitrary Common Name of the client.

For each instance, run the following commands to display the contents of the certificate.

Set up the NGINX server

Once we have the proper certificates, let’s proceed with configuring the NGINX server.

nginx configuration

The server’s certificates location is the first one to be set in the NGINX configuration file. The location of the above created server certificate server.crt and the private key server.key is specified to the NGINX directives ssl_certificate and ssl_certificate_key.

ssl_client_certificate specifies the CA file that is used to verify the client’s certificate. Setting the directive ssl_verify_client activates the verification of clients. It is the optional parameter that requires every client to transmit its certificate, along with a check to see whether this certificate is present. The result is stored in the $ssl_client_verify variable.

Upon forwarding the incoming request to the application, the NGINX performs a SSL client verification and returns a 403 Forbidden if $ssl_client_verify value hasn’t been set successful. It might be caused, for example, by a missing client certificate.

For further details regarding the NGINX configuration handling ssl certificates, checkout the ssl_module documentation.

Implement the Node JS application

“Keep it simple and focus on what matters.” Confucius quote describes the difficulty of the example below best.

The NodeJS application contains an Express server and provides a GET operation replying with a message containing the certificate information of the client.
A single return value is sufficient since the NGINX server already filters out invalid requests.

basic node application

The client certificate information will be forwarded by the NGINX server with header variables. These parameters are required for the node application to be able to utilize the information.

Considering we will deliver the entire setup with Docker Compose, a Dockerfile is necessary for you to create together with the application. See here for an example I customized to my needs based on a simple NodeJS image.

Deploy the NGINX server and the NodeJS application

The solution is available with Docker for both local machines and cloud virtual machines. It is true that there are other environments to choose from, however OpenSSL and NodeJS are essentials that need to be installed upfront.

To finally spin up all components implemented before, we use Docker Compose for deploying the NGINX and the NodeJS application. Taking the following yaml file as an example.

docker-compose.yml

After taking into account all steps, the project is now ready to take off and available for testing.

Test the solution

To determine if the settings engage correctly and everything works smoothly, cURL can be used. Following is a command that tests the interface provided above, as the components are accessible locally.
ca.crt is needed to accept the CA of the server. client.crt and client.key has to be part of the request as they are proving the clients identity.

Keep going

I have uploaded the snippets I used in this tutorial to GitHub, where you can find the whole example with more features on my account. If you are interested, feel free to clone the repository and play around with it. Try out any extensions and modifications to better understand.

This article and example present a starting point for two-way authentication, which is an opportunity to gain more knowledge about the topic and build more complex solutions based on it.

Have fun and jump right in!

--

--