# TLS 1.0 and 1.1: turning legacy protocols off without breaking anyone

> Browsers dropped TLS 1.0 and 1.1 in 2020 and PCI DSS bans them, yet many servers still accept them. See who still uses them, set a TLS 1.2 minimum on nginx, Apache, Caddy and Cloudflare, and prove it.

Updated 2026-09-25 · Security · HTML version: https://getreport.app/guides/tls-1-0-and-1-1-turning-legacy-protocols-off

A server that still accepts TLS 1.0 is like a shop that keeps a broken back door because one customer in 2012 used it. Every browser stopped using the old protocols in 2020, the standards body retired them in 2021, and the card industry has banned them since 2018. Turning them off is two lines of configuration; the only real work is confirming that nobody you care about still depends on them. This guide covers both halves.

## Quick answer

- Set the minimum to TLS 1.2 and enable 1.3. nginx: `ssl_protocols TLSv1.2 TLSv1.3;`. Apache: `SSLProtocol -all +TLSv1.2 +TLSv1.3`. Caddy: already the default. Cloudflare: SSL/TLS, Edge Certificates, Minimum TLS Version, 1.2.
- Before you do, log the negotiated protocol for a week and look for `TLSv1` and `TLSv1.1` entries. Modern browsers, phones and crawlers will not be among them.
- Drop the old cipher suites at the same time (RC4, 3DES, CBC-mode suites); keep the AEAD suites (AES-GCM, ChaCha20-Poly1305).
- Prove it: `openssl s_client -tls1_1` must fail against your host; the [SSL / TLS checker](https://getreport.app/tools/ssl-check) reports "The server only accepts TLS 1.2 or newer".

## Why turning off TLS 1.0 and 1.1 matters

### They are broken, and officially dead

TLS 1.0 dates from 1999 and 1.1 from 2006. Both depend on constructions that have since been attacked in practice: CBC-mode ciphers with a predictable initialisation vector (BEAST), MD5 and SHA-1 inside the handshake, no support at all for the authenticated (AEAD) ciphers that make TLS 1.2 and 1.3 safe. RC4, the usual workaround for the CBC problems, turned out to be weaker still. A server that offers 1.0 lets an attacker who can tamper with the connection *downgrade* to it, and then attack the weak protocol rather than the strong one the client would have preferred.

The formal end came in three steps. Chrome 84, Firefox 78, Safari and Edge removed TLS 1.0 and 1.1 in 2020; a visitor with a current browser cannot use them even if your server offers them. The IETF published RFC 8996 in March 2021, which deprecates both versions and forbids their use in new protocols. And PCI DSS, the rules for anyone who takes card payments, has prohibited "SSL and early TLS" since 30 June 2018: a shop that accepts TLS 1.0 fails its quarterly scan.

### Why 1.3 as well as 1.2

TLS 1.2 is the floor. TLS 1.3 (2018) is faster and simpler: the handshake takes one round trip instead of two, so the first byte arrives sooner on every new connection, and it removed the weak options outright (RSA key exchange, CBC, RC4, SHA-1, compression, renegotiation) so there is much less to misconfigure. HTTP/2 requires TLS 1.2 or better with a modern cipher, and HTTP/3 requires 1.3, so the protocol setting and [HTTP/2 and HTTP/3](https://getreport.app/guides/http2-and-http3) go together.

### Who still needs the old versions

Almost nobody, but "almost" is why you check your logs first. Clients that cannot speak TLS 1.2:

- Android 4.3 and older (2013); Android 4.4 supports 1.2 but some apps of that era did not enable it.
- Internet Explorer 10 and older on Windows 7, and everything on Windows XP. IE 11 on Windows 7 has TLS 1.2 on by default.
- Java 6, and Java 7 with its default client settings.
- Old payment terminals, printers, smart TVs, cameras and other embedded devices that call a web service and were never updated.
- Scripts on very old servers: `curl` and `wget` on distributions from before 2014.

For a public website the share of such visitors is a rounding error, and most of them already cannot load your site's fonts, scripts or CDN assets, which dropped the old protocols years ago. For an API that a fleet of devices calls, it is the one thing to find out before flipping the switch.

## How getReport checks it

> **Free tool:** [SSL / TLS checker](https://getreport.app/tools/ssl-check): Certificate expiry and chain, TLS versions, HTTPS enforcement, HSTS and mixed content for any site — the checks that stand between your visitors and a browser warning.

The checker makes two TLS handshakes to the host, both with the hostname as SNI, and never sends any application data. The first is a modern one, with TLS 1.2 as its minimum and HTTP/2 offered through ALPN; it records the negotiated protocol and cipher and reads the certificate. The second is deliberately old: a client that offers only TLS 1.0 and 1.1. If the server accepts that second handshake, the finding fails; if the server refuses it with a protocol-version alert or a handshake failure, it passes. If the test cannot tell (the local OpenSSL policy refused to even try), the finding is left out rather than guessed.

> **Check: The server only accepts TLS 1.2 or newer.** TLS 1.0 and 1.1 have known weaknesses and every major browser dropped them in 2020. A server that still accepts them lets an attacker downgrade a connection to one they can break.
>
> 1. Set the minimum protocol to TLS 1.2 (nginx: ssl_protocols TLSv1.2 TLSv1.3; Apache: SSLProtocol -all +TLSv1.2 +TLSv1.3).
> 2. Behind Cloudflare or another CDN, set "Minimum TLS Version" to 1.2 in its dashboard.

The evidence line reads, for example, `A TLS 1.0/1.1 handshake was accepted; a modern client negotiates TLSv1.3`. The second half is useful on its own: a server that negotiates `TLSv1.2` with a modern client has 1.3 off, which costs a round trip on every new connection.

![The TLS result panel of the SSL / TLS checker with the protocol version row: the negotiated protocol and cipher, whether a TLS 1.0/1.1 handshake was accepted, the certificate chain result and the HSTS row, each marked pass, warn or fail](https://getreport.app/guides/img/tls-1-0-and-1-1-turning-legacy-protocols-off/tls-panel.webp "The protocol row shows what a modern client negotiates and whether the legacy handshake was accepted.")

The same handshake feeds two more findings that you should re-read after changing the TLS configuration, because the files and settings sit next to each other and a typo in one often breaks the other:

> **Check: The TLS certificate chain validates.** Browsers cannot verify who issued the certificate, so visitors see "Your connection is not private" instead of your page. The usual causes are a missing intermediate certificate, an expired certificate or a name mismatch.
>
> 1. Install the full chain (your certificate plus the intermediate) that your certificate authority provides, not just the leaf.
> 2. Check that the certificate covers exactly this hostname, with and without www.

> **Check: Strict-Transport-Security header is set.** HSTS tells browsers to always use HTTPS for your site, so after the first visit a typed address or an old http:// link never starts on an unencrypted connection. Only the preload list covers the very first visit.
>
> 1. Send the header: Strict-Transport-Security: max-age=31536000; includeSubDomains.
> 2. Start with a shorter max-age (e.g. 86400) if you are not sure every subdomain supports HTTPS.

## Step by step

### 1. Log the negotiated protocol for a week

Add the protocol and cipher to the access log, then wait for a representative week (include a weekend and, for a B2B site, month-end).

**nginx**, in the `http` block of `nginx.conf`:

```nginx
log_format tls '$remote_addr [$time_local] "$request" $status '
               '$ssl_protocol $ssl_cipher "$http_user_agent"';
access_log /var/log/nginx/access.log tls;
```

**Apache**, in the virtual host or `httpd.conf` (`mod_ssl` and `mod_log_config`):

```apache
LogFormat "%h %t \"%r\" %>s %{SSL_PROTOCOL}x %{SSL_CIPHER}x \"%{User-Agent}i\"" tls
CustomLog /var/log/apache2/access.log tls
```

Then count:

```bash
grep -c -E " TLSv1(\.1)? " /var/log/nginx/access.log
grep -E " TLSv1(\.1)? " /var/log/nginx/access.log | awk -F'"' '{print $6}' | sort | uniq -c | sort -rn | head
```

The second command lists the user agents. Vulnerability scanners and bots will be most of them; look for a device or an integration you recognise. Behind Cloudflare, the Analytics dashboard shows traffic by TLS version without any server change.

### 2. Set the minimum version and the ciphers

Take the "intermediate" profile from Mozilla's SSL Configuration Generator, which is maintained for exactly this purpose and covers every mainstream server. The essential lines:

**nginx**, in the `http` block so that every server block inherits it (`nginx -t` to check, then reload):

```nginx
ssl_protocols TLSv1.2 TLSv1.3;
ssl_prefer_server_ciphers off;
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:DHE-RSA-CHACHA20-POLY1305;
ssl_session_timeout 1d;
ssl_session_cache shared:SSL:10m;
ssl_session_tickets off;
```

`TLSv1.3` needs nginx 1.13 or later built against OpenSSL 1.1.1 or later, which every supported distribution has. `ssl_prefer_server_ciphers off` lets a modern client pick ChaCha20 on a phone without AES hardware, which is faster for it.

**Apache 2.4.36 or later** (with OpenSSL 1.1.1), in the SSL virtual host or `ssl.conf`:

```apache
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:DHE-RSA-AES128-GCM-SHA256:DHE-RSA-AES256-GCM-SHA384:DHE-RSA-CHACHA20-POLY1305
SSLHonorCipherOrder off
SSLSessionTickets   off
```

On an older Apache that does not know `+TLSv1.3`, the line fails the config test; use `SSLProtocol all -SSLv3 -TLSv1 -TLSv1.1` instead, which gives TLS 1.2 only. These directives live in the server or virtual host config, never in `.htaccess`, because the protocol is negotiated before Apache knows which directory the request is for.

**Caddy** uses TLS 1.2 as its minimum and prefers 1.3 with no configuration. Only add `tls { protocols tls1.2 tls1.3 }` inside the site block if a previous admin lowered it.

**Cloudflare**: the setting that matters is on the edge, because visitors connect to Cloudflare, not to you. SSL/TLS, Edge Certificates, Minimum TLS Version: set it to TLS 1.2. The default for new zones has been TLS 1.0, so check it rather than assuming. Enable "TLS 1.3" on the same page. Your origin's own setting still governs the Cloudflare-to-origin hop; set it too.

**Hosting panels**: Plesk has a "TLS versions and ciphers" setting under Tools & Settings, and WHM configures Apache's `SSLProtocol` globally under Service Configuration. On shared hosting where none of this is exposed, ask support; it is a one-line change on their side and most hosts have already made it.

### 3. Drop the weak ciphers on purpose, not by accident

The cipher lists above contain only ECDHE or DHE key exchange (forward secrecy) with AES-GCM or ChaCha20-Poly1305 (AEAD). Things that are gone from them and should stay gone: `RC4`, `3DES` (`DES-CBC3-SHA`), every `-CBC-` and `-SHA` non-GCM suite, `NULL`, `EXPORT`, and plain `RSA` key exchange (any suite name without `ECDHE` or `DHE`). TLS 1.3 has its own set of five suites, all AEAD, which the `ssl_ciphers` and `SSLCipherSuite` lists above do not touch, so the list only affects 1.2 connections.

### 4. Reload and check the certificate still validates

`nginx -t && systemctl reload nginx` or `apachectl configtest && systemctl reload apache2`. A protocol change does not touch the certificate files, but the cipher list can: an ECDSA-only list on an RSA certificate means no cipher matches and every handshake fails. The lists above include both `ECDHE-RSA` and `ECDHE-ECDSA` suites for that reason. Re-run the checker and confirm the chain still validates.

## Platform notes

**Shopify, Wix, Squarespace, WordPress.com**: the platform terminates TLS and has enforced TLS 1.2 as a minimum for years. Nothing to change; run the checker to confirm.

**Self-hosted WordPress**: the setting is on the web server, not in WordPress. `wp-config.php`, plugins and the admin have no say in the TLS version. Follow the nginx or Apache steps, or the hosting panel.

**Mail servers on the same host**: Postfix (`smtpd_tls_protocols`) and Dovecot (`ssl_min_protocol`) have their own settings. Be more careful there: other mail servers negotiate with yours without a human to notice, and a few still speak only TLS 1.0. Most operators keep SMTP at TLS 1.2 minimum but log downgrades first.

**Load balancers and API gateways**: AWS ALB and CloudFront use named security policies (choose one whose name contains `TLS13` or `TLS-1-2`); Google Cloud load balancers use an SSL policy with a minimum version; Azure Application Gateway has a "minimum protocol version" in its SSL policy. Set it there; the instances behind it usually see plain HTTP.

## Verify

1. Re-run the [SSL / TLS checker](https://getreport.app/tools/ssl-check). The finding reads "The server only accepts TLS 1.2 or newer" and the evidence line shows `negotiated TLSv1.3`.
2. From a terminal, the old protocol must fail and the new one must succeed:

```bash
# Expected: an error such as "alert protocol version" or "handshake failure"
openssl s_client -connect example.com:443 -servername example.com -tls1_1 </dev/null

# Expected: "Protocol : TLSv1.2" and a cipher name in the output
openssl s_client -connect example.com:443 -servername example.com -tls1_2 </dev/null 2>/dev/null | grep -E "Protocol|Cipher"
```

If the first command prints `no protocols available`, your own OpenSSL refused to attempt TLS 1.1, which says nothing about the server. Force it to try with `-cipher 'DEFAULT:@SECLEVEL=0'` added to the command, or rely on the checker, which runs the same test from a machine where it is allowed.

3. The site still loads in every browser you support, the checkout completes, and the API integration you were worried about in step 1 still works.
4. A week later, the logs show no `TLSv1 ` or `TLSv1.1` lines at all, because the server no longer completes those handshakes.

## Common mistakes

- **Setting `ssl_protocols` in one `server` block and not the others.** Put it in `http` so a forgotten virtual host on the same IP cannot re-enable the old versions.
- **Setting the origin and forgetting the edge.** Behind Cloudflare or a load balancer, visitors negotiate with the edge. Its minimum-version setting is the one the checker sees.
- **`+TLSv1.3` on an Apache that does not support it.** The config test fails and the reload is refused; the old config keeps running. Read the output of `apachectl configtest`, and use the `all -SSLv3 -TLSv1 -TLSv1.1` form on older builds.
- **A cipher list with no suite matching the certificate's key type.** Every handshake fails, including yours. Keep both RSA and ECDSA suites unless you know which key you have.
- **Turning off 1.0 and 1.1 without the week of logs.** The one payment terminal or warehouse scanner that needed them fails on a Saturday. Log first, then switch, then watch the logs once more. And keep the certificate side automated, because a protocol change and an expiry in the same month are hard to tell apart from the outside: see [Certificate expiry: automate renewal, then monitor it](https://getreport.app/guides/certificate-expiry-automate-it-then-monitor-it) and, for the rest of the panel, [SSL/TLS checks that matter](https://getreport.app/guides/ssl-tls-checks-that-matter).
