jan-karel.com
Home / Security Measures / Web Security / TLS/SSL Configuration

TLS/SSL Configuration

TLS/SSL Configuration

TLS/SSL Configuration

Encryption Without Excuses

Web risk is rarely mysterious. It usually lies in predictable mistakes that remain under time pressure.

For TLS/SSL Configuration the core is a modern TLS baseline, tight certificate management and no weak exceptions.

That makes security less of a separate check after the fact and more of a standard quality of your product.

Immediate measures (15 minutes)

Why this matters

The core of TLS/SSL Configuration is risk reduction in practice. Technical context supports the measure selection, but implementation and assurance are central.

TLS versions

What to use

Version Status Action
TLS 1.3 Recommended Enable, prefer
TLS 1.2 Acceptable Enable for backward compatibility
TLS 1.1 Deprecated Disable
TLS 1.0 Deprecated Disable
SSL 3.0 Insecure (POODLE) Disable
SSL 2.0 Insecure Disable

TLS 1.3 is faster (1-RTT handshake, 0-RTT resumption), more secure (only modern cipher suites), and simpler (no legacy options to misconfigure). All modern browsers support it.

TLS 1.2 is still needed for older clients, but configure it with only secure cipher suites (see §13.2).

Why disable TLS 1.0 and 1.1? Both versions support weak cipher suites and are vulnerable to attacks such as BEAST and Lucky Thirteen. PCI DSS has required at least TLS 1.2 since 2018. All major browsers have disabled TLS 1.0/1.1 since 2020.

Cipher suites

Recommended cipher suites

TLS 1.3 (cipher suites are fixed, no configuration needed):

TLS_AES_256_GCM_SHA384
TLS_CHACHA20_POLY1305_SHA256
TLS_AES_128_GCM_SHA256

TLS 1.2 (select only secure combinations):

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

What to avoid

To avoid Reason
RC4 Broken (multiple attacks)
3DES Broken (Sweet32)
DES Far too weak (56-bit)
CBC-mode ciphers Vulnerable to padding oracle attacks
RSA key exchange (no ECDHE) No forward secrecy
NULL ciphers No encryption
EXPORT ciphers Intentionally weakened (40/56-bit)
MD5-based ciphers Broken hash

Forward secrecy

Forward secrecy (via ECDHE) means that a compromised server key does not lead to decryption of previously recorded traffic. Always use ECDHE-based cipher suites.

nginx TLS configuration

Complete copy-paste block for a secure nginx TLS configuration:

# /etc/nginx/snippets/ssl-params.conf

# Protocols — only TLS 1.2 and 1.3
ssl_protocols TLSv1.2 TLSv1.3;

# Cipher suites — server determines the order
ssl_prefer_server_ciphers on;
ssl_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';

# ECDH curve
ssl_ecdh_curve X25519:secp384r1;

# Session caching — reduces handshake overhead
ssl_session_cache shared:TLS:10m;
ssl_session_timeout 1d;
ssl_session_tickets off;  # Tickets undermine forward secrecy

# OCSP Stapling
ssl_stapling on;
ssl_stapling_verify on;
resolver 1.1.1.1 8.8.8.8 valid=300s;
resolver_timeout 5s;

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

Use in your server block:

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

    ssl_certificate /etc/letsencrypt/live/example.com/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/example.com/privkey.pem;
    ssl_trusted_certificate /etc/letsencrypt/live/example.com/chain.pem;

    include snippets/ssl-params.conf;

    # Redirect HTTP to HTTPS
    # (in a separate server block)
}

server {
    listen 80;
    listen [::]:80;
    server_name example.com;
    return 301 https://$host$request_uri;
}

Note: ssl_session_tickets off is important. Session tickets use a symmetric key stored on the server. If that key leaks, all traffic established with it can be decrypted — forward secrecy is then gone.

Apache TLS configuration

Activate with:

sudo a2enmod ssl headers
sudo a2enconf ssl-params
sudo systemctl reload apache2

VirtualHost configuration:

<VirtualHost *:443>
    ServerName example.com

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

    Include conf-available/ssl-params.conf
</VirtualHost>

<VirtualHost *:80>
    ServerName example.com
    Redirect permanent / https://example.com/
</VirtualHost>

Let's Encrypt / ACME

Let's Encrypt offers free, automated TLS certificates via the ACME protocol.

Installation (certbot)

# Debian/Ubuntu
sudo apt install certbot python3-certbot-nginx

# CentOS/RHEL
sudo dnf install certbot python3-certbot-nginx

Request a certificate

# nginx — certbot configures automatically
sudo certbot --nginx -d example.com -d www.example.com

# Apache
sudo certbot --apache -d example.com -d www.example.com

# Standalone (if you're not running a web server)
sudo certbot certonly --standalone -d example.com

# DNS challenge (for wildcard certs)
sudo certbot certonly --manual --preferred-challenges dns -d '*.example.com' -d example.com

Automatic renewal

# Test renewal
sudo certbot renew --dry-run

# Certbot automatically installs a timer/cron:
sudo systemctl list-timers | grep certbot

