Security — SSL/TLS & HTTPS
TLS (Transport Layer Security) and its predecessor SSL (Secure Sockets Layer) are cryptographic protocols that provide secure communication over a network. HTTPS is HTTP over TLS — it encrypts all data between the client and server, ensuring confidentiality, integrity, and authentication.
Without TLS, all data is transmitted in plain text — passwords, API keys, personal information, and session tokens can be intercepted by anyone on the network. Modern web applications should use TLS everywhere, not just on login pages.
The TLS handshake establishes a secure connection between client and server. It involves cipher suite negotiation, certificate verification, and key exchange.
| Step | Description | Details |
|---|---|---|
| 1 | Client Hello | Client sends supported TLS versions, cipher suites, and a random number |
| 2 | Server Hello | Server selects TLS version, cipher suite, sends its certificate and random number |
| 3 | Certificate Verification | Client verifies server certificate against trusted CAs |
| 4 | Key Exchange | Client and server derive shared session key (ECDHE, DHE) |
| 5 | Finished | Encrypted handshake messages exchanged, secure channel established |
info
Let's Encrypt is a free, automated, and open CA that provides TLS certificates via the ACME (Automated Certificate Management Environment) protocol. Certificates are valid for 90 days and must be renewed automatically.
| 1 | # Install Certbot (Let's Encrypt client) |
| 2 | brew install certbot # macOS |
| 3 | apt install certbot # Ubuntu/Debian |
| 4 | |
| 5 | # Obtain certificate (standalone mode — temporary web server) |
| 6 | certbot certonly --standalone -d example.com -d www.example.com --email admin@example.com --agree-tos |
| 7 | |
| 8 | # Obtain certificate (webroot mode — existing web server) |
| 9 | certbot certonly --webroot -w /var/www/html -d example.com -d www.example.com |
| 10 | |
| 11 | # Certificate location: |
| 12 | # /etc/letsencrypt/live/example.com/fullchain.pem |
| 13 | # /etc/letsencrypt/live/example.com/privkey.pem |
| 14 | |
| 15 | # Automatic renewal (certbot adds systemd timer) |
| 16 | certbot renew |
| 17 | |
| 18 | # Test renewal process |
| 19 | certbot renew --dry-run |
| 20 | |
| 21 | # DNS-01 challenge (for wildcard certificates) |
| 22 | certbot certonly --manual --preferred-challenges dns -d *.example.com -d example.com |
| 23 | |
| 24 | # ACME client alternatives: |
| 25 | # acme.sh, lego, Caddy automatic HTTPS, Traefik ACME |
info
TLS termination is the process of decrypting HTTPS traffic at a proxy or load balancer before forwarding it to the backend server. This offloads the CPU-intensive encryption work from application servers.
| 1 | # nginx — TLS termination configuration |
| 2 | server { |
| 3 | listen 443 ssl http2; |
| 4 | server_name example.com www.example.com; |
| 5 | |
| 6 | # Certificate paths |
| 7 | ssl_certificate /etc/letsencrypt/live/example.com/fullchain.pem; |
| 8 | ssl_certificate_key /etc/letsencrypt/live/example.com/privkey.pem; |
| 9 | |
| 10 | # TLS protocols — disable obsolete versions |
| 11 | ssl_protocols TLSv1.2 TLSv1.3; |
| 12 | |
| 13 | # Secure cipher suites (Mozilla intermediate) |
| 14 | ssl_ciphers ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256; |
| 15 | ssl_prefer_server_ciphers on; |
| 16 | |
| 17 | # OCSP stapling |
| 18 | ssl_stapling on; |
| 19 | ssl_stapling_verify on; |
| 20 | resolver 8.8.8.8 8.8.4.4 valid=300s; |
| 21 | resolver_timeout 5s; |
| 22 | |
| 23 | # HSTS |
| 24 | add_header Strict-Transport-Security "max-age=63072000; includeSubDomains; preload"; |
| 25 | |
| 26 | # Proxy to backend |
| 27 | location / { |
| 28 | proxy_pass http://backend:3000; |
| 29 | proxy_set_header Host $host; |
| 30 | proxy_set_header X-Real-IP $remote_addr; |
| 31 | proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; |
| 32 | proxy_set_header X-Forwarded-Proto $scheme; |
| 33 | } |
| 34 | } |
| 35 | |
| 36 | # Redirect HTTP to HTTPS |
| 37 | server { |
| 38 | listen 80; |
| 39 | server_name example.com www.example.com; |
| 40 | return 301 https://$server_name$request_uri; |
| 41 | } |
| 42 | |
| 43 | # Cloudflare — Full (Strict) TLS mode |
| 44 | # Cloudflare terminates TLS at their edge, re-encrypts to origin |
| 45 | # Origin certificate can be self-signed (Cloudflare Origin CA) |
| 46 | |
| 47 | # AWS ELB — TLS termination at load balancer |
| 48 | # Upload certificate to ACM (AWS Certificate Manager) |
| 49 | # Listener: HTTPS:443 -> Target Group: HTTP:3000 |
| 50 | # Target group health check: HTTP |
best practice
HSTS is a response header that tells browsers to always connect via HTTPS, even if the user types http:// or clicks an HTTP link. This prevents SSL stripping and downgrade attacks.
| 1 | # HSTS header formats |
| 2 | |
| 3 | # Basic — 1 year, current domain only |
| 4 | Strict-Transport-Security: max-age=31536000 |
| 5 | |
| 6 | # Recommended — 2 years, all subdomains, preload eligibility |
| 7 | Strict-Transport-Security: max-age=63072000; includeSubDomains; preload |
| 8 | |
| 9 | # Nginx |
| 10 | add_header Strict-Transport-Security "max-age=63072000; includeSubDomains; preload"; |
| 11 | |
| 12 | # Apache |
| 13 | Header always set Strict-Transport-Security "max-age=63072000; includeSubDomains; preload" |
| 14 | |
| 15 | # Express/Node.js (via helmet) |
| 16 | app.use(helmet.hsts({ |
| 17 | maxAge: 63072000, |
| 18 | includeSubDomains: true, |
| 19 | preload: true, |
| 20 | })); |
| 21 | |
| 22 | # HSTS Preload — submit your domain to https://hstspreload.org/ |
| 23 | # Requirements: |
| 24 | # 1. Valid HTTPS certificate on the root domain |
| 25 | # 2. Redirect HTTP to HTTPS on all subdomains |
| 26 | # 3. HSTS header with max-age >= 31536000 and includeSubDomains |
| 27 | # 4. HSTS header on the main domain for all requests |
| 28 | |
| 29 | # Once accepted, browsers hardcode your domain as HTTPS-only |
| 30 | # Removal takes months (browser updates are slow) |
danger
Certificate pinning associates a host with its expected certificate or public key. While CA verification checks against a list of trusted CAs, pinning checks against a specific certificate or key. HTTP Public Key Pinning (HPKP) is deprecated — modern approaches use Expect-CT or certificate transparency.
| 1 | // Certificate pinning in Node.js (for API clients) |
| 2 | import https from 'https'; |
| 3 | import crypto from 'crypto'; |
| 4 | import fs from 'fs'; |
| 5 | |
| 6 | // Option 1: Pin by certificate fingerprint |
| 7 | const EXPECTED_PIN = 'sha256/abc123def456...'; |
| 8 | |
| 9 | const options = { |
| 10 | hostname: 'api.example.com', |
| 11 | port: 443, |
| 12 | path: '/', |
| 13 | checkServerIdentity: (hostname, cert) => { |
| 14 | const fingerprint = crypto |
| 15 | .createHash('sha256') |
| 16 | .update(cert.raw) |
| 17 | .digest('base64'); |
| 18 | |
| 19 | const pin = `sha256/${fingerprint}`; |
| 20 | |
| 21 | if (pin !== EXPECTED_PIN) { |
| 22 | return new Error(`Certificate pin mismatch: expected ${EXPECTED_PIN}, got ${pin}`); |
| 23 | } |
| 24 | |
| 25 | return undefined; // No error — certificate matches |
| 26 | }, |
| 27 | }; |
| 28 | |
| 29 | https.get(options, (res) => { |
| 30 | console.log('Connection established with pinned certificate'); |
| 31 | }); |
| 32 | |
| 33 | // Option 2: Pin by public key |
| 34 | const EXPECTED_PUBLIC_KEY = '...'; |
| 35 | |
| 36 | const checkPublicKey = (cert) => { |
| 37 | const publicKey = crypto |
| 38 | .createHash('sha256') |
| 39 | .update(cert.pubkey) |
| 40 | .digest('base64'); |
| 41 | |
| 42 | return publicKey === EXPECTED_PUBLIC_KEY; |
| 43 | }; |
| 44 | |
| 45 | // Certificate Transparency — alternative to pinning |
| 46 | // Use Expect-CT header to enforce CT log inclusion |
| 47 | // Expect-CT: max-age=86400, enforce, report-uri="https://example.com/report" |
| 48 | |
| 49 | // Modern approach: use Certificate Transparency (CT) logs |
| 50 | // All publicly trusted CAs must log certificates to CT logs |
| 51 | // Browsers require CT for certificates issued after April 2018 |
warning
Mutual TLS extends the TLS handshake so that the client also presents a certificate to the server. Both sides verify each other's identity. It is commonly used in microservice communication, IoT, and API security.
| 1 | # Generate client certificate (mTLS) |
| 2 | # 1. Create a CA for internal use |
| 3 | openssl genrsa -out ca.key 4096 |
| 4 | openssl req -x509 -new -nodes -key ca.key -sha256 -days 3650 -out ca.crt -subj "/CN=Internal CA" |
| 5 | |
| 6 | # 2. Generate client certificate |
| 7 | openssl genrsa -out client.key 2048 |
| 8 | openssl req -new -key client.key -out client.csr -subj "/CN=api-client" |
| 9 | |
| 10 | # 3. Sign with CA |
| 11 | openssl x509 -req -in client.csr -CA ca.crt -CAkey ca.key -CAcreateserial -out client.crt -days 365 -sha256 |
| 12 | |
| 13 | # nginx — require client certificate |
| 14 | server { |
| 15 | listen 443 ssl http2; |
| 16 | server_name api.example.com; |
| 17 | |
| 18 | ssl_certificate /etc/ssl/server.crt; |
| 19 | ssl_certificate_key /etc/ssl/server.key; |
| 20 | |
| 21 | # mTLS configuration |
| 22 | ssl_client_certificate /etc/ssl/ca.crt; # Trusted CA |
| 23 | ssl_verify_client on; # Require client cert |
| 24 | ssl_verify_depth 2; # Max chain depth |
| 25 | |
| 26 | # Pass client certificate info to backend |
| 27 | location / { |
| 28 | proxy_set_header X-SSL-Client-Cert $ssl_client_cert; |
| 29 | proxy_set_header X-SSL-Client-Verify $ssl_client_verify; |
| 30 | proxy_set_header X-SSL-Client-DN $ssl_client_s_dn; |
| 31 | proxy_pass http://backend:3000; |
| 32 | } |
| 33 | } |
| 34 | |
| 35 | # Envoy proxy mTLS configuration |
| 36 | # static_resources: |
| 37 | # listeners: |
| 38 | # - filters: |
| 39 | # - typed_config: |
| 40 | # "@type": type.googleapis.com/envoy.extensions.transport_sockets.tls.v3.DownstreamTlsContext |
| 41 | # require_client_certificate: true |
| 42 | # common_tls_context: |
| 43 | # tls_certificates: |
| 44 | # certificate_chain: { filename: "/etc/envoy/server.crt" } |
| 45 | # private_key: { filename: "/etc/envoy/server.key" } |
| 46 | # validation_context: |
| 47 | # trusted_ca: { filename: "/etc/envoy/ca.crt" } |
best practice
TLS 1.3 is the latest version of the protocol, offering significant improvements in security, performance, and privacy over TLS 1.2.
| Feature | TLS 1.2 | TLS 1.3 |
|---|---|---|
| Handshake rounds | 2 round trips (2-RTT) | 1 round trip (1-RTT) or 0-RTT |
| Cipher suites | Many options (some insecure) | 5 secure AEAD ciphers only |
| Forward secrecy | Optional | Mandatory for all ciphers |
| Deprecated | Still supports RC4, 3DES, CBC | Removes all legacy algorithms |
| 0-RTT | Not supported | Supported (with anti-replay) |
| Browser support | Universal | 97%+ (2024) |
| 1 | # Check TLS version support for a domain |
| 2 | openssl s_client -connect example.com:443 -tls1_2 2>/dev/null | grep "Protocol" |
| 3 | openssl s_client -connect example.com:443 -tls1_3 2>/dev/null | grep "Protocol" |
| 4 | |
| 5 | # Test cipher suite support |
| 6 | openssl s_client -connect example.com:443 -cipher ECDHE-RSA-AES128-GCM-SHA256 2>/dev/null |
| 7 | |
| 8 | # Nginx — disable TLS 1.0 and 1.1 |
| 9 | ssl_protocols TLSv1.2 TLSv1.3; |
| 10 | |
| 11 | # Cloudflare — TLS settings in dashboard |
| 12 | # SSL/TLS > Edge TLS Settings > Minimum TLS Version: 1.2 |
| 13 | |
| 14 | # AWS CloudFront — security policy |
| 15 | # Security Policy: TLSv1.2_2021 or TLSv1.2_2023 (includes TLS 1.3) |
info
Regularly test your TLS configuration to ensure it meets current security standards. These tools check for common misconfigurations, weak ciphers, and protocol support.
| 1 | # SSL Labs — online scanner (https://www.ssllabs.com/ssltest/) |
| 2 | # Tests: certificate, protocol support, cipher strength, TLS 1.3, HSTS |
| 3 | # Score: A+ to F |
| 4 | |
| 5 | # testssl.sh — local TLS scanner |
| 6 | git clone https://github.com/drwetter/testssl.sh.git |
| 7 | cd testssl.sh |
| 8 | ./testssl.sh https://example.com |
| 9 | |
| 10 | # Check specific vulnerabilities |
| 11 | ./testssl.sh --heartbleed https://example.com |
| 12 | ./testssl.sh --poodle https://example.com |
| 13 | ./testssl.sh --logjam https://example.com |
| 14 | ./testssl.sh --freak https://example.com |
| 15 | |
| 16 | # Comprehensive scan |
| 17 | ./testssl.sh --fast --parallel https://example.com |
| 18 | ./testssl.sh --full https://example.com |
| 19 | |
| 20 | # Output formats: HTML, JSON, CSV |
| 21 | ./testssl.sh --htmlfile report.html https://example.com |
| 22 | |
| 23 | # Additional checker: Mozilla SSL Configuration Generator |
| 24 | # https://ssl-config.mozilla.org/ |
| 25 | # Generates secure configs for nginx, Apache, HAProxy, etc. |
| 26 | |
| 27 | # Qualys SSL Labs API |
| 28 | curl -s "https://api.ssllabs.com/api/v3/analyze?host=example.com" | jq . |
best practice
Use HTTPS locally during development to detect mixed content issues, test service workers, and work with secure-only APIs. mkcert creates locally trusted certificates easily.
| 1 | # mkcert — local development certificates |
| 2 | brew install mkcert # macOS |
| 3 | # apt install mkcert # Linux |
| 4 | |
| 5 | # Install local CA |
| 6 | mkcert -install |
| 7 | |
| 8 | # Create certificate for localhost and dev domains |
| 9 | mkcert localhost 127.0.0.1 ::1 |
| 10 | # Creates: localhost+2.pem, localhost+2-key.pem |
| 11 | |
| 12 | # Create for custom dev domain |
| 13 | mkcert myapp.local "*.myapp.local" |
| 14 | |
| 15 | # Use with Node.js HTTPS server |
| 16 | // const https = require('https'); |
| 17 | // const fs = require('fs'); |
| 18 | // |
| 19 | // const options = { |
| 20 | // key: fs.readFileSync('localhost+2-key.pem'), |
| 21 | // cert: fs.readFileSync('localhost+2.pem'), |
| 22 | // }; |
| 23 | // |
| 24 | // https.createServer(options, app).listen(3000); |
| 25 | |
| 26 | # Use with Next.js (next.config.js) |
| 27 | // module.exports = { |
| 28 | // devIndicators: { |
| 29 | // https: true, |
| 30 | // }, |
| 31 | // }; |
| 32 | |
| 33 | # Self-signed certificates (alternative to mkcert) |
| 34 | openssl req -x509 -nodes -days 365 -newkey rsa:2048 -keyout selfsigned.key -out selfsigned.crt -subj "/CN=localhost" -addext "subjectAltName=DNS:localhost,IP:127.0.0.1" |
| 35 | |
| 36 | # Note: self-signed certs show browser warnings |
| 37 | # mkcert is preferred because the CA is trusted locally |
info
- Use TLS 1.2 or 1.3 — disable SSLv3, TLS 1.0, and TLS 1.1.
- Use modern, secure cipher suites with forward secrecy (ECDHE).
- Enable HSTS with includeSubDomains and submit to the preload list.
- Automate certificate renewal with ACME (Let's Encrypt, ZeroSSL).
- Use OCSP stapling to improve certificate verification performance and privacy.
- Terminate TLS at the edge (load balancer, CDN, or reverse proxy).
- Test your TLS configuration regularly with SSL Labs and testssl.sh.
- Use HTTPS in all environments, including development — never disable HTTPS verification in production code.
- Pin certificates only when necessary (mobile apps) and include backup pins.
- Monitor certificate expiry — set up alerts at 30, 14, and 7 days before expiration.
best practice