javax.net.ssl.SSLHandshakeException: PKIX path building failed を Java HTTPS 呼び出しで修正する

intermediate Java2026-03-24| Java 8〜21、任意のOS(Linux/macOS/Windows)、任意のHTTPSクライアント(HttpURLConnection、OkHttp、RestTemplate、Apache HttpClient)

Error Message

javax.net.ssl.SSLHandshakeException: sun.security.validator.ValidatorException: PKIX path building failed: sun.security.provider.certpath.SunCertPathBuilderException: unable to find valid certification path to requested target
#java#ssl#https#証明書#トラストストア#pkix

状況

深夜2時。本番環境でこんなエラーが出始めました:

javax.net.ssl.SSLHandshakeException: sun.security.validator.ValidatorException: PKIX path building failed: sun.security.provider.certpath.SunCertPathBuilderException: unable to find valid certification path to requested target

1時間前まで問題なく動いていたエンドポイントです。コードは何も変えていない。でも何かが変わった——リモートサーバーが証明書をローテーションしたか、JDKがアップデートされたか、あるいはCAルート証明書が入っていない新しいサーバーにデプロイしたばかりなのかもしれません。

素早く診断して修正する方法を説明します。

何が起きているのか

JavaのSSLエンジンがリモートサーバーの証明書チェーンを検証しようとします。チェーンをたどって、トラストストア($JAVA_HOME/lib/security/cacerts)内の信頼されたルートCAを探します。一致するものが見つからない場合、PKIX path building failedがスローされます。

よくある原因:

  • サーバーがプライベート/内部CAで署名された証明書を使用しており、Javaのデフォルトトラストストアに含まれていない
  • サーバーが自己署名証明書を使用している
  • サーバーの証明書チェーンが不完全(中間CAが欠けている)
  • JDKが古く、新しいルートCAが含まれていない——たとえばLet's EncryptのISRG Root X1は8u291までJavaのトラストストアに含まれていなかった
  • 企業のプロキシがSSLインスペクションを行い、トラフィックを傍受して独自の証明書で再署名している

手順1:どの証明書が欠けているか特定する

まずサーバーが実際に送信している内容を確認します:

# サーバーの証明書チェーンを確認する
openssl s_client -connect your-api.example.com:443 -showcerts 2>/dev/null | openssl x509 -noout -issuer -subject

# またはチェーン全体を確認する
openssl s_client -connect your-api.example.com:443 -showcerts 2>/dev/null

Verify return code: 0 (ok)が表示されるか確認します。OpenSSLでは検証できてもJavaではできない場合、問題はサーバーではなくJavaのトラストストアにあります。

JavaがすでにそのCAを持っているか確認します:

keytool -list -cacerts -storepass changeit | grep -i "issuer-name"

手順2:クイックフィックス——証明書をインポートする

サーバーの証明書(またはそれを署名したCAの証明書)をダウンロードします:

# サーバー証明書をダウンロードする
openssl s_client -connect your-api.example.com:443 -showcerts 2>/dev/null < /dev/null \
  | openssl x509 -outform PEM > server.crt

# チェーンの場合は、出力から手動で全証明書を保存する
# -----BEGIN CERTIFICATE----- ブロックがそれぞれ1つの証明書

Javaのトラストストアにインポートします:

# JAVA_HOMEを確認する
java -XshowSettings:all -version 2>&1 | grep java.home

# 証明書をインポートする(必要に応じてroot/adminで実行)
keytool -import -alias my-server-cert \
  -keystore $JAVA_HOME/lib/security/cacerts \
  -storepass changeit \
  -file server.crt \
  -noprompt

アプリケーションを再起動します。エラーは解消されるはずです。

パスについての注意: Java 9以降のトラストストアは$JAVA_HOME/lib/security/cacertsにあります。Java 8では1階層深い$JAVA_HOME/jre/lib/security/cacertsです。Java 9以降は設定によってOSのトラストストアを継承することもできます。

手順3:恒久的な修正——カスタムトラストストアをバンドルする

システムのJDKトラストストアにインポートする方法は有効ですが、JDKの更新で内容が消えることがあります。より安全なアプローチは、CAの証明書をアプリにバンドルして、JVMに独自のトラストストアを指定することです。

カスタムトラストストアを作成する

