HTTPS Module - Wrapping HTTP in TLS¶
Table of Contents¶
- The TLS Layer
- https.createServer with Certificates
- Generating Self-Signed Certs with OpenSSL
- HTTP to HTTPS Redirect Pattern
- TLS Versions and Cipher Configuration
- Let's Encrypt and Certbot for Production
- Mutual TLS (mTLS) Basics
- Security : TLS 1.3 , Cipher Selection , Perfect Forward Secrecy
The TLS Layer¶
HTTPS is HTTP running over TLS (formerly SSL). Node's https module wraps http with TLS encryption using the tls and crypto core modules. Without valid certificates , the TLS handshake fails and your clients get ERR_CERT_AUTHORITY_INVALID or just a refused connection
Data sent over plain HTTP is visible to every router , ISP , and coffee shop attacker between client and server. HTTPS ensures confidentiality (encryption), integrity (no tampering), and authentication (server is who it claims)
https.createServer with Certificates¶
const https = require('node:https')
const fs = require('node:fs')
const path = require('node:path')
const options = {
key: fs.readFileSync(path.join(__dirname, 'key.pem')),
cert: fs.readFileSync(path.join(__dirname, 'cert.pem'))
}
const server = https.createServer(options, (req, res) => {
res.writeHead(200, { 'Content-Type': 'application/json' })
res.end(JSON.stringify({
secure: req.socket.encrypted,
protocol: req.socket.getProtocol() // 'TLSv1.3'
}))
})
server.listen(443, () => {
console.log('HTTPS server listening on port 443')
})
The options object is required for HTTPS - without key and cert, createServer throws ERR_INVALID_ARG_TYPE
Key options: * key - private key (PEM string or Buffer) * cert - certificate chain (PEM string or Buffer) * ca - override default CA bundle for custom certs * passphrase - if private key is encrypted * pfx - alternative to key+cert for PFX/P12 files
Generating Self-Signed Certs with OpenSSL¶
For development and testing , you dont need a CA - self-signed certs work fine (your browser will just scream "unsafe" at you)
# generate a 2048-bit RSA private key
openssl genrsa -out key.pem 2048
# create a certificate signing request
openssl req -new -key key.pem -out csr.pem -subj "/CN=localhost"
# self-sign the cert (valid for 365 days)
openssl x509 -req -in csr.pem -signkey key.pem -out cert.pem -days 365
# or do it all in one command:
openssl req -x509 -newkey rsa:2048 -keyout key.pem -out cert.pem -days 365 \
-nodes -subj "/CN=localhost"
The -nodes flag means "no DES" - your private key wont be encrypted with a passphrase. Remove -nodes if you want passphrase protection (your server will prompt for it at startup)
For multiple domains or 127.0.0.1 access:
openssl req -x509 -newkey rsa:2048 -keyout key.pem -out cert.pem -days 365 \
-nodes -subj "/CN=localhost" \
-addext "subjectAltName=DNS:localhost,DNS:*.local,IP:127.0.0.1"
HTTP to HTTPS Redirect Pattern¶
You never want HTTP traffic landing on your application logic. The pattern is an HTTP server that issues 301 redirects:
const http = require('node:http')
const https = require('node:https')
const fs = require('node:fs')
const httpsOptions = {
key: fs.readFileSync('key.pem'),
cert: fs.readFileSync('cert.pem')
}
// HTTP server - only redirects to HTTPS
http.createServer((req, res) => {
const host = req.headers.host || 'localhost'
res.writeHead(301, { 'Location': `https://${host}${req.url}` })
res.end()
}).listen(80)
// HTTPS server - actual application
https.createServer(httpsOptions, (req, res) => {
res.writeHead(200)
res.end('You made it to the secure side')
}).listen(443)
Why 301 and not 302: 301 is permanent - browsers cache it , search engines update their indexes. 301 tells the world "I never want to speak HTTP again"
HSTS header - tell browsers to never even try HTTP:
res.setHeader('Strict-Transport-Security', 'max-age=31536000; includeSubDomains')
TLS Versions and Cipher Configuration¶
Node.js lets you lock down which TLS versions and ciphers your server accepts
const options = {
key: fs.readFileSync('key.pem'),
cert: fs.readFileSync('cert.pem'),
// minimum TLS version - reject SSLv3 , TLS 1.0 , TLS 1.1
minVersion: 'TLSv1.2',
// maximum TLS version
maxVersion: 'TLSv1.3',
// specific cipher suite (OpenSSL format)
ciphers: [
'TLS_AES_256_GCM_SHA384', // TLS 1.3
'TLS_CHACHA20_POLY1305_SHA256', // TLS 1.3
'ECDHE-RSA-AES128-GCM-SHA256', // TLS 1.2
'ECDHE-ECDSA-AES256-GCM-SHA384', // TLS 1.2
].join(':'),
// prefer server cipher order over client
honorCipherOrder: true,
// require perfect forward secrecy ciphers only
ecdhCurve: 'auto'
}
What you actually want in production: * minVersion: 'TLSv1.2' - TLS 1.0 and 1.1 are deprecated everywhere * maxVersion: 'TLSv1.3' - TLS 1.3 is the gold standard * honorCipherOrder: true - server picks , not the client
Check your TLS config with openssl s_client:
openssl s_client -connect localhost:443 -tls1_2
openssl s_client -connect localhost:443 -tls1_3
Let's Encrypt and Certbot for Production¶
Self-signed certs are fine for local dev. For production you need a CA-signed cert and Let's Encrypt gives them for free
# install certbot (Ubuntu/Debian)
sudo apt install certbot
# get certificate (standalone mode - stops your server temporarily)
sudo certbot certonly --standalone -d api.yourdomain.com
# or use webroot mode (no downtime)
sudo certbot certonly --webroot -w /var/www/html -d api.yourdomain.com
Certificates land in /etc/letsencrypt/live/api.yourdomain.com/
const options = {
key: fs.readFileSync('/etc/letsencrypt/live/api.yourdomain.com/privkey.pem'),
cert: fs.readFileSync('/etc/letsencrypt/live/api.yourdomain.com/fullchain.pem')
}
https.createServer(options, handler).listen(443)
Auto-renewal with cron:
# run daily at 3 AM
0 3 * * * /usr/bin/certbot renew --quiet --post-hook "systemctl reload nginx"
Certbot certs expire in 90 days. Renewal is your problem - set up monitoring or your site goes dark
Mutual TLS (mTLS) Basics¶
mTLS flips the authentication - the client also presents a certificate to prove their identity
const options = {
key: fs.readFileSync('server-key.pem'),
cert: fs.readFileSync('server-cert.pem'),
ca: fs.readFileSync('ca-cert.pem'), // CA that signed client certs
requestCert: true, // ask client for certificate
rejectUnauthorized: true // reject if cert not signed by ca
}
const server = https.createServer(options, (req, res) => {
const clientCert = req.socket.getPeerCertificate()
if (!clientCert.subject) {
res.writeHead(401)
return res.end('{"error": "certificate required"}')
}
// client identity from certificate
const clientCN = clientCert.subject.CN
console.log(`Authenticated client: ${clientCN}`)
res.writeHead(200)
res.end(JSON.stringify({ authenticated: clientCN }))
})
mTLS use cases: * Service-to-service auth in microservices * API access for trusted clients without passwords * IoT device authentication * Zero-trust network architectures
Security : TLS 1.3 , Cipher Selection , Perfect Forward Secrecy¶
Your TLS config is your first line of defense. Screw it up and all the application security in the world wont save you
TLS 1.3 dropped in 2018 and it's a massive improvement: * Removed all insecure ciphers (no more RC4 , 3DES , CBC modes) * Reduced handshake from 2-RTT to 1-RTT (0-RTT for resuming) * Removed static RSA key exchange - every connection gets ephemeral keys * Encrypted more of the handshake (hides certificates from eavesdroppers)
Perfect Forward Secrecy (PFS) means compromising the server's long-term private key doesnt let you decrypt past recorded traffic. Each session generates ephemeral keys through Diffie-Hellman key exchange - those keys are discarded after the session ends
Ciphers that provide PFS start with ECDHE or DHE: * ECDHE-RSA-AES256-GCM-SHA384 (PFS) * ECDHE-ECDSA-AES256-GCM-SHA384 (PFS) * TLS_AES_256_GCM_SHA384 (TLS 1.3 , always PFS)
Ciphers without PFS (avoid): * RSA-AES256-GCM-SHA384 - no forward secrecy * Any TLS_RSA_* - compromised key means all past traffic decrypted
Testing your TLS config:
# check supported protocols
nmap --script ssl-enum-ciphers -p 443 localhost
# detailed handshake analysis
openssl s_client -connect localhost:443 -debug
# comprehensive check (requires testssl.sh)
./testssl.sh --parallel https://localhost:443
Minimum secure config for 2026:
const options = {
key: privateKey,
cert: certificate,
minVersion: 'TLSv1.2',
maxVersion: 'TLSv1.3',
ciphers: 'TLS_AES_256_GCM_SHA384:TLS_CHACHA20_POLY1305_SHA256:ECDHE-RSA-AES128-GCM-SHA256',
honorCipherOrder: true,
ecdhCurve: 'auto'
}
prerequisites¶
web_01_http.md - HTTP server fundamentals , request/response objects
next => web_03_routing.md