Blog/How to Fix ERR_SSL_PROTOCOL_ERROR in Nginx ("This Site Can't Provide a Secure Connection")

NginxSSLTLSTroubleshootingDevOps··greencrew

How to Fix ERR_SSL_PROTOCOL_ERROR in Nginx ("This Site Can't Provide a Secure Connection")

ERR_SSL_PROTOCOL_ERROR usually means the server answered an HTTPS request with plain HTTP, most often because the Nginx server block says listen 443 without the ssl parameter. Change it to listen 443 ssl, point ssl_certificate and ssl_certificate_key at a valid certificate, and run nginx -t && systemctl reload nginx. This guide covers every server-side and client-side cause, how to diagnose each with curl and openssl, and how to prevent it.

Short answer: Chrome shows ERR_SSL_PROTOCOL_ERROR ("This site can't provide a secure connection ... sent an invalid response") when the TLS handshake fails before any page loads. On an Nginx server, the most common cause is plain HTTP being served on port 443, usually because the server block has listen 443; instead of listen 443 ssl;. Run curl -v https://example.com: if you see wrong version number, that's the problem. Fix the listen line, make sure ssl_certificate and ssl_certificate_key are set for that server block, then sudo nginx -t && sudo systemctl reload nginx. If the server is fine, the cause is on the client side, such as an HSTS-forced .dev domain, antivirus HTTPS scanning, or a proxy.

What Is ERR_SSL_PROTOCOL_ERROR?

Before your browser loads anything over HTTPS, it runs a TLS handshake with the server to agree on a protocol version and ciphers and to verify the certificate. ERR_SSL_PROTOCOL_ERROR means the handshake broke because the server's reply wasn't valid TLS, or the connection was cut mid-handshake.

It's different from certificate errors, where the handshake works but the certificate isn't trusted. Each browser words it differently:

Browser / toolWhat you see for the same problem
Chrome, Edge, BraveERR_SSL_PROTOCOL_ERROR, "This site can't provide a secure connection"
FirefoxSSL_ERROR_RX_RECORD_TOO_LONG (plain HTTP on 443) or PR_END_OF_FILE_ERROR
Safari"Safari can't open the page because Safari can't establish a secure connection to the server"
curl (OpenSSL 3)SSL routines::wrong version number or unexpected eof while reading
openssl s_clientwrong version number, or no peer certificate available

Related Errors That Aren't the Same Thing

ErrorMeansWhere to look instead
ERR_SSL_VERSION_OR_CIPHER_MISMATCHHandshake ran but no protocol/cipher in common (often TLS 1.0/1.1 only)ssl_protocols, ssl_ciphers
NET::ERR_CERT_AUTHORITY_INVALIDCertificate not trusted (self-signed, missing chain)Use fullchain.pem, or a real CA
NET::ERR_CERT_COMMON_NAME_INVALIDCertificate doesn't cover this hostnameReissue with the right -d names
NET::ERR_CERT_DATE_INVALIDCertificate expired, or the client clock is wrongRenew, or fix the clock
ERR_CONNECTION_REFUSEDNothing listening on 443 at allFirewall, listen 443 missing
ERR_TOO_MANY_REDIRECTSHTTP ↔ HTTPS redirect loopRedirect rules, CDN SSL mode

Step 1: Diagnose with curl and openssl

Test from your own machine first, then from the server itself to skip DNS, CDNs, and firewalls.

curl -vI https://example.com

Look at the lines just after Connected to:

curl outputWhat it means
SSL routines::wrong version numberServer replied with plain HTTP. Missing ssl on listen 443 or traffic forwarded to an HTTP port
unexpected eof while reading / Connection reset by peerServer closed the connection mid-handshake (often no certificate for that server block)
SSL certificate problem: ...Handshake worked, so it's a certificate trust issue, not this error
HTTP/2 200Server is fine from here. Look at the client, CDN, or network

Confirm with OpenSSL, using -servername so SNI is sent the way a browser sends it:

openssl s_client -connect example.com:443 -servername example.com </dev/null

A healthy server prints a certificate chain and Verify return code: 0 (ok). If you see wrong version number, prove the server is speaking HTTP on 443:

curl -i http://example.com:443
# HTTP/1.1 200 OK   <- plain HTTP on the HTTPS port
# or: HTTP/1.1 400 Bad Request ... "The plain HTTP request was sent to HTTPS port"

The second response means Nginx is expecting TLS on that port, so the server block is fine and something in front of it isn't. Then check what's listening and what Nginx actually loaded:

sudo ss -tlnp | grep ':443'
sudo nginx -T 2>/dev/null | grep -nE 'listen .*443|ssl_certificate|server_name'
sudo tail -n 50 /var/log/nginx/error.log

nginx -T prints the full merged config, including every file in sites-enabled/ and conf.d/. That's the fastest way to spot a second server block on 443 that you forgot about.

