TLS Handshake (Conceptual)

When Node connects to:

https://192.168.1.50:3000

the handshake roughly becomes:

Client (Node)
   │
   │ 1. hello
   ▼
Server (Proxy)
   │
   │ 2. sends certificate
   ▼
Client verifies certificate
   │
   │ 3. exchange keys
   ▼
Encrypted channel established

The certificate is the key piece here.

What a Certificate Actually Is

A certificate is just a signed identity document.

Conceptually:

Certificate =
{
  identity: "192.168.1.50",
  public_key: XXXX,
  signed_by: CA
}

In your case:

identity → 192.168.1.50
signed_by → itself (self-signed)

The Key Pair You Generated

san.conf file

[req]
default_bits = 4096
prompt = no
default_md = sha256
distinguished_name = dn
x509_extensions = v3_req
 
[dn]
CN = 192.168.1.50 // the server IP we where hosting
 
[v3_req]
subjectAltName = @alt_names
 
[alt_names]
IP.1 = 192.168.1.50 // the server IP we where hosting
IP.2 = 127.0.0.1
openssl req -x509 -nodes -days 365 \
-newkey rsa:4096 \
-keyout key.pem \
-out cert.pem \
-config san.conf

generated two files:

key.pem
cert.pem

key.pem

private key

private_key = secret

Used to:

decrypt handshake secrets
sign TLS handshake
prove server identity

This must never leave the VM.

cert.pem

Your public certificate

public_key + identity + signature

Clients receive this during TLS handshake.

Why TLS Needs a CA (Trust Problem)

Imagine anyone could generate this certificate:

CN = google.com

Then impersonate Google.

To prevent that, the internet uses Certificate Authorities (CAs).

Normal chain:

Server cert
   │
Intermediate CA
   │
Root CA

And your computer trusts the Root CA.

Self-Signed Certificate

our certificate looks like:

Issuer  = CN=192.168.1.50
Subject = CN=192.168.1.50

Meaning:

server signed its own certificate

So Node sees:

Unknown CA

and rejects it.

What NODE_EXTRA_CA_CERTS Did

You added:

export NODE_EXTRA_CA_CERTS=/etc/ssl/certs/proxy-ca.pem

This tells Node:

Also trust this certificate as a root CA

So Node’s trust store becomes:

Default Root CAs
+ your proxy certificate

Now Node accepts the handshake.

Why CN Alone Didn’t Work

Older TLS used:

CN = hostname

Example:

CN = example.com

Modern TLS (RFC 6125) ignores CN for hostname verification.

Instead it uses:

Subject Alternative Name (SAN)

So validation is:

requested_host ∈ SAN ?

If not → reject.

That is why Node threw:

ERR_TLS_CERT_ALTNAME_INVALID

Why Version 3 Was Required

Certificates have versions:

v1 → no extensions
v2 → limited
v3 → extensions allowed

SAN is an extension.

So only:

Version: 3

can include:

X509v3 Subject Alternative Name

Your first certificate was:

Version: 1

So SAN was impossible.

Config File Explained

Your config probably looked like this:

[req]
distinguished_name=req_distinguished_name
x509_extensions=v3_req
prompt=no

[req]

Defines request settings.

x509_extensions=v3_req

tells OpenSSL:

apply v3 extensions when generating cert

[req_distinguished_name]

CN = 192.168.1.50

Defines identity fields.

[v3_req]

subjectAltName=@alt_names

Enables SAN extension.

[alt_names]

IP.1 = 192.168.1.50
IP.2 = 127.0.0.1

Defines allowed hosts.

Node checks this list during connection.

11. Final Working Certificate Structure

Your certificate now contains something like:

Certificate
  Version: 3
  Subject: CN=192.168.1.50

  Extensions:
     X509v3 Subject Alternative Name:
         IP:192.168.1.50
         IP:127.0.0.1

13. How Your Proxy Uses It

Your proxy server:

https.createServer({
   key: key.pem,
   cert: cert.pem
})

This means:

private key → prove identity
certificate → advertise identity