control over protocol negotiation, certificate lifecycle, and client validation. The following steps outline a hardened, scalable configuration strategy.
Step 1: Enforce Protocol and Cipher Suite Boundaries
TLS 1.3 fixes the cipher suite negotiation flaws present in TLS 1.2 by removing weak algorithms and standardizing key exchange. Configure servers to advertise only TLS 1.3 by default. If legacy support is unavoidable, restrict TLS 1.2 to explicitly whitelisted ECDHE cipher suites with AEAD encryption.
Required TLS 1.3 cipher suites:
TLS_AES_128_GCM_SHA256
TLS_AES_256_GCM_SHA384
TLS_CHACHA20_POLY1305_SHA256
Required TLS 1.2 fallback (if needed):
TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256
TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256
TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305_SHA256
Disable all RSA key exchange, CBC modes, SHA-1, and NULL/EXPORT ciphers. Server-side configuration should prioritize ChaCha20-Poly1305 for mobile and low-power clients, and AES-GCM for hardware-accelerated environments.
Step 2: Certificate Chain and Key Management
A valid TLS connection requires a complete certificate chain: leaf certificate, intermediate CA, and root trust anchor. Missing intermediates cause validation failures on strict clients. Use ECDSA P-256 or P-384 keys for leaf certificates; they provide equivalent security to RSA-2048/4096 with smaller signatures and faster verification.
Automate issuance and rotation using ACME v2 (RFC 8555). Static certificate management is the primary cause of production outages. Implement certificate transparency logging and pre-certificates to detect misissuance early.
Step 3: Server-Side Hardening Parameters
Enable HTTP Strict Transport Security (HSTS) with a minimum max-age of 31536000 seconds, includeSubDomains, and preload after a validation period. Configure OCSP stapling to shift certificate revocation verification to the server, reducing client latency and preventing privacy leaks. Disable session tickets or rotate their keys frequently to maintain forward secrecy. Enable ECDHE key exchange explicitly to ensure ephemeral keys are generated per session.
Step 4: Client-Side Implementation in TypeScript
Node.js and modern TypeScript environments expose the tls and https modules for strict client configuration. The following example demonstrates production-grade TLS client initialization with explicit CA trust, certificate pinning options, and handshake validation.
import https from 'https';
import tls from 'tls';
import { readFileSync } from 'fs';
import { createHash } from 'crypto';
interface TlsClientConfig {
caPath: string;
certPath?: string;
keyPath?: string;
passphrase?: string;
pinFingerprint?: string;
rejectUnauthorized: boolean;
}
export function createSecureTlsAgent(config: TlsClientConfig): https.Agent {
const tlsOptions: tls.SecureContextOptions = {
ca: readFileSync(config.caPath),
rejectUnauthorized: config.rejectUnauthorized,
minVersion: 'TLSv1.3',
maxVersion: 'TLSv1.3',
ciphers: 'TLS_AES_128_GCM_SHA256:TLS_AES_256_GCM_SHA384:TLS_CHACHA20_POLY1305_SHA256',
honorCipherOrder: true,
secureOptions:
tls.constants.SSL_OP_NO_TLSv1 |
tls.constants.SSL_OP_NO_TLSv1_1 |
tls.constants.SSL_OP_NO_TLSv1_2,
};
if (config.certPath && config.keyPath) {
tlsOptions.cert = readFileSync(config.certPath);
tlsOptions.key = readFileSync(config.keyPath);
if (config.passphrase) tlsOptions.passphrase = config.passphrase;
}
const agent = new https.Agent({
...tlsOptions,
checkServerIdentity: (host, cert) => {
// Default Node.js validation
const defaultResult = tls.checkServerIdentity(host, cert);
if (defaultResult) return defaultResult;
// Optional: Certificate pinning validation
if (config.pinFingerprint) {
const fingerprint = createHash('sha256')
.update(cert.raw)
.digest('base64');
if (fingerprint !== config.pinFingerprint) {
return new Error('Certificate pin mismatch');
}
}
return undefined;
},
});
return agent;
}
// Usage example
const secureAgent = createSecureTlsAgent({
caPath: './certs/ca-bundle.pem',
rejectUnauthorized: true,
pinFingerprint: 'XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX',
});
https.get('https://api.production.internal/health', { agent: secureAgent }, (res) => {
console.log(`Status: ${res.statusCode}`);
}).on('error', (err) => {
console.error('TLS handshake failed:', err.message);
});
Architecture Decisions and Rationale
- TLS 1.3 enforcement: Eliminates legacy handshake complexity, removes RSA key exchange, and enforces forward secrecy by default.
- Explicit cipher ordering: Prevents downgrade attacks and ensures hardware acceleration paths are prioritized.
- Client-side
checkServerIdentity override: Allows integration with internal PKI, certificate pinning, or custom validation logic without disabling rejectUnauthorized.
- OCSP stapling + CRL fallback: Balances revocation accuracy with latency. Staging servers should validate stapled responses before production rollout.
- Session ticket key rotation: Maintains forward secrecy when 0-RTT is enabled. Keys must be rotated at least every 24 hours or per deployment.
Pitfall Guide
1. Enabling TLS 1.0/1.1 for "Compatibility"
Legacy protocols are vulnerable to POODLE, BEAST, SLOTH, and protocol downgrade attacks. Modern clients ignore them regardless of server advertisement.
Best practice: Disable at the network edge. If legacy clients exist, isolate them behind a dedicated endpoint with strict rate limiting and monitoring.
2. Incomplete Certificate Chain
Missing intermediate certificates cause validation failures on Android, iOS, and enterprise proxy environments. Browsers often cache intermediates, masking the issue during development.
Best practice: Always concatenate leaf + intermediates in server configuration. Validate chain completeness using openssl s_client -connect host:port -showcerts.
3. Static Certificate Management
Manual renewal causes expiration outages and breaks CI/CD velocity. Certificates rotated outside automation lack audit trails and rollback capability.
Best practice: Implement ACME automation with webhook validation. Store private keys in HSM or KMS. Enforce 90-day maximum validity for public endpoints.
4. HSTS Misconfiguration
Setting max-age too low provides no protection. Preloading without includeSubDomains leaves subdomains vulnerable. Incorrect preload submission triggers browser warnings.
Best practice: Start with max-age=300, monitor error rates, then escalate to 31536000. Submit to HSTS preload list only after 30 days of stable operation.
5. Disabling OCSP Stapling
Clients must query OCSP responders directly, increasing latency and exposing browsing patterns to third parties. Revocation checks fail silently when responders are unreachable.
Best practice: Enable stapling at the reverse proxy. Configure ssl_stapling_verify on and validate responder certificates. Fall back to CRL only if stapling fails.
6. Client-Side rejectUnauthorized: false in Production
Disabling certificate validation removes all TLS guarantees. Often introduced in development and accidentally deployed.
Best practice: Never disable validation in production. Use custom CA bundles or checkServerIdentity overrides for internal PKI. Enforce via linting and CI gates.
7. Over-Reliance on Framework Defaults
Express, Fastify, and cloud load balancers ship with permissive cipher lists and TLS 1.2 fallbacks. Defaults prioritize compatibility over security.
Best practice: Explicitly declare protocol and cipher constraints in configuration. Audit framework defaults against NIST SP 800-52 Rev 2 guidelines.
Production Bundle
Action Checklist
Decision Matrix
| Scenario | Recommended Approach | Why | Cost Impact |
|---|
| Public-facing API | TLS 1.3 only, ECDSA P-256, ACME automation | Maximizes security, minimizes handshake latency, reduces operational overhead | Low (cloud-native certs, automated rotation) |
| Internal mTLS service | TLS 1.3, private CA, certificate pinning, mutual auth | Ensures service identity, prevents lateral movement, enforces zero-trust | Medium (PKI infrastructure, key management) |
| Legacy client support | Isolated endpoint, TLS 1.2 ECDHE+AESGCM only, strict rate limiting | Contains vulnerability surface, preserves primary endpoint security | High (dual infrastructure, monitoring overhead) |
| High-throughput CDN | TLS 1.3, session tickets with key rotation, OCSP stapling | Optimizes 0-RTT resumption, reduces CPU per connection, maintains forward secrecy | Low (edge provider handles most config) |
Configuration Template
Nginx Production TLS Configuration
server {
listen 443 ssl http2;
server_name api.example.com;
# Certificate chain
ssl_certificate /etc/ssl/certs/leaf+chain.pem;
ssl_certificate_key /etc/ssl/private/leaf.key;
# Protocol enforcement
ssl_protocols TLSv1.3;
ssl_prefer_server_ciphers on;
# Cipher suite ordering
ssl_ciphers TLS_AES_128_GCM_SHA256:TLS_AES_256_GCM_SHA384:TLS_CHACHA20_POLY1305_SHA256;
# Session and handshake optimization
ssl_session_timeout 1d;
ssl_session_cache shared:TLS:10m;
ssl_session_tickets off;
# OCSP stapling
ssl_stapling on;
ssl_stapling_verify on;
resolver 8.8.8.8 8.8.4.4 valid=300s;
resolver_timeout 5s;
# HSTS
add_header Strict-Transport-Security "max-age=31536000; includeSubDomains; preload" always;
# Security headers
add_header X-Content-Type-Options nosniff always;
add_header X-Frame-Options DENY always;
}
TypeScript Server TLS Context (Node.js)
import https from 'https';
import { readFileSync } from 'fs';
import { createServer } from 'http';
const tlsOptions = {
key: readFileSync('./certs/server.key'),
cert: readFileSync('./certs/server+chain.pem'),
minVersion: 'TLSv1.3',
maxVersion: 'TLSv1.3',
ciphers: 'TLS_AES_128_GCM_SHA256:TLS_AES_256_GCM_SHA384:TLS_CHACHA20_POLY1305_SHA256',
honorCipherOrder: true,
secureOptions: 0x00000004, // SSL_OP_NO_COMPRESSION
requestCert: false, // Set true for mTLS
rejectUnauthorized: true,
};
const server = https.createServer(tlsOptions, (req, res) => {
res.writeHead(200, { 'Content-Type': 'text/plain' });
res.end('TLS 1.3 secure connection established.\n');
});
server.listen(443, () => {
console.log('Secure server running on port 443');
});
Quick Start Guide
- Generate test certificates: Run
openssl req -x509 -newkey ec -pkeyopt ec_paramgen_curve:prime256v1 -keyout server.key -out server.pem -days 90 -nodes -subj "/CN=localhost" for local validation.
- Configure server constraints: Apply the Nginx or TypeScript template above. Ensure
ssl_protocols TLSv1.3 and explicit cipher ordering are set.
- Validate handshake: Execute
openssl s_client -connect localhost:443 -tls1_3 -brief. Confirm protocol version, cipher suite, and chain completeness.
- Enable HSTS and scan: Add HSTS header, restart service, then run
testssl.sh localhost:443 or submit to SSL Labs. Verify A+ rating and zero warnings before production deployment.