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.
Method 1: The OpenSSL One-Liner (Recommended)
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 theNODE_EXTRA_CA_CERTSenvironment 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
-installadds the CA to the Windows trust store, Node.js uses its own pre-compiled list of trusted CAs. You will still need to pointNODE_EXTRA_CA_CERTSto themkcertroot CA. You can find the path to this root CA by runningmkcert -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:
- Configure the Server: Update your Node app (or reverse proxy) to use the newly generated cert and key pair, then restart the service.
- Configure the Client: Set the
NODE_EXTRA_CA_CERTSenvironment variable on the client machine. Point it to yourserver-cert.pem(if using OpenSSL) or the root CA directory (if usingmkcert). - Test the Connection: Run a simple cURL or HTTP request test to confirm you receive a
200 OKstatus 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:
- bifrostservices:
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:
- bifrostdocker 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.pemservices:
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: