mTLS with NGINX and NodeJS
An example on two parties authenticating each other
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.
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.
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.
openssl req \
-newkey rsa:4096 \
-x509 \
-keyout ca.key \
-out ca.crt \
-days 30 \
-nodes \
-subj "/CN=my_ca"
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.
openssl req \
-newkey rsa:4096 \
-keyout server.key \
-out server.csr \
-nodes \
-days 30 \
-subj "/CN=localhost"openssl x509 \
-req \
-in server.csr \
-out server.crt \
-CA ca.crt \
-CAkey ca.key \
-CAcreateserial \
-days 30
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.
openssl req \
-newkey rsa:4096 \
-keyout client.key \
-out client.csr \
-nodes \
-days 30 \
-subj "/CN=client"openssl x509 \
-req \
-in client.csr \
-out client.crt \
-CA ca.crt \
-CAkey ca.key \
-CAcreateserial \
-days 30
For each instance, run the following commands to display the contents of the certificate.
### display the contents of ca.crt
openssl x509 -in ca.crt -text -noout### output
Certificate:
Data:
Version: 1 (0x0)
Serial Number: 9377707732938553850 (0x82244f7f753c79fa)
Signature Algorithm: sha256WithRSAEncryption
Issuer: CN=CA_NAME
Validity
Not Before: Feb 27 14:56:53 2021 GMT
Not After : Feb 27 14:56:53 2022 GMT
Subject: CN=CA_NAME
Subject Public Key Info:
Public Key Algorithm: rsaEncryption
Public-Key: (4096 bit)
Modulus:
00:f8:5a:b9:c9:99:82:5b:45:d2:1e:9f:05:6e:60:
....
Exponent: 65537 (0x10001)
Signature Algorithm: sha256WithRSAEncryption
42:7c:d8:93:e9:01:f0:c9:21:db:dc:94:68:77:92:a6:3e:6f:
...### display the contents of server.crt
openssl x509 -in server.crt -text -noout...### display the contents of client.crt
openssl x509 -in client.crt -text -noout...
Set up the NGINX server
Once we have the proper certificates, let’s proceed with configuring the NGINX server.
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_certificate /etc/ssl/server.crt;
ssl_certificate_key /etc/ssl/server.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.
ssl_client_certificate /etc/nginx/client_certs/ca.crt; ssl_verify_client optional;
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.
if ($ssl_client_verify != SUCCESS) { return 403; }
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.
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.
proxy_set_header SSL_Client_Issuer $ssl_client_i_dn; proxy_set_header SSL_Client $ssl_client_s_dn; proxy_set_header SSL_Client_Verify $ssl_client_verify;
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.
After taking into account all steps, the project is now ready to take off and available for testing.
docker-compose build
docker-compose up
### Alternatively combine both commands into one:docker-compose up --build
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.
curl https://localhost \
--cacert ca.crt \
--key client.key \
--cert client.crt### successful response with correct parameters should be returned
Hello client, your certificate was issued by my_ca!
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!