Step 2: Fix listen 443 Without ssl (Most Common Cause)

This config serves plain HTTP on port 443:

server {
    listen 443;              # wrong: no "ssl"
    server_name example.com;
    ssl_certificate     /etc/letsencrypt/live/example.com/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/example.com/privkey.pem;
}

Having ssl_certificate lines isn't enough. TLS is only enabled by the ssl parameter on listen. Fix it:

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

    ssl_certificate     /etc/letsencrypt/live/example.com/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/example.com/privkey.pem;
    ssl_protocols TLSv1.2 TLSv1.3;

    root /var/www/example.com;
}

server {
    listen 80;
    listen [::]:80;
    server_name example.com www.example.com;
    return 301 https://$host$request_uri;
}
sudo nginx -t && sudo systemctl reload nginx
curl -vI https://example.com

Old guides use ssl on;. That directive was deprecated in Nginx 1.15.0 and removed in 1.25.1, so on current versions nginx -t fails with an unknown directive error. Use listen 443 ssl; instead. Likewise, listen 443 ssl http2; still works but is deprecated since 1.25.1 in favor of the separate http2 on; directive. See the ngx_http_ssl_module docs and configuring HTTPS servers.

Check Every Server Block on Port 443

Nginx decides which server block answers a connection by IP:port first, then by SNI hostname. A port either speaks TLS or it doesn't, so don't mix listen 443 ssl in one block with plain listen 443 in another, or with a stray listen 443 on a specific IP. A leftover block from an old site or a copied snippet is enough to break HTTPS for some hostnames. Make every listen 443 line in the output of nginx -T include ssl.

Step 3: Make Sure Every SSL Server Block Has a Certificate

When a block listens with ssl but has no certificate (and none is inherited from the http level), Nginx accepts the TCP connection and then fails the handshake. The error log says:

no "ssl_certificate" is defined for the "listen ... ssl" directive in ...

or, at request time:

no "ssl_certificate" is defined in server listening on SSL port while SSL handshaking

The usual culprit is a default server on 443 (for example a catch-all server_name _; block) with no certificate. Clients whose SNI doesn't match another block, or that send no SNI, land there and the handshake fails. Give the catch-all a certificate, even a self-signed one, or reject those handshakes outright (supported since Nginx 1.19.4):

server {
    listen 443 ssl default_server;
    listen [::]:443 ssl default_server;
    server_name _;
    ssl_reject_handshake on;
}

Also confirm the files exist and match:

sudo ls -l /etc/letsencrypt/live/example.com/
sudo openssl x509 -noout -modulus -in /etc/letsencrypt/live/example.com/fullchain.pem | openssl md5
sudo openssl rsa  -noout -modulus -in /etc/letsencrypt/live/example.com/privkey.pem  | openssl md5

The two hashes must match. (This modulus comparison works for RSA keys. Certbot issues ECDSA keys by default in recent versions, so for those compare openssl x509 -noout -pubkey with openssl pkey -pubout instead.) If you don't have a certificate yet, follow how to install Let's Encrypt SSL on Nginx with Certbot.

Step 4: Check What's in Front of Nginx

