Using locally trusted certificates in Node.js

In most cases you do not need to set up your Node.js server to us HTTPS/SSL since it is very common to deploy your backend behind a reverse proxy like Nginx which serves certificates. If you are using Express.js you set trust proxy to true and all is fine.

When to use your own CA root certificate in Node.js?

If your Node app should work as a client in that it needs to call various backend services protected by HTTPS it could be complicated. By default, Node.js is built with a bundle of commonly used CA root certificates, but we still may encounter errors when your Node app makes an HTTPS API call to backend services hosted with self-signed certificates (i.e. company-specific private CA). The errors you can get originate from the SSL hand-shake process. When a client begins to establish a connection to server, a TLS handshake happens. It is a series of messages exchanged between the client and server, in which they agree with the version of the TLS and cipher suites to use, verify the identity of the server and generate the session keys. Most of the time, these errors are thrown because the client is not able to verify the server certificate because the server certificate is self-signed. The solution to this is to specify the CA certificate that is expected from the server. In other words, the common name of the certificate needs to match with the server certificate.

In Node.js you can do this by the environment variable NODE_EXTRA_CA_CERTS. From Node version 7.3.0, NODE_EXTRA_CA_CERTS environment variable is introduced to pass in a CA certificate file. This allows the “root” CAs to be extended with the extra certificates. The file should consist of one or more trusted certificates in PEM format.

I was also investigating an OICD Provider package in order to play with Open ID Connect and realized I have to run my backend using HTTPS/SSL.

Use your own CA root certificate in Node.js

At first, I thought I should skip this, but it is fun to refresh my memory and learn so here comes a description of my journey to set up att local development environment to use HTTPS/SSL on the backend, installing a local certificate authorities, CA, and updating the host file on my Windows 10 computer.

I googled and found a tool, mkcert, that seemed to be the perfect match. With mkcert, you do not need to use certificates from real certificate authorities (CAs) for development. Using certificates from real CAs can be dangerous or impossible for hosts like example.test, localhost or 127.0.0.1, and self-signed certificates can cause trust errors.

The solution can be to managing your own CA, but it usually involves arcane commands, specialized knowledge and manual steps. mkcert looked very easy to use and, I decided to have a go.

Use your own certificate authorities with mkcert

When you install mkcert the first time, it creates and installs a local CA in the root store, and generates locally trusted certificates. It does not automatically configure servers to use the certificates, that is up to you.

To find out where the CA root is located you can run the command;

mkcert --CAROOT

Warning: the rootCA-key.pem file that is automatically generated gives complete power to intercept secure requests from your machine. Do not share it.

It is also important to know that Node.js comes as mentioned earlier bundled with common used CA root certificates and thus does not use the system root store, so it won't accept mkcert certificates automatically. Instead, you will have to set the NODE_EXTRA_CA_CERTS environment variable.

With all this in mind, when you want to set up your local development computer to use *.example.com domain names and run your Node.js with HTTP/SSL, you can do this after installation of mkcert.

Edit your host file located in \Windows\System32\drivers\hosts, you need to map the ip address 127.0.0.1 to example.com domain name.

Run mkcert to create a certificate and a key for example.com.

mkcert example.com

Now you will have a certificate, and a key file generated to be used in your Node.js server listening on the example.com domain. The certificate is at "./example.com.pem" and, the key at "./example.com-key.pem"

Just a short snippet on the app code using express.js.

import { strict as assert } from 'assert';
import * as fs from 'fs'
import * as https from 'https'
import express from 'express'
import bodyParser from 'body-parser'
import morgan from 'morgan'
import cors from 'cors'
import helmet from 'helmet'
import compression from 'compression'

assert(process.env.SERVER_KEY_FILE, 'process.env.SERVER_KEY_FILE missing')
assert(process.env.SERVER_CERT_FILE, 'process.env.SERVER_CERT_FILE missing')
const key = fs.readFileSync(process.env.SERVER_KEY_FILE, 'utf-8')
const cert = fs.readFileSync(process.env.SERVER_CERT_FILE, 'utf-8')
const { port, appName } = appConfig
const HOST = `${appName}:${port}`

const app = express()
app.use(bodyParser.json())
app.use(bodyParser.urlencoded({ extended: true }))
app.use(cors("*"))
app.use(compression())
app.use(helmet())
app.use(morgan('combined'))

// Routes etc

https.createServer({ key, cert }, app).listen(appConfig.port, () => {
  console.log(`Listening on: ${HOST}`)
})

Test the certificate by using Chrome and access the server. if everything has gone well, you access it via HTTPS/SSL and, the connection is secure.

References


Published: 2021-04-01
Author: Henrik Grönvall
Henrik Grönvall
Copyright © 2022 Henrik Grönvall Consulting AB