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 offis 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:
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-nginxRequest 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.comAutomatic 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-hookto 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:
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.comCertificate 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:
- Once your domain is in the preload list, it can take months to remove it
- If a subdomain does not have HTTPS and you use
includeSubDomains, that subdomain becomes unreachable - 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
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.comtestssl.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 |
| HTTP → HTTPS 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.
Further reading in the knowledge base
These articles in the portal give you more background and practical context:
- APIs — the invisible glue of the internet
- SSL/TLS — why that padlock in your browser matters
- Encryption — the art of making things unreadable
- Password hashing — how websites store your password
- Penetration tests vs. vulnerability scans
You need an account to access the knowledge base. Log in or register.
Related security measures
These articles provide additional context and depth: