HTTPS and TLS Certificates

node:https is node:http over a tls.TLSSocket: the streams and every timeout from Keep-Alive and Timeouts are identical. What changes is that createServer needs a key and a certificate carrying a subjectAltName — a bare common name has not been accepted for years.

An HTTPS server that reports the negotiated connectionJavaScript
import { createServer } from 'node:https';
import { readFileSync } from 'node:fs';
const opts = { key: readFileSync('key.pem'), cert: readFileSync('cert.pem'),
               minVersion: 'TLSv1.3' };
createServer(opts, (req, res) =>
  res.end(`${req.socket.getProtocol()} ${req.socket.getCipher().standardName}\n`))
  .on('secureConnection', (s) => console.log('sni:', s.servername))
  .listen(8443, '127.0.0.1');
Output
$ openssl req -x509 -newkey rsa:2048 -nodes -keyout key.pem -out cert.pem -days 365 \
    -subj "/CN=localhost" -addext "subjectAltName=DNS:localhost,IP:127.0.0.1"
$ curl --cacert cert.pem https://localhost:8443/
TLSv1.3 TLS_AES_256_GCM_SHA384
sni: localhost

Without --cacert, curl 3,008 refuses: the certificate is signed by nobody the client trusts. The development fix is mkcert 59,701 (https://github.com/FiloSottile/mkcert 59,701 ), a local certificate authority; the production fix is Let's Encrypt 1,144 . Never NODE_TLS_REJECT_UNAUTHORIZED=0, which disables verification process-wide — pass ca to the one client that needs it.

servername is the SNI hostname the client sent before any HTTP header existed, which is how one socket serves many certificates: supply SNICallback. A load balancer usually terminates TLS in production (Running Behind a Reverse Proxy); you still need this locally for Secure cookies and mutual TLS (requestCert: true).