Web Security

TLS Configuration Best Practices: Cipher Suites, Protocols, and Certificate Pinning

Drop TLS 1.0/1.1, remove weak ciphers, enable HSTS preloading and OCSP stapling — complete nginx ssl_conf_command and Go/Node.js TLS configuration examples.

March 9, 20266 min readShipSafer Team

TLS Configuration Best Practices: Cipher Suites, Protocols, and Certificate Pinning

TLS (Transport Layer Security) is the cryptographic foundation of web security. A poorly configured TLS stack enables downgrade attacks, decryption of traffic captured today and broken by quantum computers tomorrow, and man-in-the-middle attacks against clients that accept weak certificates. Getting TLS configuration right is not optional — it is table stakes.

TLS Protocol Versions

What to disable immediately

TLS 1.0 was published in 1999. It is vulnerable to BEAST, POODLE, and several other practical attacks. The PCI-DSS standard required disabling it by June 2018. There is no legitimate reason to support it.

TLS 1.1 is deprecated by RFC 8996 (2021). It lacks support for AEAD cipher suites and is not significantly stronger than TLS 1.0 in practice.

SSL 3.0 and below must never be enabled. They are completely broken.

What to require

TLS 1.2 is the minimum acceptable version for 2026. It supports AEAD cipher suites (AES-GCM, ChaCha20-Poly1305) and is widely supported across all modern clients. Some legacy enterprise and IoT clients still use 1.2.

TLS 1.3 is the current standard. It eliminates an entire class of cipher suite vulnerabilities by removing RSA key exchange and all CBC cipher suites. It reduces handshake round trips from two to one, improving latency. Enable it alongside TLS 1.2 for maximum compatibility.

Cipher Suite Configuration

Weak ciphers to eliminate

  • RC4 — statistically biased keystream; practically broken.
  • 3DES (SWEET32 attack) — 64-bit block cipher, vulnerable to birthday attacks.
  • NULL and EXPORT ciphers — provide no encryption or intentionally weak encryption.
  • CBC mode with RSA key exchange (in TLS 1.2) — vulnerable to BEAST, Lucky13, and POODLE variants. Prefer ECDHE + AEAD.
  • MD5 and SHA-1 in signatures — collision vulnerabilities; use SHA-256+.

Recommended cipher suites (TLS 1.2)

TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384
TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256
TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305_SHA256
TLS_DHE_RSA_WITH_AES_256_GCM_SHA384
TLS_DHE_RSA_WITH_AES_128_GCM_SHA256

All use ECDHE or DHE (ephemeral key exchange), providing perfect forward secrecy (PFS). If the server's private key is later compromised, past traffic cannot be decrypted.

TLS 1.3 cipher suites are non-negotiable by design — the spec defines only five, all of which are strong. You do not need to configure them explicitly.

nginx TLS Configuration

Modern nginx with ssl_conf_command (requires OpenSSL 1.1.1+):

server {
    listen 443 ssl;
    listen [::]:443 ssl;
    http2 on;

    ssl_certificate     /etc/ssl/certs/yourapp.com.crt;
    ssl_certificate_key /etc/ssl/private/yourapp.com.key;

    # Protocol versions
    ssl_protocols TLSv1.2 TLSv1.3;

    # TLS 1.2 cipher suites (TLS 1.3 ciphers are automatic)
    ssl_ciphers ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:DHE-RSA-AES128-GCM-SHA256:DHE-RSA-AES256-GCM-SHA384;
    ssl_prefer_server_ciphers off; # Let TLS 1.3 negotiate freely

    # Key exchange parameters
    ssl_ecdh_curve X25519:prime256v1:secp384r1;
    ssl_dhparam /etc/ssl/dhparam4096.pem; # Generated with: openssl dhparam -out dhparam4096.pem 4096

    # Session resumption (reduces handshake overhead)
    ssl_session_cache shared:SSL:10m;
    ssl_session_timeout 1d;
    ssl_session_tickets off; # Session tickets weaken PFS

    # OCSP stapling
    ssl_stapling on;
    ssl_stapling_verify on;
    ssl_trusted_certificate /etc/ssl/certs/chain.pem;
    resolver 8.8.8.8 1.1.1.1 valid=300s;
    resolver_timeout 5s;

    # HSTS
    add_header Strict-Transport-Security "max-age=31536000; includeSubDomains; preload" always;

    # Use ssl_conf_command for OpenSSL-level controls
    ssl_conf_command Options PrioritizeChaCha;
    ssl_conf_command Ciphersuites TLS_AES_256_GCM_SHA384:TLS_CHACHA20_POLY1305_SHA256:TLS_AES_128_GCM_SHA256;
}

# Redirect HTTP to HTTPS
server {
    listen 80;
    listen [::]:80;
    return 301 https://$host$request_uri;
}

Generate a strong DH parameter file (do this once, store the result):

openssl dhparam -out /etc/ssl/dhparam4096.pem 4096

OCSP Stapling

