Understanding TLS
Transport Layer Security (TLS) is the cryptographic protocol that secures communication over the internet. When you see HTTPS in your browser's address bar, TLS is what provides the encryption, authentication, and integrity of data in transit. TLS is the successor to SSL (Secure Sockets Layer), and while people often say "SSL" colloquially, modern systems should exclusively use TLS 1.2 or TLS 1.3.
TLS provides three key security properties: encryption (confidentiality — prevents eavesdropping), authentication (verifies the server's identity via certificates), and integrity (ensures data has not been modified in transit using MAC codes). The TLS handshake establishes these properties before any application data is exchanged.
TLS 1.3 Improvements
- Faster handshake: TLS 1.3 completes the handshake in one round trip (1-RTT) instead of two, reducing latency. It also supports 0-RTT resumption for returning clients.
- Stronger security: Removed weak ciphers (RC4, 3DES, CBC mode), static RSA key exchange, and compression. Only AEAD ciphers are allowed.
- Forward secrecy by default: All TLS 1.3 cipher suites use ephemeral Diffie-Hellman key exchange, ensuring past sessions remain secure even if the server's private key is compromised.
- Simplified cipher suites: Only five cipher suites in TLS 1.3, eliminating the complexity and misconfiguration risks of TLS 1.2's hundreds of options.
Certificate Management
TLS certificates bind a public key to a domain name, verified by a Certificate Authority (CA). When a browser connects to your server, it verifies that the certificate is valid, not expired, issued by a trusted CA, and matches the requested domain. Let's Encrypt has made obtaining free certificates trivial, so there is no excuse for running HTTP in production.
import https from "https";
import fs from "fs";
import tls from "tls";
// Secure HTTPS server configuration
const server = https.createServer(
{
// Certificate files
key: fs.readFileSync("/etc/letsencrypt/live/example.com/privkey.pem"),
cert: fs.readFileSync("/etc/letsencrypt/live/example.com/fullchain.pem"),
// Minimum TLS version (disable TLS 1.0 and 1.1)
minVersion: "TLSv1.2",
// Prefer server cipher order
honorCipherOrder: true,
// Recommended cipher suites for TLS 1.2
// TLS 1.3 ciphers are configured automatically
ciphers: [
"ECDHE-ECDSA-AES256-GCM-SHA384",
"ECDHE-RSA-AES256-GCM-SHA384",
"ECDHE-ECDSA-CHACHA20-POLY1305",
"ECDHE-RSA-CHACHA20-POLY1305",
"ECDHE-ECDSA-AES128-GCM-SHA256",
"ECDHE-RSA-AES128-GCM-SHA256",
].join(":"),
// Enable OCSP stapling for faster certificate verification
// (requires additional setup with your CA)
},
app
);
// Redirect HTTP to HTTPS
import http from "http";
http.createServer((req, res) => {
res.writeHead(301, {
Location: `https://${req.headers.host}${req.url}`,
});
res.end();
}).listen(80);
// Certificate expiry monitoring
function checkCertificateExpiry(hostname: string): Promise<{
daysUntilExpiry: number;
issuer: string;
validTo: Date;
}> {
return new Promise((resolve, reject) => {
const socket = tls.connect(443, hostname, { servername: hostname }, () => {
const cert = socket.getPeerCertificate();
const validTo = new Date(cert.valid_to);
const daysUntilExpiry = Math.floor(
(validTo.getTime() - Date.now()) / (1000 * 60 * 60 * 24)
);
socket.end();
resolve({
daysUntilExpiry,
issuer: cert.issuer.O,
validTo,
});
});
socket.on("error", reject);
});
}
// Alert if certificate expires within 30 days
async function monitorCertificates(domains: string[]) {
for (const domain of domains) {
const info = await checkCertificateExpiry(domain);
if (info.daysUntilExpiry < 30) {
await alertOps({
severity: info.daysUntilExpiry < 7 ? "critical" : "warning",
message: `Certificate for ${domain} expires in ${info.daysUntilExpiry} days`,
});
}
}
}
HSTS (HTTP Strict Transport Security)
HSTS tells browsers to always use HTTPS when communicating with your domain, even if the user types http:// or clicks an HTTP link. Without HSTS, the first request to your site might be over HTTP before being redirected to HTTPS, creating a window for a man-in-the-middle attack to intercept the initial request.
// HSTS header implementation
app.use((req, res, next) => {
// max-age: How long (seconds) the browser should remember to only use HTTPS
// includeSubDomains: Apply to all subdomains
// preload: Opt into browser HSTS preload lists
res.setHeader(
"Strict-Transport-Security",
"max-age=63072000; includeSubDomains; preload"
);
next();
});
// Certificate pinning for critical APIs (use with caution)
// This pins specific certificates or public keys
// WARNING: Incorrect pinning can make your site unreachable
const pinnedCertificates = [
"sha256/AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=", // Primary
"sha256/BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB=", // Backup
];
function verifyCertificatePin(socket: tls.TLSSocket): boolean {
const cert = socket.getPeerCertificate();
const publicKeyHash = crypto
.createHash("sha256")
.update(cert.pubkey)
.digest("base64");
return pinnedCertificates.includes(`sha256/${publicKeyHash}`);
}
Security Warning: TLS Misconfigurations
- Do not use TLS 1.0 or 1.1: These versions have known vulnerabilities (POODLE, BEAST). Use TLS 1.2 minimum, prefer TLS 1.3.
- Do not use weak cipher suites: Disable RC4, 3DES, CBC-mode ciphers, and export ciphers. Use only AEAD ciphers (GCM, ChaCha20-Poly1305).
- Do not disable certificate validation: Never set rejectUnauthorized: false in production. This defeats the entire purpose of TLS.
- Automate certificate renewal: Expired certificates cause outages and security warnings. Use certbot or similar automation.
Mutual TLS (mTLS)
Standard TLS only authenticates the server — the client verifies the server's certificate, but the server does not verify the client. Mutual TLS (mTLS) adds client certificate authentication, where both parties present and verify certificates. This is commonly used for service-to-service communication in microservices architectures.
// mTLS server configuration
const mtlsServer = https.createServer(
{
key: fs.readFileSync("./certs/server-key.pem"),
cert: fs.readFileSync("./certs/server-cert.pem"),
ca: fs.readFileSync("./certs/ca-cert.pem"), // CA that signed client certs
requestCert: true, // Request client certificate
rejectUnauthorized: true, // Reject clients without valid certificate
minVersion: "TLSv1.2",
},
(req, res) => {
// Access client certificate info
const clientCert = (req.socket as tls.TLSSocket).getPeerCertificate();
console.log("Client:", clientCert.subject.CN);
res.writeHead(200);
res.end("Authenticated via mTLS");
}
);
// mTLS client making requests
async function mtlsFetch(url: string, options: RequestInit = {}) {
const agent = new https.Agent({
key: fs.readFileSync("./certs/client-key.pem"),
cert: fs.readFileSync("./certs/client-cert.pem"),
ca: fs.readFileSync("./certs/ca-cert.pem"),
});
return fetch(url, { ...options, agent } as any);
}
TLS Configuration Checklist
- Use TLS 1.3: With TLS 1.2 as a fallback for older clients.
- Enable HSTS: With a long max-age, includeSubDomains, and preload.
- Use strong cipher suites: ECDHE with AES-GCM or ChaCha20-Poly1305.
- Automate certificates: Use Let's Encrypt with auto-renewal.
- Monitor certificate expiry: Alert at least 30 days before expiration.
- Test your configuration: Use SSL Labs (ssllabs.com/ssltest) to verify your setup.