Resolving curl (35) error:0A00010B:SSL routines::wrong version number

Root Cause Diagnosis

OpenSSL 3.x returns error:0A00010B (SSL_R_WRONG_VERSION_NUMBER) when the first bytes received after the TLS ClientHello are not a valid TLS record. The server on that port is almost always speaking plaintext (HTTP, or a non-TLS protocol) or a different TLS framing, or an intermediary (proxy, load balancer) answered the handshake with a non-TLS response. The most common triggers are an https:// URL pointed at an HTTP-only port, an HTTPS_PROXY variable that makes curl use TLS to an HTTP proxy, and a server whose TLS listener is not enabled on that port.

Quick Fix

Test the same endpoint over plain HTTP, and bypass any proxy variables:

curl -v http://example.com:8080/
curl -v --noproxy '*' https://example.com/

If the http:// request succeeds, the port is not TLS-enabled: fix the URL scheme or the server’s TLS configuration. If --noproxy '*' succeeds, the proxy configuration is the cause (see steps 3 and 4).

Step-by-Step Resolution

1. Capture the exact failure and the port being contacted.

curl -v https://example.com:8443/ 2>&1 | head -n 20

Look for Connected to ... port N and the line error:0A00010B. Note the port. Port 80 and most non-443 application ports (3000, 8000, 8080, 8888) usually serve plaintext.

2. Check whether the port speaks TLS at all.

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

no peer certificate available with wrong version number, or output that starts with HTTP/1.1 400 Bad Request, confirms a plaintext listener. A certificate chain and a Protocol : TLSv1.3 line confirms TLS works and the fault is elsewhere.

3. Remove proxy interference. List proxy variables in the current shell:

env | grep -i proxy

A common misconfiguration is https_proxy=https://proxy:3128. Most proxies expect plaintext for the proxy connection itself (the CONNECT method carries the TLS), so use the http:// scheme for the proxy:

export https_proxy=http://proxy.internal:3128
export HTTPS_PROXY=http://proxy.internal:3128

Persistent locations to check: /etc/environment, /etc/profile.d/*.sh, ~/.bashrc, ~/.curlrc, and for apt /etc/apt/apt.conf.d/proxy.

4. Verify curl’s proxy handling for the failing request.

curl -v -x http://proxy.internal:3128 https://example.com/

A HTTP/1.1 200 Connection established line after CONNECT means the tunnel works. If TLS to the proxy itself is required, use --proxy https://proxy:port, and only if the proxy has a TLS listener.

See also  Resolving Docker DNS lookup failure inside container on custom bridge network

5. Fix the server side (Nginx). A missing ssl parameter on the listen directive causes plaintext on that port:

server {
    listen 443 ssl;
    listen [::]:443 ssl;
    ssl_certificate     /etc/ssl/certs/example.com.pem;
    ssl_certificate_key /etc/ssl/private/example.com.key;
}

Validate and reload:

sudo nginx -t && sudo systemctl reload nginx
sudo ss -ltnp | grep ':443'

6. Fix the server side (Apache). Confirm SSL is enabled and the vhost binds correctly:

sudo a2enmod ssl
sudo apachectl -t -D DUMP_VHOSTS | grep -E ':443|:8443'
Listen 443
<VirtualHost *:443>
    SSLEngine on
    SSLCertificateFile    /etc/ssl/certs/example.com.pem
    SSLCertificateKeyFile /etc/ssl/private/example.com.key
</VirtualHost>

7. Fix the server side (application servers and containers). Frameworks such as Node, Flask, Django runserver, Spring Boot, and Vite dev servers default to HTTP. Either call them with http:// or enable TLS:

# Spring Boot
server.ssl.enabled=true
server.ssl.key-store=/path/keystore.p12

# Node.js
https.createServer({ key, cert }, app).listen(8443);

For containers, confirm the published port maps to the TLS listener, not the HTTP one:

docker ps --format '{{.Names}}\t{{.Ports}}'

8. Fix reverse proxy and load balancer protocol mismatch. A TLS-terminating proxy that forwards to a backend using the wrong scheme returns this error to the proxy, which can surface to the client. Match the backend scheme:

location / {
    proxy_pass http://127.0.0.1:8080;   # use https:// only if the backend serves TLS
}

For AWS ALB/NLB, Cloudflare, or HAProxy, check that the target group or origin protocol (HTTP vs HTTPS) matches what the backend listens for. In Cloudflare, set SSL/TLS → Overview to a mode compatible with the origin (for example Full or Full (strict) only when the origin serves valid TLS).

9. Verify.

curl -vI https://example.com/ 2>&1 | grep -E 'SSL connection|HTTP/'

Expected: SSL connection using TLSv1.3 (or TLSv1.2) and an HTTP status line.

See also  How to clear dead Docker overlay2 storage directory safely without breaking daemon

Why This Fails for Some Users

  • Wrong scheme on a non-standard port. https://host:8080 against an HTTP service is the single most frequent cause. The error is identical on Linux, macOS, and Windows because it originates in OpenSSL 3.x (curl built against OpenSSL 1.1.1 prints error:1408F10B:SSL routines:ssl3_get_record:wrong version number instead).
  • Different TLS library, different message. Curl built with Schannel, Secure Transport, GnuTLS, or NSS reports other strings. The code 0A00010B appears only with OpenSSL 3.x. Check with curl -V.
  • Environment-variable proxy leakage. Variables are case-insensitive for the scheme but curl prefers lowercase https_proxy. Containers, CI runners, and sudo can inherit or strip these differently: sudo -E preserves them, plain sudo often does not, so a command works as one user and fails as another.
  • PAC or system proxy on Windows and macOS. Browser-configured proxies do not apply to curl. A corporate TLS-intercepting proxy returning an HTML error page over a plaintext socket also produces this error.
  • Captive portals and ISP middleboxes. Public Wi-Fi may answer port 443 with plaintext HTTP redirects. Test from a different network.
  • STARTTLS protocols. SMTP (587), IMAP (143), and POP3 (110) begin in plaintext and upgrade. Using smtps:// or imaps:// against these ports fails. Use smtp://host:587 --ssl-reqd or --starttls smtp.
  • FTP and FTPS. Implicit FTPS (990) differs from explicit FTPS (21). Use ftp:// with --ssl-reqd for explicit mode.
  • Database and non-HTTP services. Pointing curl at PostgreSQL, Redis, MySQL, or an SSH port with https:// returns this error because those servers send their own banners.
  • Docker and Kubernetes port mapping. -p 443:8080 publishes an HTTP backend on 443. Clients see a TLS port that returns plaintext. Check kubectl get svc and the targetPort, and confirm the Ingress backend-protocol annotation (for example nginx.ingress.kubernetes.io/backend-protocol: "HTTPS").
  • IPv6 versus IPv4 listeners. The name may resolve to an IPv6 address bound to a different service. Compare curl -4 -v and curl -6 -v.
  • Server rejecting legacy protocol versions. If the server accepts only TLS 1.3 and the client’s OpenSSL build or --tlsv1.x option limits it to older versions, the failure can surface as a version error. Test with curl --tlsv1.2 --tls-max 1.3 -v and inspect openssl s_client -tls1_3.

Leave a Comment