If Nginx is correct when tested on the server (curl -vI --resolve example.com:443:127.0.0.1 https://example.com) but fails from outside, something in between is breaking TLS.

SetupCauseFix
DockerPort mapped as -p 443:80, so container HTTP is exposed on host 443Map 443:443 and configure TLS in the container, or terminate TLS on the host
Cloud load balancerListener on 443 forwards TLS straight to an HTTP target (or the reverse)Match the listener protocol to the target protocol
Another service on 443Apache, a VPN, or a panel bound to 443 first`sudo ss -tlnp
Router port forwardingWAN 443 forwarded to LAN port 80Forward 443 → 443
CDN/proxy in TCP passthroughOrigin isn't speaking TLS on the port the CDN connects toEnable TLS on the origin or change the CDN's origin port
Reverse proxy to an appproxy_pass https://... to an app that only speaks HTTPUse proxy_pass http://127.0.0.1:PORT (see reverse proxy guide)

The last row shows up in the Nginx error log as SSL_do_handshake() failed ... wrong version number while SSL handshaking to upstream, and the browser usually gets a 502 rather than ERR_SSL_PROTOCOL_ERROR. It's the same mistake in the other direction.

Step 5: Rule Out Protocol and Cipher Problems

Modern browsers require TLS 1.2 or later. If your config forces old protocols only, the error changes to ERR_SSL_VERSION_OR_CIPHER_MISMATCH, but some clients report a generic protocol error. Use:

ssl_protocols TLSv1.2 TLSv1.3;

Test each version:

openssl s_client -connect example.com:443 -servername example.com -tls1_2 </dev/null | grep -E 'Protocol|Cipher'
openssl s_client -connect example.com:443 -servername example.com -tls1_3 </dev/null | grep -E 'Protocol|Cipher'

Mozilla's SSL Configuration Generator produces a tested ssl_protocols and ssl_ciphers set for your Nginx and OpenSSL versions.

Step 6: Client-Side Causes

If curl and openssl succeed from several networks, the server is fine and the problem is on the visitor's machine:

CauseHow to tellFix
HSTS-preloaded TLD (.dev, .app) on a local HTTP serverChrome forces HTTPS for these domains, so your local HTTP-only server failsUse localhost or .test, or serve local HTTPS
Domain previously sent HSTSSite worked over HTTPS once; now HTTPS is broken and the browser won't fall backFix HTTPS; for local testing, delete the domain at chrome://net-internals/#hsts
Antivirus "HTTPS scanning"Fails on one machine only; works in a VM or on mobileDisable web/HTTPS scanning or update the antivirus
Corporate proxy or captive portalFails on one network onlyTest on mobile data; ask IT about TLS inspection
Typed https:// for a dev serverhttps://localhost:3000 while the dev server only speaks HTTPUse http://localhost:3000 or enable HTTPS in the dev server
Wrong system clockUsually certificate date errors, sometimes generic handshake failuresEnable automatic time sync
Stale cached stateError persists after a fixTry an incognito window, clear the SSL state, restart the browser

Also be careful with HSTS headers. Once browsers have seen Strict-Transport-Security, they refuse plain HTTP for that domain for the whole max-age. A broken TLS config then means the site is completely unreachable for returning visitors, with no HTTP fallback.

ERR_SSL_PROTOCOL_ERROR Troubleshooting Table

SymptomCauseFix
curl: wrong version numberPlain HTTP on port 443listen 443 ssl; in every 443 block
curl -i http://host:443 returns a normal pageSame as above, confirmedAdd ssl to listen, reload
nginx -t: unknown directive "ssl"ssl on; removed in Nginx 1.25.1Delete it, use listen 443 ssl;
Error log: no "ssl_certificate" is definedSSL server block without a certificateAdd a certificate or ssl_reject_handshake on;
Works for example.com, fails for another hostnameThat name falls through to a default server without a certificateAdd the name to the right block, give the default a cert
Works on the server, fails from outsideDocker/LB/router forwarding 443 to an HTTP portFix the port mapping or listener protocol
Fails only on one machine or networkAntivirus, proxy, or HSTS on a local domainSee the client-side table
Error appeared after a Certbot runCertbot edited the wrong block, or a duplicate block exists`nginx -T
Error appeared after renewalNginx not reloaded or a file path changedsudo systemctl reload nginx, check the deploy hook

Get Alerted When HTTPS Breaks

TLS problems usually come from a change: a new server block, a Certbot run that edited the wrong file, a Docker port mapping, a load balancer tweak. They're also easy to miss when you're testing from a machine that has cached state, or over HTTP.

GreenCrew runs HTTP uptime checks from outside your network. Point a check at your https:// URL, and when a handshake failure makes the site unreachable, you get an alert on Discord, Slack, or Telegram, while your public status page keeps users informed. Add the check before you change TLS config, not after. See how to get website downtime alerts on Discord, Slack, and Telegram.

Frequently Asked Questions

What causes ERR_SSL_PROTOCOL_ERROR?

The browser received something that isn't a valid TLS handshake, or the connection was closed mid-handshake. On Nginx servers it's most often listen 443 without ssl, a server block on 443 with no certificate, or a port forward that sends HTTPS traffic to an HTTP port.

Is ERR_SSL_PROTOCOL_ERROR a server or a client problem?

Usually the server. Test with curl -vI https://yourdomain and openssl s_client from a couple of networks. If they fail, it's the server or something in front of it. If they succeed and only one browser or machine fails, it's client-side (antivirus, proxy, HSTS, cached state).

Why does it only happen on localhost or a .dev domain?

Chrome has .dev and .app on the HSTS preload list, so it always uses HTTPS for them. If your local server only speaks HTTP, the handshake fails. Use localhost or a .test domain for local work, or run local HTTPS with a self-signed certificate.

Does clearing the browser cache fix ERR_SSL_PROTOCOL_ERROR?

Only when the server was already fixed and the browser is holding stale state. Clearing the cache doesn't fix a misconfigured server, so check with curl first and only then clear the cache or try an incognito window.

Conclusion

ERR_SSL_PROTOCOL_ERROR means the TLS handshake failed before any HTTP happened. On Nginx, start with curl -vI https://yourdomain: wrong version number points at listen 443 without ssl or a port forward to HTTP, and a reset connection points at a server block with no certificate. Fix the listen lines, give every SSL block (including the default) a certificate or ssl_reject_handshake, keep ssl_protocols TLSv1.2 TLSv1.3, and check what sits in front of Nginx. Then set up an external HTTPS check with GreenCrew so the next broken handshake reaches you as an alert.