# Manually add cron if missing:
# /etc/cron.d/certbot
0 0,12 * * * root certbot renew --quiet --deploy-hook "systemctl reload nginx"

Important: Use --deploy-hook to reload nginx/Apache after renewal. Without a reload the web server uses the old certificate until the next restart.

Wildcard certificates

Wildcard certificates (*.example.com) require DNS validation:

sudo certbot certonly \
  --manual \
  --preferred-challenges dns \
  -d '*.example.com' \
  -d example.com

Certbot asks you to create an _acme-challenge TXT record in your DNS. For automation, use a DNS plugin:

# Example: Cloudflare
sudo apt install python3-certbot-dns-cloudflare
sudo certbot certonly \
  --dns-cloudflare \
  --dns-cloudflare-credentials /etc/letsencrypt/cloudflare.ini \
  -d '*.example.com' -d example.com

Certificate management

Lifecycle

Request → Validation → Issuance → Installation → Monitoring → Renewal → ...

Let's Encrypt certificates are valid for 90 days. Renew at least 30 days before the expiry date.

Monitoring for expiry

# Check expiry date of a certificate
echo | openssl s_client -connect example.com:443 -servername example.com 2>/dev/null \
  | openssl x509 -noout -dates

# One-liner: days until expiry
echo | openssl s_client -connect example.com:443 -servername example.com 2>/dev/null \
  | openssl x509 -noout -enddate \
  | cut -d= -f2 \
  | xargs -I{} bash -c 'echo $(( ($(date -d "{}" +%s) - $(date +%s)) / 86400 )) days remaining'

Set up alerts for certificates expiring within 14 days. Use tools such as:

  • certbot (built-in renewal)
  • cert-manager (Kubernetes)
  • AWS Certificate Manager (ACM, automatic renewal)

Certificate Transparency (CT) logs

CT logs are public registers of all issued certificates. Monitor them to detect unauthorized certificates for your domain:

  • https://crt.sh/?q=example.com — search all issued certs
  • Facebook CT Monitor — free alerting
  • Censys — search and monitor certificates

HSTS in detail

Configuration

Strict-Transport-Security: max-age=31536000; includeSubDomains; preload
Parameter Effect Recommendation
max-age Browser remembers HTTPS requirement for X seconds 31536000 (1 year)
includeSubDomains Also applies to all subdomains Yes, if all subdomains have HTTPS
preload Permission for inclusion in browser preload list Yes, after thorough testing

Risks

HSTS preload is a one-way street:

  1. Once your domain is in the preload list, it can take months to remove it
  2. If a subdomain does not have HTTPS and you use includeSubDomains, that subdomain becomes unreachable
  3. An error in the HSTS configuration can make your site unreachable

Safe rollout

Step 1: max-age=300 (5 minutes) — test
Step 2: max-age=86400 (1 day) — monitor
Step 3: max-age=604800 (1 week) — stable?
Step 4: max-age=2592000 (30 days) — still good?
Step 5: max-age=31536000; includeSubDomains — production
Step 6: max-age=31536000; includeSubDomains; preload — submit to hstspreload.org

OCSP Stapling

OCSP (Online Certificate Status Protocol) checks whether a certificate has been revoked. Without stapling the browser must query the OCSP server itself — that is slow and a privacy issue (the CA can see which sites you visit).

With OCSP stapling the server periodically fetches an OCSP response and sends it along in the TLS handshake. Faster and more privacy-friendly.

nginx

ssl_stapling on;
ssl_stapling_verify on;
ssl_trusted_certificate /etc/letsencrypt/live/example.com/chain.pem;
resolver 1.1.1.1 8.8.8.8 valid=300s;

Apache

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

Testing

# Check if OCSP stapling is working
echo | openssl s_client -connect example.com:443 -servername example.com -status 2>/dev/null \
  | grep -A 5 "OCSP Response"

Certificate pinning

Certificate pinning restricts which certificates or CAs are valid for your domain. This protects against compromised CAs.

When to use

  • Mobile apps with their own backend: pin the leaf certificate or the intermediate CA
  • API clients with a known server: pin the public key (SPKI hash)

When not to use

  • Websites in the browser: pinning has been removed from Chrome and Firefox (HPKP deprecated). Use Certificate Transparency monitoring instead
  • Public APIs with many clients: too complex to manage

Example: Python requests with pinning

import hashlib
import ssl
import requests
from requests.adapters import HTTPAdapter

class PinningAdapter(HTTPAdapter):
    PINS = {
        b'\x2b\x06\x01...',  # SHA-256 of SPKI
    }

    def send(self, request, **kwargs):
        # Implementation via ssl context verify
        return super().send(request, **kwargs)

Advice: For most websites certificate pinning is too risky and too complex. Rely on Certificate Transparency monitoring and HSTS instead of pinning.

Test tools

testssl.sh

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

# Full report
./testssl.sh example.com

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

# Vulnerabilities only
./testssl.sh --vulnerable example.com

