Encryption in Transit: TLS Configuration

Transport Layer Security (TLS) is the protocol that keeps your data safe as it travels across networks. Every HTTPS connection, every secure API call, every encrypted email relay depends on TLS doing...

Key Insights

  • TLS 1.3 should be your default protocol—it’s faster, more secure, and eliminates entire categories of vulnerabilities present in older versions
  • Forward secrecy through ECDHE cipher suites ensures that compromising your private key doesn’t expose past communications
  • Automated certificate management with tools like Certbot eliminates the most common cause of TLS failures: expired certificates

Introduction to TLS and Why It Matters

Transport Layer Security (TLS) is the protocol that keeps your data safe as it travels across networks. Every HTTPS connection, every secure API call, every encrypted email relay depends on TLS doing its job correctly. Get it wrong, and you’re either leaking sensitive data or breaking compatibility with legitimate clients.

The evolution from SSL 3.0 through TLS 1.0, 1.1, 1.2, and now 1.3 represents decades of cryptographic improvements and vulnerability patches. SSL and TLS 1.0/1.1 are officially deprecated. If you’re still supporting them, you’re actively choosing to be vulnerable to attacks like POODLE and BEAST.

TLS 1.3, finalized in 2018, removed broken features, simplified the handshake, and made forward secrecy mandatory. There’s no good reason to avoid it in 2024.

TLS Handshake Explained

Understanding the handshake helps you debug connection failures and make informed configuration decisions. Here’s the simplified flow:

  1. Client Hello: Client sends supported TLS versions, cipher suites, and a random value
  2. Server Hello: Server selects protocol version and cipher suite, sends its random value
  3. Certificate: Server sends its certificate chain
  4. Key Exchange: Both parties establish shared secrets using asymmetric cryptography
  5. Finished: Both sides verify the handshake integrity and switch to encrypted communication

TLS 1.3 collapses this into fewer round trips, reducing latency significantly.

You can observe the handshake using OpenSSL’s debug output:

openssl s_client -connect example.com:443 -state -debug 2>&1 | grep -E "(SSL_connect|Cipher|Protocol)"

For more detailed analysis:

openssl s_client -connect example.com:443 -msg <<< "Q" 2>&1 | head -50

This shows you the actual messages exchanged. You’ll see ClientHello, ServerHello, Certificate, and Finished messages in sequence. When debugging connection failures, this output tells you exactly where the handshake breaks down.

Configuring TLS for Web Servers

Nginx Configuration

Here’s a production-ready Nginx TLS configuration:

server {
    listen 443 ssl http2;
    listen [::]:443 ssl http2;
    server_name example.com;

    # Certificate files
    ssl_certificate /etc/letsencrypt/live/example.com/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/example.com/privkey.pem;

    # Protocol versions - TLS 1.2 and 1.3 only
    ssl_protocols TLSv1.2 TLSv1.3;

    # Cipher suites - TLS 1.3 ciphers are configured automatically
    # These are for TLS 1.2 fallback
    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;
    ssl_prefer_server_ciphers off;

    # OCSP stapling
    ssl_stapling on;
    ssl_stapling_verify on;
    ssl_trusted_certificate /etc/letsencrypt/live/example.com/chain.pem;
    resolver 8.8.8.8 8.8.4.4 valid=300s;
    resolver_timeout 5s;

    # Session configuration
    ssl_session_timeout 1d;
    ssl_session_cache shared:SSL:50m;
    ssl_session_tickets off;

    # Security headers
    add_header Strict-Transport-Security "max-age=63072000" always;
}

Note that ssl_prefer_server_ciphers off is intentional for TLS 1.3—clients generally make better cipher choices now, and this allows them to prefer ChaCha20 on mobile devices where it’s faster than AES without hardware acceleration.

Apache Configuration

The equivalent Apache configuration:

<VirtualHost *:443>
    ServerName example.com

    SSLEngine on
    SSLCertificateFile /etc/letsencrypt/live/example.com/fullchain.pem
    SSLCertificateKeyFile /etc/letsencrypt/live/example.com/privkey.pem

    # Protocol versions
    SSLProtocol -all +TLSv1.2 +TLSv1.3

    # Cipher suites
    SSLCipherSuite 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
    SSLHonorCipherOrder off

    # OCSP stapling
    SSLUseStapling on
    SSLStaplingCache "shmcb:logs/ssl_stapling(32768)"

    # HSTS
    Header always set Strict-Transport-Security "max-age=63072000"
</VirtualHost>

Certificate Management Best Practices

Let’s Encrypt changed the game for certificate management. There’s no excuse for expired certificates when automation is this simple.

Install Certbot and obtain your initial certificate:

# Install Certbot (Debian/Ubuntu)
apt update && apt install certbot python3-certbot-nginx

