NodeJSTLS/HTTPS & Secure Transport

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)

Text
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

Text
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.
Terminate TLS at the proxy — Node rarely handles certificates directly in production
Running TLS inside Node (`https.createServer({ cert, key }, app)`) is correct for small tools and development, but for production the standard is to let a reverse proxy (Nginx, Caddy, AWS ALB, Cloudflare) terminate TLS and forward plain HTTP to Node on an internal port. The proxy handles certificate rotation, modern TLS cipher negotiation, HTTP/2, and edge caching. Node's job is the application; the proxy's job is the network edge. This also lets you scale Node horizontally without duplicating certificate management.
Nginx TLS configuration

nginx.conf

Text
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;
}
Disable TLS 1.0 and 1.1 — they have known protocol weaknesses (POODLE, BEAST)
TLS 1.0 and 1.1 are deprecated by all major browsers and standard bodies. Enable only TLS 1.2 and 1.3. TLS 1.3 is faster (1-RTT handshake) and has a smaller, more secure cipher suite — prefer it. Most modern Nginx/OpenSSL configurations already do this, but verify with `openssl s_client -connect yourapp.com:443` or Mozilla's SSL Configuration Generator.
Let's Encrypt — free, automated certificates

Bash
# 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
Let's Encrypt certificates expire in 90 days — automate renewal or the site goes down
The 90-day lifetime is intentional — it encourages automation. Certbot installs a renewal timer automatically, but verify it's running (`systemctl status certbot.timer`) and test it with `--dry-run`. For cloud deployments, AWS ACM (Certificate Manager) renews automatically and integrates with ALB; Cloudflare manages TLS entirely. The worst production incident you can have is an expired certificate that's entirely preventable with a one-time automation setup.
Trust X-Forwarded-Proto behind a proxy

JS
// 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()
})
Without `trust proxy`, `req.secure` is always false behind a reverse proxy
When Nginx forwards traffic to Node on port 3000 as plain HTTP, `req.protocol` is `'http'` even though the client used HTTPS. Without `app.set('trust proxy', 1)`, Express doesn't read the `X-Forwarded-Proto: https` header set by the proxy, so `req.secure` is always `false`. This breaks HTTPS-only redirects and can cause secure cookies to be sent over "plain HTTP" (they aren't — Nginx encrypted it — but Node doesn't know). Set `trust proxy` to the number of proxy hops between the user and Node.
Security checklist
  • TLS 1.2 and 1.3 only — disable older versions.

  • HTTPS redirect — every HTTP request → 301 to HTTPS.

  • HSTSStrict-Transport-Security: max-age=31536000.

  • Automate certificate renewal — expiry is a preventable outage.

  • trust proxy set correctly — so req.secure and cookies work behind a proxy.

  • Strong ciphers — use Mozilla's SSL Config Generator as a reference.

  • Verify with SSL Labs — targets an A+ rating.

Next
Wrap it all up: [Security Best Practices](/nodejs/security-best-practices).