2026年6月25日
2026年6月25日
SSL/TLSハンドシェイクエラーの原因と直し方|openssl s_clientでの切り分け手順
HTTPS の接続で「SSL handshake failed」「handshake failure」「SSLHandshakeException」といったエラーが出るのは、通信を暗号化する前の取り決め(ハンドシェイク)がどこかで失敗しているためです。原因は証明書・TLS のバージョン・暗号方式・接続先の取り違えなど複数あり、エラーメッセージを見れば多くは見当が付きます。
この記事では、openssl s_client と curl で原因を切り分け、Apache・Nginx の設定で直す手順を説明します。コマンドの出力例は Ubuntu 24.04(OpenSSL 3.0.13、curl 8.5.0、nginx 1.24.0)で実際に確認したものです(2026年10月時点)。
まず確認すること
| 確認すること | 方法 |
|---|---|
| 一部の端末・アプリだけか、全員か | 別のブラウザー・別の回線・curl で試す |
| いつから起きたか | 証明書の更新・サーバー設定の変更・OS の更新の時期と照らす |
| どのエラーメッセージか | ブラウザーの詳細、curl -v、アプリのログ |
全員で起きるならサーバー側(証明書・設定)、特定のクライアントだけならクライアント側(古い OS・古い Java・時計のずれ・社内プロキシ)が疑わしくなります。
openssl s_client で接続してみる
サーバーとのハンドシェイクを直接試せるのが openssl s_client です。-servername で SNI(どのドメインに接続したいかをサーバーに伝える情報)を渡します。
openssl s_client -connect example.com:443 -servername example.com </dev/null
出力の中で次の行を見ます。
New, TLSv1.3, Cipher is TLS_AES_256_GCM_SHA384
Verify return code: 0 (ok)
New, TLSv1.3, Cipher is ...… ハンドシェイクが成立し、使われた TLS バージョンと暗号方式Verify return code: 0 (ok)… 証明書の検証に成功
証明書の中身(発行先・発行者・期限・対象ドメイン)は次のコマンドで確認できます。
openssl s_client -connect example.com:443 -servername example.com </dev/null 2>/dev/null \
| openssl x509 -noout -subject -issuer -dates -ext subjectAltName
curl -v でも、ハンドシェイクの途中経過とエラーの内容を確認できます。
curl -v https://example.com/ -o /dev/null
エラー別の原因と直し方
unable to get local issuer certificate(中間証明書の不足)
openssl s_client で次のように出る場合です。
verify error:num=20:unable to get local issuer certificate
Verify return code: 21 (unable to verify the first certificate)
curl では curl: (60) SSL certificate problem: unable to get local issuer certificate、Java では PKIX path building failed となります。
サーバーがサーバー証明書だけを送り、中間証明書を送っていないことが典型的な原因です。ブラウザーは中間証明書を補える場合があるため「ブラウザーでは見られるのに、curl やアプリ、API 連携だけ失敗する」という形で表れます。
直し方は、サーバー証明書と中間証明書をつなげたファイル(Let's Encrypt なら fullchain.pem)を指定することです。
# Nginx
ssl_certificate /etc/letsencrypt/live/example.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/example.com/privkey.pem;
# Apache 2.4.8 以降(中間証明書を含むファイルを SSLCertificateFile に指定できる)
SSLCertificateFile /etc/letsencrypt/live/example.com/fullchain.pem
SSLCertificateKeyFile /etc/letsencrypt/live/example.com/privkey.pem
cert.pem(サーバー証明書だけ)を指定していないか確認してください。自己署名証明書や社内 CA の証明書を使っている場合も同じエラーになります。その場合はクライアント側にその CA を信頼させる必要があります(curl --cacert ca.crt や openssl s_client -CAfile ca.crt で、CA を渡すと Verify return code: 0 (ok) になることを確認できます)。
tlsv1 alert protocol version(TLS バージョンの不一致)
サーバーとクライアントに、共通して使える TLS バージョンがない場合です。TLS 1.3 だけを許可したサーバーに TLS 1.2 で接続すると、次のようになりました。
openssl s_client -connect example.com:443 -servername example.com -tls1_2 </dev/null
error:0A00042E:SSL routines:ssl3_read_bytes:tlsv1 alert protocol version:...:SSL alert number 70
no peer certificate available
curl では curl: (35) ... tlsv1 alert protocol version です。サーバーの Nginx のエラーログには、info レベルで次のように残りました。
SSL_do_handshake() failed (SSL: error:0A000102:SSL routines::unsupported protocol) while SSL handshaking
-tls1_2・-tls1_3 を付けて試すと、サーバーがどのバージョンに対応しているかを確かめられます。現在の一般的な設定は TLS 1.2 と 1.3 の両方を有効にし、1.0・1.1 を無効にするものです。
# Nginx(server ブロックまたは http ブロック)
ssl_protocols TLSv1.2 TLSv1.3;
# Apache(mod_ssl)
SSLProtocol -all +TLSv1.2 +TLSv1.3
TLS 1.2 に対応していない古いクライアント(古い Android や組み込み機器など)は、サーバー側では救えません。クライアント側の更新が必要です。
handshake failure(暗号方式の不一致)
TLS バージョンは合っていても、共通の暗号方式(暗号スイート)がない場合は handshake failure(alert number 40)になります。独自に厳しい ssl_ciphers / SSLCipherSuite を書いている場合に起きやすいため、独自の指定を外して OpenSSL の既定に戻すか、Mozilla の SSL Configuration Generator の「Intermediate」設定を基準にすると、互換性と安全性の釣り合いが取りやすくなります。弱い暗号の無効化については SSLの弱い暗号スイートを無効化する方法 も参考にしてください。
証明書の期限切れ・まだ有効でない
certificate has expired や certificate is not yet valid が出る場合は、証明書の期限を確認します。
sudo openssl x509 -in /etc/letsencrypt/live/example.com/fullchain.pem -noout -dates
- 期限が切れていれば更新します(Let's Encrypt なら
sudo certbot renew、更新後に Web サーバーを再読み込み)。 - 期限内なのにエラーになる場合は、サーバーではなく接続しているクライアントの時計がずれていないか確認します。
- ファイルは更新済みなのにエラーが続く場合は、Web サーバーを再読み込みしていない可能性があります(
sudo systemctl reload nginxなど)。
自動更新の設定は WordPressのLet's Encrypt証明書を自動更新する方法 で説明しています。
証明書のドメインが違う(SNI・名前の不一致)
curl で SSL: no alternative certificate subject name matches target host name と出る場合や、ブラウザーで「この接続ではプライバシーが保護されません」とドメインの不一致が示される場合は、別のドメイン用の証明書が返っています。
- 証明書の対象ドメイン(
subjectAltName)に、アクセスしたドメイン(wwwの有無を含む)が入っているか確認します。 - 1台のサーバーで複数のドメインを運用している場合、
server_name(Nginx)・ServerName(Apache)が正しく、そのドメインのserver/VirtualHostに証明書が設定されているかを確認します。openssl s_clientで-servernameを付けたときと付けないときで結果が変わる場合、SNI が関係しています。
HTTP と HTTPS のポートの取り違え
443 番ポートに HTTP で接続すると、Nginx は 400 The plain HTTP request was sent to HTTPS port を返します。逆に、HTTPS で 80 番ポートなど TLS を話さないポートに接続すると、ハンドシェイクの段階で失敗します。アプリの設定やリバースプロキシの転送先で、http:// と https://、ポート番号の組み合わせを確認してください。
Apache で Listen 443 はあるのに SSLEngine on の VirtualHost がない場合も、443 番で平文の HTTP が返り、同じ種類のエラーになります。Ubuntu では sudo a2enmod ssl と sudo a2ensite default-ssl(または自分で作った SSL 用の設定)が有効か確認します。
サーバー側のログを見る
Nginx は、クライアント側の理由によるハンドシェイクの失敗を info レベルで記録します。標準の error レベルでは残らないため、調べる間だけ一時的に上げます。
error_log /var/log/nginx/error.log info;
sudo nginx -t && sudo systemctl reload nginx
sudo tail -f /var/log/nginx/error.log | grep -i ssl
検証環境では、次のような記録が残りました。
| ログの内容 | 意味 |
|---|---|
unsupported protocol |
クライアントが、サーバーで無効にしている TLS バージョンで接続してきた |
tlsv1 alert unknown ca |
クライアントがサーバーの証明書を信頼できず、接続を打ち切った |
client sent plain HTTP request to HTTPS port |
443 番に HTTP で接続してきた |
Apache では LogLevel ssl:info を設定すると、mod_ssl の詳細がエラーログに出ます。調べ終わったらログレベルを元に戻してください。
Java アプリで SSLHandshakeException が出る場合
Java の javax.net.ssl.SSLHandshakeException も、原因は上と同じです。
PKIX path building failed… 中間証明書の不足、または Java のトラストストアにない CA(社内 CA・自己署名)Received fatal alert: protocol_version… TLS バージョンの不一致No appropriate protocol… Java 側で TLS 1.0・1.1 が無効になっている状態で、古いバージョンでしか接続できない相手につないでいる
起動オプションに -Djavax.net.debug=ssl:handshake を付けると、ハンドシェイクの詳細がログに出ます。
CDN やロードバランサーを使っている場合
Cloudflare などの CDN を経由している場合、ブラウザーと CDN の間と、CDN と元のサーバー(オリジン)の間で、ハンドシェイクが別々に行われます。CDN の画面に表示されるエラー(Cloudflare の 525 など)は、CDN とオリジンの間の失敗を示しています。オリジンのサーバーに直接 openssl s_client で接続し、上の手順で確認してください。WordPress サイトでの例は WordPressでSSLハンドシェイクエラーが出るときの対処法 にまとめています。
まとめ
openssl s_client -connect ホスト:443 -servername ホストで、TLS バージョン・暗号方式・証明書の検証結果を確認するunable to get local issuer certificateは中間証明書の不足が多い。fullchain.pemを指定するprotocol versionは TLS バージョンの不一致。サーバーは TLS 1.2 と 1.3 を有効にする- 期限切れは証明書の更新と Web サーバーの再読み込み、期限内ならクライアントの時計を確認する
- Nginx はハンドシェイクの失敗を info レベルで記録する。調べる間だけログレベルを上げる