# デフォルトのcacertsのコピーから開始する
cp $JAVA_HOME/lib/security/cacerts my-app-truststore.jks

# CAの証明書を追加する
keytool -import -alias internal-ca \
  -keystore my-app-truststore.jks \
  -storepass changeit \
  -file internal-ca.crt \
  -noprompt

起動時にJVMへ指定する

java -Djavax.net.ssl.trustStore=/path/to/my-app-truststore.jks \
     -Djavax.net.ssl.trustStorePassword=changeit \
     -jar your-app.jar

またはコードで設定する(Spring Boot/プログラム的な方法)

System.setProperty("javax.net.ssl.trustStore", "/path/to/my-app-truststore.jks");
System.setProperty("javax.net.ssl.trustStorePassword", "changeit");

HTTPS接続が行われる前に記述してください——理想的にはアプリ起動時、Springコンテキストがロードされる前です。

特殊ケース:企業のSSLインスペクションプロキシ

企業ネットワーク上では、プロキシがHTTPSトラフィックを傍受して、独自のCAでレスポンスに再署名することがあります。アプリが受け取る証明書は、実際のリモートCAではなく「Zscaler」や会社名が発行者になっています。

# 実際に受け取っている証明書の発行者を確認する
openssl s_client -connect your-api.example.com:443 2>/dev/null | grep "issuer"

発行者に企業のプロキシが表示されていますか?IT部門にプロキシのルートCA証明書を入手して、上記の手順でインポートしてください。

特殊ケース:古いJDKでのLet's Encrypt

Let's EncryptのルートCAであるISRG Root X1は、8u291と11.0.11でJavaのトラストストアに追加されました。古いJDKにはそもそも含まれていません。

# JDKのバージョンを確認する
java -version

# ISRG Root X1 が含まれているか確認する
keytool -list -cacerts -storepass changeit | grep -i "isrg"

JDKを更新するのが最もクリーンな解決策です。今すぐ更新できない場合は、letsencrypt.orgからISRG Root X1をダウンロードして手動でインポートしてください。

修正を確認する

# JavaのSSLデバッグ出力を使ってハンドシェイクが成功するか確認する
java -Djavax.net.debug=ssl:handshake -jar your-app.jar 2>&1 | grep -E "(Certificate chain|Finished|Exception)"

ハンドシェイクが成功すると、例外なしにFinishedが表示されます。デバッグ出力にはハンドシェイクの全ログと、どの証明書が検証されたかが含まれます。

デプロイせずに素早く動作確認する場合:

curl -v https://your-api.example.com/health
# エンドポイントが生きているか確認するためにSSLを完全にバイパスする
wget --no-check-certificate https://your-api.example.com/health

やってはいけないこと

Stack Overflowにはこんな方法を勧める回答が溢れています:

// 本番環境でこれを絶対にやらないこと
TrustManager[] trustAllCerts = new TrustManager[] {
    new X509TrustManager() {
        public X509Certificate[] getAcceptedIssuers() { return null; }
        public void checkClientTrusted(X509Certificate[] certs, String authType) {}
        public void checkServerTrusted(X509Certificate[] certs, String authType) {}
    }
};
SSLContext sc = SSLContext.getInstance("SSL");
sc.init(null, trustAllCerts, new java.security.SecureRandom());
HttpsURLConnection.setDefaultSSLSocketFactory(sc.getSocketFactory());

これは証明書の検証を完全に無効にします。すべての接続がMITM攻撃に対して脆弱になります——警告なしに、静かに。10分間のローカルデバッグには使えるかもしれませんが、デプロイされた環境では絶対に許容されません。

ヒント

インターネットからCAの証明書をダウンロードする際は、インポート前にファイルを検証してください。壊れた証明書や改ざんされた証明書は、静かに失敗するか、あるいは意図しないものをインポートしてしまう可能性があります。ToolCraftのHash Generatorはブラウザ上で完全に動作します——証明書の内容を貼り付けてSHA-256ハッシュを取得し、公式ソースと照合してください。アップロード不要、サーバーへの送信もありません。

CI/CDパイプラインに組み込む価値もあります:デプロイ前にカスタムトラストストアに期待するエイリアスがすべて含まれているかを確認するkeytool -listチェックです。深夜2時に証明書の欠落を発見するよりずっといいです。

Related Error Notes