NodeJS | How to Generate a Self-Signed PEM Certificate with SAN Configuration

If you are setting up a local development server or internal service, you will inevitably need to generate a self-signed certificate. However, most tutorials skip a critical step: the Subject Alternative Name (SAN) configuration.

Without a proper SAN, modern environments and runtimes—especially Node.js—will flat-out reject your certificate, even if you have explicitly trusted it in your system. Here is the complete guide to generating a self-signed PEM certificate with a fully configured SAN, using either OpenSSL or mkcert.


Using OpenSSL is the most robust method because it doesn’t require installing third-party package managers. You can generate the certificate and the private key in a single command, injecting the SAN directly via the -addext flag.

Run the following command (formatted for PowerShell):

PowerShell

openssl req -x509 -newkey rsa:2048 -nodes -days 365 `
  -keyout C:\certs\server-key.pem `
  -out C:\certs\server-cert.pem `
  -subj "/CN=localhost" `
  -addext "subjectAltName=DNS:localhost,IP:127.0.0.1"

What This Generates:

  • server-cert.pem: The actual certificate. You will point your server’s TLS configuration to this file. On the client side (e.g., in Node.js), you will set the NODE_EXTRA_CA_CERTS environment variable to point to this exact file.
  • server-key.pem: The private key. This is strictly for your server configuration and should never be shared with or accessed by the client.

Expanding the SAN for LAN Access

If you plan to access your server via a machine name or a local network IP address, you must add those specific identifiers to the SAN. Simply modify the -addext parameter:

PowerShell

-addext "subjectAltName=DNS:localhost,DNS:mypc.local,IP:127.0.0.1,IP:192.168.1.50"

Method 2: The mkcert Approach (Easier)

If you don’t want to memorize OpenSSL flags, mkcert is a fantastic alternative that automatically handles SANs and local trust stores.

If you are on Windows, you can install and run it via winget:

PowerShell

# Install mkcert
winget install FiloSottile.mkcert

# Install the local CA in your system trust store
mkcert -install

# Generate the certificate for localhost and local IP
mkcert localhost 127.0.0.1

This will output two files: localhost+1.pem and localhost+1-key.pem, both pre-configured with the correct SANs.

Note for Node.js users: While -install adds the CA to the Windows trust store, Node.js uses its own pre-compiled list of trusted CAs. You will still need to point NODE_EXTRA_CA_CERTS to the mkcert root CA. You can find the path to this root CA by running mkcert -CAROOT.

How to Configure Node.js with Your New PEM Certificate

Once you have your key and certificate pair, follow these steps to integrate them into your Node.js application:

  1. Configure the Server: Update your Node app (or reverse proxy) to use the newly generated cert and key pair, then restart the service.
  2. Configure the Client: Set the NODE_EXTRA_CA_CERTS environment variable on the client machine. Point it to your server-cert.pem (if using OpenSSL) or the root CA directory (if using mkcert).
  3. Test the Connection: Run a simple cURL or HTTP request test to confirm you receive a 200 OK status before launching your full application suite.

Troubleshooting Tip

If your tests are already returning a 200 OK, you only need to regenerate the certificate if your current SAN doesn’t match the hostname you are actively using. Otherwise, your existing PEM is perfectly fine, and any connection errors are likely due to your client application failing to inherit the NODE_EXTRA_CA_CERTS environment variable.

services:
  bifrost:
    image: maximhq/bifrost
    volumes:
      - ./data:/app/data

  nginx:
    image: nginx:alpine
    ports:
      - "8443:443"
    volumes:
      - ./nginx.conf:/etc/nginx/conf.d/default.conf:ro
      - C:/certs:/etc/nginx/certs:ro
    depends_on:
      - bifrost
services:
  bifrost:
    image: maximhq/bifrost
    volumes:
      - ./data:/app/data

  nginx:
    image: nginx:alpine
    ports:
      - "8443:443"
    volumes:
      - ./nginx.conf:/etc/nginx/conf.d/default.conf:ro
      - C:/certs:/etc/nginx/certs:ro
    depends_on:
      - bifrost
docker run -d --name caddy --network host \
  -v /etc/bifrost/certs:/certs:ro caddy:alpine \
  caddy reverse-proxy --from https://0.0.0.0:8443 --to 127.0.0.1:8080 \
  --tls /certs/bifrost-cert.pem --tls-key /certs/bifrost-key.pem
services:
  bifrost:
    image: maximhq/bifrost
    volumes:
      - ./data:/app/data
    # no ports: — not exposed to the outside, only Caddy can reach it

  caddy:
    image: caddy:alpine
    ports:
      - "8443:8443"
    volumes:
      - ./Caddyfile:/etc/caddy/Caddyfile:ro
      - ./certs:/certs:ro
      - caddy_data:/data
    depends_on:
      - bifrost

volumes:
  caddy_data:
Scroll to Top