# Obtain certificate with automatic Nginx configuration
certbot --nginx -d example.com -d www.example.com

# Or obtain certificate only (manual configuration)
certbot certonly --webroot -w /var/www/html -d example.com

Certbot installs a systemd timer for automatic renewal. Verify it’s active:

systemctl status certbot.timer

For custom renewal hooks (like reloading Nginx), create a deploy hook:

#!/bin/bash
# /etc/letsencrypt/renewal-hooks/deploy/reload-nginx.sh

systemctl reload nginx

Make it executable:

chmod +x /etc/letsencrypt/renewal-hooks/deploy/reload-nginx.sh

Test the renewal process without actually renewing:

certbot renew --dry-run

Private key protection is critical. Your private keys should have restricted permissions:

chmod 600 /etc/letsencrypt/live/example.com/privkey.pem
chown root:root /etc/letsencrypt/live/example.com/privkey.pem

Hardening Cipher Suites and Protocol Versions

Cipher suite selection involves trade-offs between security and compatibility. Here are three profiles:

Modern (TLS 1.3 only, maximum security)

ssl_protocols TLSv1.3;
# TLS 1.3 cipher suites are fixed and secure by default

This breaks compatibility with older clients but provides the strongest security.

ssl_protocols TLSv1.2 TLSv1.3;
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;

This supports clients from roughly 2014 onward while maintaining forward secrecy.

Legacy (When you must support old clients)

ssl_protocols TLSv1.2 TLSv1.3;
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;

Never include RC4, 3DES, or export ciphers. Never enable SSLv3 or TLS 1.0/1.1.

Testing and Validating TLS Configuration

After configuration changes, validate your setup before considering it production-ready.

Quick OpenSSL test

# Test connection and show certificate
openssl s_client -connect example.com:443 -servername example.com <<< "Q"

# Test specific TLS version
openssl s_client -connect example.com:443 -tls1_3 <<< "Q"

# Verify TLS 1.1 is rejected (should fail)
openssl s_client -connect example.com:443 -tls1_1 <<< "Q"

Comprehensive testing with testssl.sh

# Install testssl.sh
git clone --depth 1 https://github.com/drwetter/testssl.sh.git
cd testssl.sh

# Run full scan
./testssl.sh example.com

# Quick scan focusing on vulnerabilities
./testssl.sh --vulnerable example.com

# Check cipher suites only
./testssl.sh --cipher-per-proto example.com

SSL Labs

For public-facing servers, run your domain through SSL Labs. Aim for an A+ rating. Common issues that prevent A+:

  • Missing HSTS header
  • Supporting TLS 1.0/1.1
  • Weak cipher suites in the list
  • Incomplete certificate chain

Common Pitfalls and Troubleshooting

Certificate chain problems

The most common TLS issue is an incomplete certificate chain. Your server must send intermediate certificates, not just the leaf certificate.

Diagnose chain issues:

# Show full certificate chain
openssl s_client -connect example.com:443 -servername example.com -showcerts <<< "Q"

# Verify chain against system trust store
openssl s_client -connect example.com:443 -servername example.com -verify_return_error <<< "Q"

If you see “unable to verify the first certificate,” you’re missing intermediates. With Let’s Encrypt, use fullchain.pem, not cert.pem.

Mixed content warnings

After enabling HTTPS, browsers block HTTP resources loaded on HTTPS pages. Audit your site:

# Find HTTP references in your codebase
grep -r "http://" --include="*.html" --include="*.js" --include="*.css" .

Use protocol-relative URLs (//example.com/resource) or HTTPS URLs exclusively.

HSTS considerations

HTTP Strict Transport Security tells browsers to always use HTTPS. Once set, you can’t easily go back to HTTP.

Start with a short max-age during testing:

add_header Strict-Transport-Security "max-age=300" always;

Once confident, increase to two years and add preload eligibility:

add_header Strict-Transport-Security "max-age=63072000; includeSubDomains; preload" always;

Debugging connection failures

When clients can’t connect, gather information systematically:

# Check what the server actually offers
openssl s_client -connect example.com:443 -servername example.com -cipher 'ALL' <<< "Q" 2>&1 | grep -E "(Cipher|Protocol)"

# Test from client's perspective with specific cipher
openssl s_client -connect example.com:443 -cipher 'ECDHE-RSA-AES128-GCM-SHA256' <<< "Q"

# Check certificate dates
echo | openssl s_client -connect example.com:443 2>/dev/null | openssl x509 -noout -dates

TLS configuration isn’t set-and-forget. Cryptographic best practices evolve, and your configuration should evolve with them. Schedule quarterly reviews of your TLS setup, re-run SSL Labs tests, and stay informed about newly discovered vulnerabilities. The effort pays dividends in both security and user trust.

Liked this? There's more.

Every week: one practical technique, explained simply, with code you can use immediately.