TLS/HTTPS & Secure Transport
TLS (Transport Layer Security) — the protocol behind HTTPS — encrypts the connection between client and server so that passwords, session tokens, and personal data can't be read or modified in transit. Without it, any coffee-shop Wi-Fi, ISP, or network middlebox can see and alter every byte. In production, Node.js apps almost never terminate TLS themselves — a reverse proxy (Nginx, Caddy, a load balancer) handles the certificate and hands off plain HTTP to Node. This page covers TLS fundamentals, the proxy model, certificate management with Let's Encrypt, and the configuration mistakes that make TLS meaningless.
How TLS works (the brief version)
Client → Server TLS Handshake: 1. Client says: "I want TLS; here's what I support" (TLS ClientHello) 2. Server sends its certificate (public key + identity, signed by a CA) 3. Client verifies the certificate against trusted CA roots 4. They negotiate a shared symmetric key (Diffie-Hellman or similar) 5. All subsequent data is encrypted with that symmetric key Result: encrypted channel + server identity verified. → No eavesdropping (encryption) + No impersonation (certificate)
The reverse-proxy model for Node
Internet → [Nginx / Caddy / Load Balancer] :443 (TLS termination)
↓ HTTP (plain, internal network)
Node.js app :3000
Why: certificate renewal, TLS config, and performance tuning belong to
the proxy layer. Node focuses on application logic.Nginx TLS configuration
nginx.conf
server {
listen 443 ssl http2;
server_name yourapp.com;
ssl_certificate /etc/letsencrypt/live/yourapp.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/yourapp.com/privkey.pem;
ssl_protocols TLSv1.2 TLSv1.3; # disable TLS 1.0 and 1.1
ssl_ciphers HIGH:!aNULL:!MD5;
ssl_prefer_server_ciphers on;
# HSTS — browser must use HTTPS for 1 year:
add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;
location / {
proxy_pass http://localhost:3000;
proxy_set_header X-Forwarded-Proto $scheme; # tell Node the original scheme
proxy_set_header X-Real-IP $remote_addr;
}
}
# Redirect HTTP → HTTPS:
server {
listen 80;
return 301 https://$host$request_uri;
}Let's Encrypt — free, automated certificates
# Certbot (the most common Let's Encrypt client): sudo apt install certbot python3-certbot-nginx sudo certbot --nginx -d yourapp.com -d www.yourapp.com # Auto-renew (Let's Encrypt certs expire after 90 days): # Certbot installs a cron/systemd timer — verify it: sudo certbot renew --dry-run
Trust X-Forwarded-Proto behind a proxy
// Tell Express it's behind a trusted proxy so req.protocol === 'https':
app.set('trust proxy', 1) // trust one hop (your load balancer/Nginx)
// Now you can enforce HTTPS in middleware:
app.use((req, res, next) => {
if (req.protocol !== 'https' && process.env.NODE_ENV === 'production') {
return res.redirect(301, `https://${req.hostname}${req.originalUrl}`)
}
next()
})Security checklist
TLS 1.2 and 1.3 only — disable older versions.
HTTPS redirect — every HTTP request → 301 to HTTPS.
HSTS —
Strict-Transport-Security: max-age=31536000.Automate certificate renewal — expiry is a preventable outage.
trust proxyset correctly — soreq.secureand cookies work behind a proxy.Strong ciphers — use Mozilla's SSL Config Generator as a reference.
Verify with SSL Labs — targets an A+ rating.