testssl.sh is the most complete CLI tool for TLS analysis. It tests protocols, cipher suites, certificates, vulnerabilities (Heartbleed, ROBOT, CRIME, etc.) and best practices.

SSL Labs

Test your configuration online via https://www.ssllabs.com/ssltest/. Goal: A+ rating. Requirements for A+:

  • TLS 1.2+ with strong cipher suites
  • HSTS with max-age >= 6 months
  • No known vulnerabilities

openssl s_client

# Basic connection test
openssl s_client -connect example.com:443 -servername example.com

# Test specific TLS version
openssl s_client -connect example.com:443 -tls1_2
openssl s_client -connect example.com:443 -tls1_3

# View certificate chain
openssl s_client -connect example.com:443 -showcerts

# Test cipher suite
openssl s_client -connect example.com:443 -cipher 'ECDHE-RSA-AES256-GCM-SHA384'

Mozilla SSL Configuration Generator

The Mozilla SSL Configuration Generator generates secure configurations for nginx, Apache, HAProxy, and other servers. Available at:

https://ssl-config.mozilla.org/

Three profiles: - Modern: TLS 1.3 only — for new deployments - Intermediate: TLS 1.2 + 1.3 — recommended for most servers - Old: TLS 1.0+ — only if you need to support old clients (avoid this)

Common mistakes

Mistake Risk Solution
Expired certificate Browser shows warning, users click away Automatic renewal (certbot) + monitoring
Self-signed in production No chain of trust, MitM possible Use Let's Encrypt (free)
Mixed content HTTP resources on HTTPS page, lock icon disappears Use relative URLs or // protocol
TLS 1.0/1.1 still enabled Vulnerable to known attacks Disable: ssl_protocols TLSv1.2 TLSv1.3
Weak cipher suites Brute-forceable encryption Use only ECDHE + AES-GCM/ChaCha20
No forward secrecy Stored traffic decryptable later Use ECDHE, avoid static RSA
SSL compression (CRIME) CRIME attack: session tokens leak SSLCompression off / default off in nginx
No HSTS SSL stripping attack possible Set Strict-Transport-Security header
Session tickets enabled Forward secrecy undermined ssl_session_tickets off
Incomplete certificate chain Some clients do not trust the cert Use fullchain.pem, not just cert.pem
Wildcard everywhere Too broad: applies to all subdomains Use specific certs where possible

Resolving mixed content

<!-- WRONG — HTTP resource on HTTPS page -->
<img src="http://cdn.example.com/logo.png">
<script src="http://cdn.example.com/app.js"></script>

<!-- CORRECT — protocol-relative or HTTPS -->
<img src="https://cdn.example.com/logo.png">
<script src="https://cdn.example.com/app.js"></script>

<!-- ALSO CORRECT — relative path if same domain -->
<img src="/static/logo.png">

Use CSP to detect and block mixed content:

Content-Security-Policy: upgrade-insecure-requests;

This directive automatically upgrades HTTP requests to HTTPS. Useful as a migration measure, but not as a permanent solution — fix the URLs.

Checklist

Measure Value Priority
TLS versions Only 1.2 and 1.3 Critical
Cipher suites ECDHE + AES-GCM / ChaCha20 Critical
Forward secrecy ECDHE enabled Critical
Certificate Valid, not self-signed, complete chain Critical
Auto-renewal certbot timer/cron active Critical
HSTS max-age=31536000; includeSubDomains High
OCSP stapling Enabled High
Session tickets Disabled High
SSL compression Disabled High
HTTPHTTPS redirect 301 redirect on port 80 Critical
Mixed content No HTTP resources on HTTPS pages High
CT monitoring Alerts for unexpected certificates Medium
SSL Labs score A+ Recommended

TLS configuration is one of the few areas in security where you can truly say: "do this, copy this configuration, and you're done." There are no edge cases, no "it depends", no subtle nuances. Disable TLS 1.0 and 1.1. Use only ECDHE cipher suites. Enable HSTS. Automate your certificates. Test with SSL Labs. Done.

And yet. And yet at every assessment we find servers with TLS 1.0, RC4 cipher suites, expired certificates, and no HSTS. We find webshops processing credit card data over a connection that a cryptography student can break in an afternoon.

It is not as if the information is not available. Mozilla has a configuration generator that literally outputs the rules for you. Let's Encrypt has made certificates free and automated. testssl.sh tells you in a second what is wrong. Everything is there. You just have to do it.

But "it's on the roadmap." Next to the security headers from the previous chapter. And the input validation from the chapter before that. Three free measures, three times "it's on the backlog." And we wonder why the average data breach notification still takes 277 days to discover.

Summary

Configure TLS with only version 1.2 and 1.3, use ECDHE cipher suites for forward secrecy, automate certificate management with Let's Encrypt, and enable HSTS and OCSP stapling. Test with testssl.sh or SSL Labs for an A+ rating. This is not a complex task — it is an afternoon project that protects your server for years.

Op de hoogte blijven?

Ontvang maandelijks cybersecurity-inzichten in je inbox.

← Web Security ← Home