Online Certificate Status Protocol (OCSP) is how browsers verify a certificate has not been revoked. Without stapling, the browser makes a request to the CA's OCSP responder during each TLS handshake — adding latency and leaking browsing behavior to the CA.

With OCSP stapling, the server fetches the OCSP response and includes it in the TLS handshake. The browser gets revocation status without making an external request.

Enable it in nginx as shown above (ssl_stapling on). Verify it is working:

openssl s_client -connect yourapp.com:443 -status 2>/dev/null \
  | grep -A 17 'OCSP Response'

You should see OCSP Response Status: successful and Cert Status: Good.

HSTS Preloading

HSTS protects users who have previously visited your site. HSTS preloading goes further — browsers ship with a hardcoded list of domains that always use HTTPS, even on the very first request. This eliminates the window of vulnerability before a user's first HTTPS visit.

Requirements for preloading:

  1. Serve a valid HSTS header with max-age of at least 31536000.
  2. Include includeSubDomains.
  3. Include preload.
  4. All subdomains must be HTTPS-capable.
  5. Submit at hstspreload.org.

Once submitted, expect 1-3 months for inclusion in Chrome's list. Removal takes equally long, so be certain before preloading.

TLS Configuration in Node.js

import https from 'https';
import fs from 'fs';
import tls from 'tls';

const options: https.ServerOptions = {
  cert: fs.readFileSync('/etc/ssl/certs/yourapp.com.crt'),
  key: fs.readFileSync('/etc/ssl/private/yourapp.com.key'),
  ca: fs.readFileSync('/etc/ssl/certs/chain.pem'),

  // Protocol versions
  minVersion: 'TLSv1.2' as tls.SecureVersion,
  maxVersion: 'TLSv1.3' as tls.SecureVersion,

  // Cipher suites
  ciphers: [
    'TLS_AES_256_GCM_SHA384',         // TLS 1.3
    'TLS_CHACHA20_POLY1305_SHA256',   // TLS 1.3
    'TLS_AES_128_GCM_SHA256',         // TLS 1.3
    'ECDHE-RSA-AES256-GCM-SHA384',   // TLS 1.2
    'ECDHE-RSA-AES128-GCM-SHA256',   // TLS 1.2
    'ECDHE-RSA-CHACHA20-POLY1305',   // TLS 1.2
  ].join(':'),

  honorCipherOrder: true,
  ecdhCurve: 'X25519:P-256:P-384',

  // Disable session tickets for PFS
  sessionTimeout: 300,
};

const server = https.createServer(options, app);

Certificate Pinning

Certificate pinning causes a client to reject TLS connections unless the server presents a certificate matching a pre-stored fingerprint. It provides additional protection against CA compromise or mis-issuance.

HTTP Public Key Pinning (HPKP) — Deprecated

The Public-Key-Pins response header was the browser-level pinning mechanism. It is now deprecated and removed from major browsers due to abuse potential (misconfigured pins can permanently brick a site). Do not use it.

Pinning in Mobile and Server-to-Server Clients

Pinning remains valuable in native mobile apps and server-to-server HTTP clients where you control the client code.

In Node.js fetch/got for service-to-service calls:

import https from 'https';
import crypto from 'crypto';
import tls from 'tls';

const EXPECTED_PINS = new Set([
  'sha256/base64encodedPublicKeyHash1=',
  'sha256/base64encodedPublicKeyHash2=', // backup pin
]);

function computePin(cert: tls.PeerCertificate): string {
  const publicKey = cert.pubkey;
  const hash = crypto.createHash('sha256').update(publicKey).digest('base64');
  return `sha256/${hash}`;
}

const agent = new https.Agent({
  checkServerIdentity: (host, cert) => {
    const pin = computePin(cert);
    if (!EXPECTED_PINS.has(pin)) {
      return new Error(`Certificate pin mismatch for ${host}: ${pin}`);
    }
    return undefined;
  },
});

// Use agent in fetch calls to the pinned service

Always include at least two pins — one for your current certificate and one backup (a pre-generated key pair or your CA's key), so you can rotate without downtime.

Validating Your TLS Configuration

Use these tools to audit your configuration:

# testssl.sh — comprehensive local scanner
testssl.sh https://yourapp.com

# Qualys SSL Labs (online, rates A through F)
# https://www.ssllabs.com/ssltest/

# Check specific protocol support
openssl s_client -connect yourapp.com:443 -tls1   # Should fail
openssl s_client -connect yourapp.com:443 -tls1_1 # Should fail
openssl s_client -connect yourapp.com:443 -tls1_2 # Should succeed
openssl s_client -connect yourapp.com:443 -tls1_3 # Should succeed

Target an A+ rating on SSL Labs. The configuration examples in this guide achieve that rating for new deployments.

TLS configuration is a set-and-maintain task. Automate certificate renewal (via Certbot or your cloud provider's ACM), pin rotation calendaring, and quarterly SSL Labs scans to ensure your security posture does not quietly degrade.

Check Your Security Score — Free

See exactly how your domain scores on DMARC, TLS, HTTP headers, and 25+ other automated security checks in under 60 seconds.