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.1openssl req -x509 -nodes -days 365 \
-newkey rsa:4096 \
-keyout key.pem \
-out cert.pem \
-config san.confgenerated 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