Why TLS 1.0 and 1.1 Are Dead
TLS 1.0 debuted in 1999 and TLS 1.1 followed in 2006. Both are now formally deprecated. The IETF published RFC 8996 in March 2021, officially marking them as obsolete. PCI DSS 3.1 banned TLS 1.0 in 2015. Every major browser — Chrome, Firefox, Safari, Edge — has dropped support. If your servers still accept these protocols, you are exposing clients to known vulnerabilities like POODLE (CVE-2014-3566), BEAST (CVE-2011-3389), and Lucky13 (CVE-2013-0169).
Run the OpsCheck TLS Scanner against your domain to see exactly which protocols and cipher suites your server advertises. The scanner checks TLS 1.0 through 1.3 and flags weak ciphers in one pass.
Before You Start: Audit Your Current State
Pick a representative set of production hosts — web servers, load balancers, mail servers, internal APIs — and inventory their TLS configurations. Don't guess. Use tools that connect to the actual listening port and negotiate the TLS handshake.
Scan with OpenSSL from CLI
# Test whether a server accepts TLS 1.0
openssl s_client -connect example.com:443 -tls1 < /dev/null 2>/dev/null | grep -c "CONNECTED"
# Test TLS 1.1
openssl s_client -connect example.com:443 -tls1_1 < /dev/null 2>/dev/null | grep -c "CONNECTED"
# Test TLS 1.2
openssl s_client -connect example.com:443 -tls1_2 < /dev/null 2>/dev/null | grep -c "CONNECTED"
If any of these return a CONNECTED line, that protocol version is enabled. You want TLS 1.0 and 1.1 to return zero hits.
Bulk-Check with the TLS Scanner
For scanning multiple endpoints, use the OpsCheck TLS Scanner to punch through the list. It reports protocol support, cipher suites, and certificate chain validity per endpoint. Batch mode saves hours compared to iterating openssl s_client manually.
Check HTTP Version Negotiation Too
HTTP/2 and HTTP/3 require TLS 1.2+ (h2) or TLS 1.3 (h3). If a server advertises HTTP/2 but allows TLS 1.0 fallback, you have a configuration gap. Verify with the OpsCheck HTTP Version Checker — it tells you which HTTP version was negotiated and whether ALPN advertised h2 correctly.
Real-World Troubleshooting: The Legacy Client Problem
A hosting company I worked with ran a cPanel cluster across 40 servers. Their billing portal required TLS 1.2, but the webmail interface still accepted TLS 1.0 because the Dovecot/Exim config shipped with older protocol defaults. Customers on Windows 7 systems could log into webmail but couldn't pay their invoices — same domain, different ports, inconsistent TLS policy.
The fix was three-pronged: (1) audit every port with nmap ssl-enum-ciphers, (2) standardise TLS minimums in a configuration management template, (3) set up monitoring to catch drift. They cut their TLS 1.0 exposure from 12 open ports down to zero in a weekend maintenance window.
Step 1: Configure TLS 1.2/1.3 on Apache
Find your SSL configuration block — typically in /etc/apache2/sites-available/default-ssl.conf or /etc/httpd/conf.d/ssl.conf. Set the SSLProtocol directive explicitly:
SSLProtocol -all +TLSv1.2 +TLSv1.3
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 on
The -all prefix disables everything, then you explicitly add back what you want. This prevents config merge accidents where a distro default re-enables old protocols. After editing, validate and restart:
apachectl configtest
systemctl restart apache2 # Debian/Ubuntu
systemctl restart httpd # RHEL/CentOS
Step 2: Configure TLS 1.2/1.3 on Nginx
Nginx uses ssl_protocols and ssl_ciphers directives in the server block or http block:
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;
ssl_prefer_server_ciphers on;
Reload nginx with nginx -t && systemctl reload nginx. Nginx 1.19.4+ supports TLS 1.3 out of the box; earlier versions need OpenSSL 1.1.1+ compiled into the binary.
Step 3: HAProxy
HAProxy's bind line controls TLS versions. In your frontend or listen block:
bind :::443 v4v6 ssl crt /etc/ssl/example.pem alpn h2,http/1.1 ssl-min-ver TLSv1.2
ssl-default-server-options ssl-min-ver TLSv1.2
HAProxy 2.0+ also supports ssl-max-ver TLSv1.3 if you need to cap at 1.3 for audit purposes. Validate with haproxy -c -f /etc/haproxy/haproxy.cfg.
Step 4: Postfix and Dovecot for Mail Servers
Mail servers are the most commonly forgotten TLS endpoints. SMTP on port 25 still negotiates STARTTLS with opportunistic encryption, and many default configs leave TLS 1.0 enabled for backward compatibility with ancient MTAs.
Postfix — in /etc/postfix/main.cf:
smtpd_tls_mandatory_protocols = !SSLv2, !SSLv3, !TLSv1, !TLSv1.1
smtpd_tls_protocols = !SSLv2, !SSLv3, !TLSv1, !TLSv1.1
smtp_tls_mandatory_protocols = !SSLv2, !SSLv3, !TLSv1, !TLSv1.1
smtp_tls_protocols = !SSLv2, !SSLv3, !TLSv1, !TLSv1.1
smtpd_tls_mandatory_ciphers = high
smtpd_tls_eecdh_grade = ultra
Restart with systemctl restart postfix. Verify with postconf -n | grep tls.
Dovecot — in /etc/dovecot/conf.d/10-ssl.conf:
ssl_min_protocol = TLSv1.2
ssl_cipher_list = ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256
Restart with systemctl restart dovecot.
Step 5: MySQL and PostgreSQL TLS
Databases often run on internal networks so admins skip TLS entirely. If you do enforce TLS on database connections, make sure the configured minimum isn't TLS 1.0.
MySQL 8.0 — in /etc/mysql/mysql.conf.d/mysqld.cnf:
[mysqld]
tls_version = TLSv1.2,TLSv1.3
PostgreSQL — in /var/lib/pgsql/data/postgresql.conf:
ssl_min_protocol_version = 'TLSv1.2'
Set ssl = on and restart. Clients that can't negotiate TLS 1.2 will get a connection error instead of silently falling back to a weak cipher.
Validating Your Work
After changing configs, don't assume it worked — verify.
Verify with OpenSSL
# Should succeed with TLS 1.2
openssl s_client -connect example.com:443 -tls1_2 < /dev/null 2>/dev/null | grep "Protocol"
# Should fail with TLS 1.0
openssl s_client -connect example.com:443 -tls1 < /dev/null 2>/dev/null
echo "Exit code: $?" # Non-zero = handshake rejected
Verify with the OpsCheck SSL Checker
Use the OpsCheck SSL Certificate Checker to run a complete handshake test. It reports the negotiated protocol, cipher suite, certificate chain, and expiry dates. Run it before and after your migration to confirm the protocol version changed.
Check HTTP Headers for HSTS
Once TLS 1.2+ is enforced, add the Strict-Transport-Security header to tell browsers to never connect over plain HTTP:
# Apache
Header always set Strict-Transport-Security "max-age=63072000; includeSubDomains; preload"
# Nginx
add_header Strict-Transport-Security "max-age=63072000; includeSubDomains; preload";
Test with curl -I https://example.com 2>/dev/null | grep -i strict.
What About Cipher Suites?
Disabling old protocols is only half the battle. You also need modern cipher suites. TLS 1.2 has cipher suites from 2008 that are still secure (GCM-based AEAD ciphers) and others that are weak (CBC-mode ciphers vulnerable to Lucky13).
Good TLS 1.2 ciphers: ECDHE-ECDSA-AES128-GCM-SHA256, ECDHE-RSA-AES128-GCM-SHA256, ECDHE-ECDSA-AES256-GCM-SHA384, ECDHE-RSA-AES256-GCM-SHA384.
TLS 1.3 simplifies this — it mandates forward-secrecy AEAD ciphers and drops RSA key exchange, CBC mode, and static DH entirely.
Rollback Plan
Before touching production, write down the original config values. Keep a terminal open with the old config ready to paste back. Better: use config management (Ansible, Puppet, Salt) so every change is version-controlled and revertible with a single commit revert.
# Snapshot current configs before editing
for f in /etc/apache2 /etc/nginx /etc/postfix /etc/dovecot; do
[ -d "$f" ] && tar czf "backup-$(date +%Y%m%d-%H%M%S)-$(basename $f).tgz" "$f"
done
Monitoring for Drift
TLS configuration drifts. A junior admin restores a backup that had old settings. A package update resets a config file. Monitor continuously:
# Cron-based TLS protocol check (runs weekly)
#!/bin/bash
HOSTS=("example.com:443" "mail.example.com:993")
for host in "${HOSTS[@]}"; do
if openssl s_client -connect "$host" -tls1 < /dev/null 2>/dev/null; then
echo "WARNING: $host still accepts TLS 1.0" | mail -s "TLS regression detected" ops@example.com
fi
done
Or run the OpsCheck TLS Scanner on a schedule through your monitoring pipeline.
Summary Checklist
- Audit all TLS endpoints with the TLS Scanner and OpenSSL CLI
- Disable TLS 1.0 and 1.1 in web server configs (Apache, Nginx, HAProxy)
- Update mail server configs (Postfix, Dovecot) — these are the most missed
- Update database TLS minimums if you enforce encrypted connections
- Verify with OpsCheck SSL Checker — confirm protocol version changed
- Add HSTS header to prevent downgrade attacks
- Set up monitoring to catch future regressions
- Document the new baseline in your runbook
TLS migration isn't glamorous, but leaving old protocols enabled is an open invitation to downgrade attacks. The good news: it is purely a configuration change. No code to write, no certificates to reissue, no downtime if you test first. Run the audit, update the configs, verify with the scanner, and close the door on TLS 1.0 for good.