Quick fix
# Disable legacy protocols globally on the client (edit java.security, most common fix)
# $JAVA_HOME/jre/lib/security/java.security (Oracle) or $JAVA_HOME/conf/security/java.security (OpenJDK)
jdk.tls.disabledAlgorithms=SSLv3, TLSv1, TLSv1.1, RC4, MD5withRSA, DH keySize < 768
# Per-application JVM flags (-Dhttps.protocols only affects HttpsURLConnection,
# -Djdk.tls.client.protocols covers the whole client)
-Dhttps.protocols=TLSv1.2,TLSv1.3 -Djdk.tls.client.protocols=TLSv1.2,TLSv1.3
# Server side (nginx)
ssl_protocols TLSv1.2 TLSv1.3;
What you will run into
An old project (JDK 1.6/1.7/early 1.8) starts failing HTTPS calls after a dependency upgrade, server migration or enabling HTTPS:
- Client throws
javax.net.ssl.SSLHandshakeException: Received fatal alert: protocol_versionorNo appropriate protocol (protocol is disabled or cipher suites are inappropriate); openssl s_client -connect <host>:<port> -tls1works but-tls1_2fails (server only speaks legacy protocols); or the opposite - a new server rejects the old client.
Why this happens
TLS handshake requires a non-empty intersection between the client's and server's enabled protocol sets. When the intersection is empty the server sends alert protocol_version (alert 70), which surfaces in Java as SSLHandshakeException. Three typical traps:
| Trap | Mechanism |
|---|---|
| Server only enables TLSv1.0/1.1 | New JDK/OpenSSL disables legacy protocols by default, client has nothing to negotiate |
| Old JDK defaults to TLSv1.0 max | JDK 1.7 and below do not enable TLS1.2 by default; cannot reach TLS1.2-only servers |
| Hardcoded legacy protocols in code | setEnabledProtocols("TLSv1"), SSLv3, custom SSLContext, disabled hostname verification |
JDK defaults by version (summarized from the source article):
| JDK version | Default TLS capability | Key point |
|---|---|---|
| 1.6 | max TLSv1.0 | need a newer JDK for TLS1.2 |
| 1.7.0_79 | supports TLS1.2 but not enabled by default | enable manually + disable TLSv1/1.1 |
| 8u112 | TLS1.2; no TLS1.3 | TLSv1/1.1 not disabled by default, disable manually |
| 8u261~8u340 | TLS1.3 enabled for server role | 8u341+ also enabled for client role |
| 11.0.0~11.0.10 | TLS1.2/1.3; TLSv1/1.1 not disabled | 11.0.11+ disables TLSv1/1.1 by default |
The TLS handshake is "protocol-set intersection" negotiation: the client sends its supported protocol list, the server picks from the intersection; an empty intersection yields alert protocol_version (70), surfacing as SSLHandshakeException in Java.
The problem is always "one side's capability set is out of step with its era". The server only enables TLSv1.0/1.1, while modern clients disable them by default.
Or the client is too old — JDK ≤1.7 defaults to max TLSv1.0 and cannot reach servers that only support 1.2+. Code may also hard-code legacy protocols.
JDK defaults evolve over versions (8u112 supports 1.2 but not 1.3; 8u341+ enables 1.3 as client default; 11.0.11+ disables 1.0/1.1 by default).
Deterministic takeaway: on handshake failure, determine both sides' capabilities first. Probe the server with openssl s_client -tls1_2 -connect <host>:<port>; on the client, -Djavax.net.debug=ssl:handshake shows the sent protocol list.
Fix per side: enable newer protocols on the server, upgrade the JDK, or remove hard-coded protocols from code.
On old JDKs, TLS1.3 has known bugs (JDK-8211806 etc.) — avoid enabling it before 11.0.8.
Fixed diagnostic: alert 70 = empty protocol intersection, not a certificate or cipher problem.
Step by step
1. Find out which side is restricted — Required
# Three-way probe: 1.2 OK / 1.0, 1.1 rejected -> server is modern, the client is the problem
openssl s_client -connect <host>:<port> -tls1_2
# 1.0 OK / 1.2 rejected -> server still runs legacy TLS, upgrade the server first
openssl s_client -connect <host>:<port> -tls1
2. Client: disable legacy protocols (three scopes) — Required
- Global (recommended): add
TLSv1, TLSv1.1tojdk.tls.disabledAlgorithmsin java.security, restart apps; this affects every app on that JDK - apps that still need TLSv1/1.1 will break, confirm first. - JVM flags (current app):
-Dhttps.protocols=TLSv1.2,TLSv1.3 -Djdk.tls.client.protocols=TLSv1.2,TLSv1.3. - Code (single connection):
connection.setEnabledProtocols(new String[]{"TLSv1.2", "TLSv1.3"}); Spring Boot:server.ssl.enabled-protocols: TLSv1.2, TLSv1.3.
3. Hunt down hardcoded protocols and disabled verification — Required
grep -rn "setHostnameVerifier" --include="*.java" .
grep -rn "TLSv1\|SSLv3" --include="*.java" .
Check: HttpsURLConnection.setEnabledProtocols, Apache HttpClient SSLConnectionSocketFactory protocol lists, custom RestTemplate SSL contexts; never ship a HostnameVerifier that skips certificate checks.
4. Verify — Required
openssl s_client -connect <host>:<port> -tls1_2 # should succeed
openssl s_client -connect <host>:<port> -tls1 # should fail
openssl s_client -connect <host>:<port> -tls1_1 # should fail
App logs show no SSLHandshakeException / NoSuchAlgorithmException and negotiate TLSv1.2.
How to confirm it works
The three-way probe shows 1.2 OK, 1.0/1.1 rejected, and the app handshake version is TLSv1.2+; Java no longer throws Received fatal alert: protocol_version.
Other environments
| Environment | Notes |
|---|---|
| Server (nginx/openssl) | ssl_protocols TLSv1.2 TLSv1.3;, equivalent to the "new server" scenario in the repro log |
| OpenSSL 3.5 | disables TLSv1.0/1.1 at the default security level, same mechanism as JDK 8u261+/11.0.11+ |
| Not verified | per-version JDK default tables come from the source article; no legacy JDK available locally |
Symptom
HTTPS calls fail with SSLHandshakeException: Received fatal alert: protocol_version or No appropriate protocol; the openssl three-way probe is asymmetric between TLS1.0/1.1 and TLS1.2 (one side connects, the other does not).
Cause
The intersection of the client's and server's enabled TLS sets is empty: server only speaks TLSv1.0/1.1 while the client disables them by default; or an old JDK caps at TLS1.0 against a modern server; or code hardcodes legacy protocols.
Solution
- Use the openssl three-way probe (
-tls1/-tls1_1/-tls1_2) to find which side is restricted; - Server: nginx
ssl_protocols TLSv1.2 TLSv1.3;, upgrade old openssl; - Client: add
TLSv1, TLSv1.1tojdk.tls.disabledAlgorithmsin java.security (global), or-Djdk.tls.client.protocols=TLSv1.2,TLSv1.3(per-app), orsetEnabledProtocolsin code (per-connection); - Find and remove
TLSv1/SSLv3 hardcoding and HostnameVerifier that skips verification; - Verify with the probe: 1.2 connects, 1.0/1.1 rejected.
Verification Evidence
- Mechanism repro (2026-08-30, WSL Ubuntu, OpenSSL 3.5.5): local TLS1.0-only server -> TLS1.2 client handshake fails (
unsupported protocol); TLS1.0 client succeeds. Reverse: TLS1.2-only server -> TLS1.0 client fails withtlsv1 alert protocol version(the underlying cause of Java'sReceived fatal alert: protocol_version), TLS1.2 client succeeds. local repro log 2026-08-30. - Not verified: no legacy JDK available locally; per-version default tables come from the source article ("The Long-Read TLS Protocol Upgrade Pitfall Handbook", Chinese original, 2026-08-26); OpenSSL 3.5 default security level disables TLS1.0/1.1, same mechanism as modern JDKs.
Related canonicals
Read in order: precondition -> same root / same domain / related:
- Related (similar errors, different cause):
- GitHub direct connection slow/fails (similar errors (TLS drops), different root cause)
Applicable tools (2)
This problem is linked from the tool pages below — reachable from either side.
jdkopensslSource & evidence
← Security & Governance domain | Problem Catalog Experience | Facing another issue? Go to the Helpdesk