エラーメッセージ
curl: (35) TLS connect error: error:0A000410:SSL routines::ssl/tls alert handshake failure
別の表記
- curl: (35) error:0A000410:SSL routines::sslv3 alert handshake failure
- curl: (35) error:14094410:SSL routines:ssl3_read_bytes:sslv3 alert handshake failure
OpenSSL 3.3 以降では理由が「ssl/tls alert handshake failure」と表記され、OpenSSL 3.1 以前では「sslv3 alert handshake failure」と出力されます(1.1.1 ではコード 14094410 と関数名 ssl3_read_bytes も付きます)。最近の curl のリリースでは、先頭に「TLS connect error:」が付きます。
curl (OpenSSL backend) — CURLE_SSL_CONNECT_ERROR, TLS alert 40 (handshake_failure) received from the server
このエラーの意味
サーバーは TLS アラート 40(handshake_failure)を送信してハンドシェイクを終了しました。curl が提示した内容では、パラメーターについて合意できなかったということです。このアラートには理由が含まれないため、実際の原因はサーバーのログでしか確認できません。クライアント側でできるのは、双方がサポートしている内容を比較することだけです。この時点では、証明書はまだ検証されていません。
発生する理由
共通の暗号スイートがない
よくあるRSA 鍵交換、CBC-SHA1、3DES、RC4 の暗号スイートに限定された古いサーバーが、それらのサポートを廃止したクライアントからの接続を受ける場合があります。また、少数の暗号スイートしか許可しない堅牢化されたサーバーが、それらを持たない古いクライアントからの接続を受ける場合もあります。いずれの場合も、サーバーは ClientHello の中に受け入れ可能なものを見つけられません。
クライアントがサーバー名(SNI)を送信しなかった
よくあるCDN やマルチテナントのプロキシ(Cloudflare、AWS CloudFront、多くの Kubernetes Ingress)は、SNI のないハンドシェイクや、自分が提供していない名前に対するハンドシェイクを拒否します。IP アドレスで接続したり、SNI を省略するツールを使ったりすると、まさにこのアラートが発生します。
サーバーがクライアント証明書を要求している
ときどき相互 TLS 用に設定された一部の TLS 1.2 サーバーは、クライアント証明書がない場合や受け入れられない場合に、より具体的な証明書関連のアラートではなく handshake_failure で応答します。
共通の鍵交換グループまたは署名アルゴリズムがない
ときどきポスト量子ハイブリッドのグループ、brainpool 曲線、RSA-PSS 署名のいずれかしか許可しないサーバーは、それらを提示しないクライアントとの接続に失敗することがあります。非常に古いクライアントでは、その逆も起こります。
修正方法
- 1.
双方が提示する内容を比較する
サーバーの暗号スイートとプロトコルバージョンを列挙し、クライアントの暗号リストと比較してください。curl の詳細出力を見ると、ハンドシェイクがどこまで進んだか、どのバージョンが試行されたかがわかります。
bash # What the server accepts (needs nmap) and what this client offers nmap --script ssl-enum-ciphers -p 443 api.example.com openssl ciphers -v 'DEFAULT' | head -20 # Full handshake trace from curl's side curl -sv https://api.example.com/ -o /dev/null 2>&1 | grep -E '^\* (TLS|SSL)' - 2.
サーバー管理者:現行の暗号設定を使う
TLS 1.3 と、ECDHE および AEAD の暗号スイート(AES-GCM、ChaCha20-Poly1305)を使う TLS 1.2 を提供してください。過去 10 年間のクライアントはすべてこれらに対応しており、最近のソフトウェアで RSA 鍵交換や CBC を必要とするものはありません。
text # nginx: TLS 1.2 with forward-secret AEAD suites + TLS 1.3 ssl_protocols TLSv1.2 TLSv1.3; ssl_ciphers ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305; ssl_prefer_server_ciphers off; - 3.
正しいサーバー名を送信する
URL ではホスト名を使うか、--resolve でアドレスを強制しつつ名前は維持してください。openssl s_client では、IP アドレスに接続するときに -servername を指定してください。
bash # Use the host name in the URL (SNI is sent automatically); for IP targets keep the name: curl --resolve api.example.com:443:203.0.113.10 https://api.example.com/ # openssl s_client sends SNI only with -servername (or a host name in -connect) openssl s_client -connect 203.0.113.10:443 -servername api.example.com </dev/null - 4.
相互 TLS のクライアント証明書を提示する
エンドポイントが mTLS の API であれば、クライアント証明書と鍵を設定してください。サーバーのログでは、この問題はピア証明書がない、または検証できないこととして示されます。
bash # Mutual TLS: present the client certificate and key the server expects curl --cert client.pem --key client.key https://mtls.example.com/
関連するエラー
SSL routines:ssl3_read_bytes:ssl/tls alert handshake failureOpenSSL (client side)SSL routines:tls_post_process_client_hello:no shared cipherOpenSSL (server side: nginx, Apache, Postfix, openssl s_server)curl: (35) TLS connect error: error:0A00042E:SSL routines::tlsv1 alert protocol…curl (OpenSSL backend)[SSL: SSLV3_ALERT_HANDSHAKE_FAILURE] ssl/tls alert handshake failurePython ssl module (OpenSSL)javax.net.ssl.SSLHandshakeException: (handshake_failure) Received fatal alert:…Java (JSSE)