When a TLS client sends its opening message, it expects a TLS record in reply. If the first bytes that come back are not a valid TLS record, OpenSSL reports wrong version number. Despite the name, this is rarely a real version disagreement. It almost always means the other end is not speaking TLS on that port.
How it appears:
curl: (35) error:0A00010B:SSL routines::wrong version number
curl: (35) error:1408F10B:SSL routines:ssl3_get_record:wrong version number
requests.exceptions.SSLError: ... [SSL: WRONG_VERSION_NUMBER] wrong version number (_ssl.c:1129)
The first is OpenSSL 3, the second OpenSSL 1.x. A genuine TLS version problem normally produces protocol version or tlsv1 alert protocol version instead. See sslv3 alert handshake failure for those.
Causes, most likely first
- HTTPS to a plain HTTP port. You used
https://against a port serving HTTP, such as 80, 8080 or a dev server on 3000. The server replies with something likeHTTP/1.1 400 Bad Request, whose first bytes OpenSSL cannot read as TLS. - An HTTPS proxy setting that points at an HTTP proxy.
https_proxy=https://proxy.corp:3128makes the client try TLS to the proxy itself. Most proxies speak plain HTTP on that port and use CONNECT for the tunnel, so the correct value ishttp://proxy.corp:3128. This is the most common cause on corporate networks and CI. - STARTTLS versus implicit TLS mismatch. SMTP on 587 or 25, IMAP on 143 and others begin in plain text and upgrade. Connecting with implicit TLS to those ports fails this way, as does the reverse on 465/993/995.
- A service listening without TLS after a misconfiguration:
listen 443;withoutsslin nginx, or a virtual host missingSSLEngine onin Apache. - Non-HTTP service on the port: SSH, a database or a load balancer in TCP passthrough mode.
- A captive portal or middlebox replying with its own HTML or a TCP reset.
Step 1: see what is really on the port
Ask for plain HTTP on the same port:
curl -v http://example.com:8443/
If that returns a normal HTTP response, the port is not TLS. Alternatively, connect raw and look:
printf 'HEAD / HTTP/1.0\r\n\r\n' | nc example.com 8443
Readable HTTP/1.1 ... text confirms plain HTTP. Binary garbage followed by a handshake means TLS. An SSH-2.0-... banner means SSH. The Port Checker confirms the port is open, and openssl s_client tells you whether TLS works at all:
openssl s_client -connect example.com:443 -servername example.com </dev/null 2>&1 | head -n 20
On a healthy port you see CONNECTED, a certificate chain and SSL-Session. On this failure you see error:0A00010B or no peer certificate available.
Step 2: check proxy variables
env | grep -i proxy
curl -v https://example.com/ 2>&1 | grep -iE "proxy|CONNECT"
Look for https_proxy, HTTPS_PROXY, ALL_PROXY and package-manager settings (npm config get https-proxy, git config --get http.proxy, pip config list). If the value begins with https://, change it to http://, unless you know the proxy genuinely terminates TLS on that port. Test by bypassing it: curl --noproxy '*' https://example.com/.
Step 3: use the right scheme for mail
openssl s_client -starttls smtp -connect mail.example.com:587 -crlf
openssl s_client -connect mail.example.com:465 -crlf # implicit TLS
openssl s_client -starttls imap -connect mail.example.com:143
Use -starttls for upgrade-style ports, and no flag for implicit TLS ports. In client libraries, the matching toggles are often named secure, use_tls or starttls; setting both or neither causes this error.
If you own the server
Make sure TLS is actually enabled on the listener.
server {
listen 443 ssl; # "ssl" is required; plain "listen 443;" serves HTTP
server_name example.com;
ssl_certificate /etc/ssl/example.com/fullchain.pem;
ssl_certificate_key /etc/ssl/example.com/privkey.pem;
}
<VirtualHost *:443>
SSLEngine on
SSLCertificateFile /etc/ssl/example.com/fullchain.pem
SSLCertificateKeyFile /etc/ssl/example.com/privkey.pem
</VirtualHost>
After a reload, run sudo ss -ltnp | grep :443 to confirm the right process owns the port. In containers, check that the published port maps to the TLS listener and not the HTTP one. Behind a load balancer, check whether the backend protocol is set to HTTP or HTTPS and match it to what your app serves. A mismatch there can show up as this error or as 502s.
Verify
Repeat the failing command; success looks like an HTTP status line. Check the endpoint using the HTTP Status Checker and the SSL Checker, and add a monitor so a bad deploy cannot silently turn off TLS.
Next steps
- Compare with ERR_SSL_PROTOCOL_ERROR in browsers.
- Read curl error 35 for other causes of that exit code.
- Use the Port Checker to confirm which ports respond.
- Read connection refused, timed out and reset explained.
FAQ
Is it a TLS version problem?
Rarely. It means the response was not a TLS record at all. Real version disagreements show protocol version alerts.
Why do I get it only through the corporate proxy?
Your proxy variable probably starts with https:// while the proxy speaks plain HTTP. Change the scheme to http://.
Why do I get it on port 587?
Port 587 normally uses STARTTLS. Connect in plain text first and upgrade, rather than starting TLS immediately.
Can a WAF or CDN cause it?
Yes, if its origin-side protocol setting disagrees with what your origin serves. Match the origin protocol and port in the CDN configuration.
Get notified when this fix updates
Plus occasional practical tips on uptime and monitoring, nothing else. Unsubscribe anytime.
This page is general information written by our editorial team and updated when documentation changes. Servers and providers differ, so test changes safely first. See our editorial policy and disclaimer.