Securing Node.js Communication: A Deep Dive into Mutual TLS (mTLS)

PART 02 - Let's Build & Feel It

Welcome back, tech enthusiasts! In the first part, we covered the basics of mTLS in simple terms. Now, let's take a deep dive into the mTLS realm with Node.js. Ready to explore the depths with me? Let's explore mTLS together in this exciting journey!

Recap: Mutual Transport Layer Security (mTLS) is a security protocol that allows two parties to securely communicate with each other over the internet. It provides a way for the client and server to authenticate each other and encrypt their communication, ensuring that only authorized parties can access the data being transmitted.

In mTLS, both the client and server must present a valid certificate to prove their identity. The client verifies the server's certificate, and the server verifies the client's certificate. If either party cannot present a valid certificate, the connection is terminated. This helps prevent man-in-the-middle attacks, where a third party intercepts and alters the communication between the client and server.

Prerequisites

Before we start, ensure you have the following prerequisites:

  1. Node.js: Make sure you have Node.js installed on your system. You can download it from the official Node.js website.

  2. OpenSSL: Install OpenSSL, a robust, full-featured open-source toolkit that implements the Secure Sockets Layer (SSL) and Transport Layer Security (TLS) protocols. You can download it from the official OpenSSL website.

Step 1: Generating Certificates with OpenSSL

To set up mTLS, we need certificates for both the server and client. Let's start by generating the necessary certificates.

1.1 Create a Certificate Authority (CA)

In a real-world scenario, you'd typically obtain certificates from a trusted certificate authority. However, for testing purposes, we'll create our own self-signed CA.

# Create a private key for the CA
openssl genrsa -out ca-key.pem 2048

# Create a self-signed certificate for the CA
openssl req -x509 -new -nodes -key ca-key.pem -sha256 -days 365 -out ca.pem

1.2 Generate Server and Client Certificates

Next, we'll generate certificates for the server and client.

Server Certificate

# Create a private key for the server
openssl genrsa -out server-key.pem 2048

# Create a certificate signing request (CSR) for the server
openssl req -new -key server-key.pem -out server-csr.pem

# Sign the CSR with the CA to generate the server certificate
openssl x509 -req -in server-csr.pem -CA ca.pem -CAkey ca-key.pem -CAcreateserial -out server-cert.pem -days 365 -sha256

Client Certificate

# Create a private key for the client
openssl genrsa -out client-key.pem 2048

# Create a certificate signing request (CSR) for the client
openssl req -new -key client-key.pem -out client-csr.pem

# Sign the CSR with the CA to generate the client certificate
openssl x509 -req -in client-csr.pem -CA ca.pem -CAkey ca-key.pem -CAcreateserial -out client-cert.pem -days 365 -sha256

Now that we have our certificates, let's proceed to implement the server and client in Node.js.

These are the files that were generated after executing the above commands.

Step 2: Node.js Server with mTLS

First, let's set up the Node.js server to use mTLS.

const fs = require("fs");
const https = require("https");

const key = fs.readFileSync(`${process.cwd()}/files/server/server-key.pem`);
const cert = fs.readFileSync(`${process.cwd()}/files/server/server-cert.pem`);
const ca = [fs.readFileSync(`${process.cwd()}/files/ca/ca.pem`)];

const options = {
  key,
  cert,
  ca,
  // Requesting the client to provide a certificate, to authenticate.
  requestCert: true,
  // It this set as `true`, server will not accept any unauthenticated traffic
  // Based on the use case this will be route specific.
  rejectUnauthorized: true
};

https
  .createServer(options, function (req, res) {
    console.log(`${new Date()} ${req.socket.remoteAddress} ${req.method} ${req.url}`);
    res.writeHead(200);
    res.end("Success!");
  }).listen(3002);

Step 3: Node.js Client with mTLS

Next, let's set up the Node.js client to use mTLS.

const fs = require('fs');
const https = require('https');

const message = { msg: 'Hi Server!' };

const req = https.request( {
    host: 'server.mtls-tester.com',
    port: 3002,
    secureProtocol: 'TLSv1_2_method',
    key: fs.readFileSync(`${process.cwd()}/files/client/client-key.pem`),
    cert: fs.readFileSync(`${process.cwd()}/files/client/client-cert.pem`),
    ca: [
      fs.readFileSync(`${process.cwd()}/files/ca/ca.pem`)
    ],
    path: '/',
    method: 'GET',
    headers: {
      'Content-Type': 'application/json',
      'Content-Length': Buffer.byteLength(JSON.stringify(message))
    }
  }, (response) => {
    console.log('Response statusCode: ', response.statusCode);
    console.log('Response headers: ', response.headers);
    console.log(`Server Host Name: ${ response.socket.getPeerCertificate().subject.CN }`);

    if (response.statusCode !== 200) {
      console.log(`Wrong status code`);
      return;
    }

    let rawData = '';
    response.on('data', function (data) {
      rawData += data;
    });

    response.on('end', function () {
      if (rawData.length > 0) {
        console.log(`Received message: ${rawData}`);
      }
      console.log(`TLS Connection closed!`);
      req.end();
      return;
    });
  }
);

req.on('socket', (socket) => {
  socket.on('secureConnect', () => {
    if (socket.authorized === false) {
      console.log(`SOCKET AUTH FAILED ${socket.authorizationError}`);
    }
    console.log('TLS Connection established successfully!');
  });
  socket.setTimeout(10000);
  socket.on('timeout', () => {
    console.log('TLS Socket Timeout!');
    req.end();
    return;
  });
});

req.on('error', (err) => {
  console.log(`TLS Socket ERROR (${err})`);
  req.end();
  return;
});

req.write(JSON.stringify(message));

Step 4: Testing the mTLS Flow

Let's run the server and client to test the mTLS communication.

  1. Start the server:

     node server.js
    
  2. Start the client (in a separate terminal):

     node client.js
    

You should see the client successfully communicating with the server over a secure mTLS connection.

For the sample code to run correctly, please add the test domain names to your /etc/hosts file. This will prevent connection errors.

Congratulations! You've successfully set up a Node.js server and client using mTLS. Explore further and integrate mTLS into your applications to ensure secure and authenticated communication.

For full code example please visit following GitHub Repository.

Stay tuned for more insights into secure coding and implementation practices. Happy coding! ๐Ÿš€

ย