本文へスキップ
ecdsa.com
← すべての記事

Java の PKIX path building failed:原因と正しい対処法

Java は、ブラウザーが信頼するものをそのまま信頼するわけではありません。独自のトラストストアを使い、不足している中間証明書をデフォルトでは取得せず、どの原因で失敗しても同じメッセージを出します。以下の原因はいずれも JDK 25 で再現したもので、どれに該当するかを判別するコマンドも併記しています。

要点:PKIX path building failed: ... unable to find valid certification path to requested target は、サーバーの証明書から、JVM が実際に読み込んだトラストストア内の信頼済みルートのいずれかまで、チェーンを構築できなかったことを意味します。そのトラストストアは通常 $JAVA_HOME/lib/security/cacerts であり、OS のストアではありません。よくある原因は、そのファイルに含まれていないプライベート CA やプロキシの CA、中間証明書を送信しないサーバー、想定とは別のトラストストアを使っている JVM、新しいルートを含まない古い JDK、そして SNI で名前が送信されていないことです。どれに該当するかは openssl s_client -showcerts と -Djavax.net.debug=ssl:handshake:verbose で突き止められます。そのうえで、トラストストアかサーバーを修正してください。すべてを受け入れるトラストマネージャーは、決してインストールしてはいけません。

メッセージの意味

現在の JDK では、例外の全文は次のようになります。JDK 21 と 25 では、TLS アラート名が先頭に付きます。

JDK 25.0.4.1、プライベート CA を使うサーバーに HttpClient で接続
javax.net.ssl.SSLHandshakeException: (certificate_unknown) PKIX path building failed: sun.security.provider.certpath.SunCertPathBuilderException: unable to find valid certification path to requested target

各部分の意味は次のとおりです。

  • PKIX は、RFC 5280 で定められた X.509 証明書パスのモデルです。Java の実装は sun.security.provider.certpath にあります。
  • パス構築(path building)は、2 段階のうちの最初の段階です。Java は、サーバーのリーフ証明書からトラストアンカーまでをつなぐ証明書の並びを、何か 1 つ見つけようとします。それに成功した場合に限り、パス検証(path validation)で日付、鍵用途、制約、失効が確認されます。2 段階目の失敗にはそれぞれ専用のメッセージがあり、たとえば PKIX path validation failed: validity check failed がそうです。
  • 「requested target」は、サーバーの証明書を指します。

フレームワークはこの例外をラップします。たとえば Spring の ResourceAccessException、Maven の「Could not transfer artifact」、Gradle の「Could not GET」です。それでも PKIX のテキストは原因の連鎖(cause)の中に残っているので、スタックトレース全体を検索してください。

5 つの原因

5 つとも、eclipse-temurin:25.0.4.1_1-jdk-noble と小さな java.net.http.HttpClient のプログラムで再現しました。テスト用の PKI は、ルート CA、中間 CA(「Issuing CA 1」)、api.example.test のリーフ証明書で構成されています。

#状況結果
1プライベート CA、JDK デフォルトの cacertsPKIX path building failed
2cacerts のコピーにルートをインポート、サーバーはリーフ証明書と中間証明書を送信HTTP 200
3同じトラストストア、サーバーはリーフ証明書のみを送信PKIX path building failed
43 と同じ、-Dcom.sun.security.enableAIAcaIssuers=true を指定PKIX path building failed
4b4 と同じ、さらに -Dcom.sun.security.allowedAIALocations=http://aia.example.test:8080/ を指定HTTP 200
5サーバーは SNI で証明書を選択、クライアントは SNI を送信HTTP 200
6同じサーバー、-Djsse.enableSNIExtension=false を指定PKIX path building failed
7同じサーバー、IP アドレスを使った URL(SNI なし)PKIX path building failed

1. Java が信頼していない CA から発行された証明書

最もよくあるケースです。社内サービス、Kubernetes の Ingress、社内の証明書を使う Nexus や Artifactory、データベース、そして TLS インスペクションを行うプロキシ越しに見るすべてのサイトが該当します。このプロキシは、通信を自身のルートで署名し直します。そのルートを Windows や macOS にインストールすればブラウザーでは問題がなくなりますが、ほとんどのプラットフォームで Java は OS のストアを読みません。JDK の cacerts(この Temurin 25 のビルドでは 118 エントリ)は、「Example Corp Root CA」が何者なのかを知りません。

2. サーバーが中間証明書を送信していない

サーバーは、リーフ証明書と、ルートに至るまでの中間証明書(ルート自体は含まない)を送信しなければなりません。ところが、リーフ証明書だけを設定したサーバーは少なくありません。ブラウザーは中間証明書をキャッシュしたり、証明書の Authority Information Access(AIA)拡張にある URL から取得したりするため、この誤りが表に出ないことがよくあります。Java はデフォルトでは、そのどちらも行いません。表の 3 行目と 4 行目がこの状況です。ルートは信頼されていますが、Java が「Issuing CA 1」を一度も目にしないため、パスを構築できません。

サーバーが送信している内容は、次の 2 行で確認できます。

openssl s_client -showcerts:ポート 8443 のサーバーはチェーンを送信し、8444 はリーフ証明書だけを送信
$ openssl s_client -connect api.example.test:8443 -servername api.example.test -showcerts </dev/null 2>/dev/null | grep -E '^ *[0-9]+ s:|^ *i:'
 0 s:CN = api.example.test
   i:CN = Example Corp Issuing CA 1
 1 s:CN = Example Corp Issuing CA 1
   i:CN = Example Corp Root CA
$ openssl s_client -connect api.example.test:8444 -servername api.example.test -showcerts </dev/null 2>/dev/null | grep -E '^ *[0-9]+ s:|^ *i:'
 0 s:CN = api.example.test
   i:CN = Example Corp Issuing CA 1

最後の i: 行に、信頼しているルート以外の名前が出ていれば、中間証明書が不足しています。証明書チェーン構築ツールは、貼り付けた証明書のバンドルを正しい順に並べ、不足している中間証明書を AIA 経由で取得して、サーバーが送信すべきチェーンを正確に示します。

3. JVM が想定とは別のトラストストアを使っている

ルートをインポートしたのに、まだ失敗する場合のよくある理由は次のとおりです。

  • 別の JDK:IDE、Gradle のツールチェーン、Maven Wrapper、アプリケーションサーバー、Docker イメージは、それぞれ独自の JDK を持ち込みます。keytool -cacerts が編集するのは、その keytool を実行している JDK のトラストストアだけです。
  • 置き換えられたトラストストア:-Djavax.net.ssl.trustStore=only-corp.p12 は cacerts に追加されるのではなく、それを置き換えます。社内のルートだけを含むストアでは、すべての公開サイトへの接続が失敗します。
JDK 25:社内のルートだけを含むトラストストアで letsencrypt.org に接続
javax.net.ssl.SSLHandshakeException: (certificate_unknown) PKIX path building failed: sun.security.provider.certpath.SunCertPathBuilderException: unable to find valid certification path to requested target
  • トラストストアのパスの入力ミス:javax.net.ssl.trustStore で指定したファイルが存在しなくても、JDK 25 はエラーになりません。何も知らせずに cacerts にフォールバックするので、インポートしたはずのルートはそこにありません。それがわかるのは、トラストマネージャーのデバッグ出力だけです。
JDK 25:-Djavax.net.ssl.trustStore=/tmp/app-trustore.p12(入力ミス)、-Djavax.net.debug=ssl:trustmanager
Inaccessible trust store: /tmp/app-trustore.p12
trustStore is: /opt/java/openjdk/lib/security/cacerts
Fatal (CERTIFICATE_UNKNOWN): PKIX path building failed: sun.security.provider.certpath.SunCertPathBuilderException: unable to find valid certification path to requested target (

存在はするものの、信頼済みの証明書を 1 つも含まないストアは、別のエラー the trustAnchors parameter must be non-empty で失敗します。

  • フレームワーク側のストア:Spring Boot の SSL バンドル、Tomcat のコネクター、Kafka、データベースドライバーには独自のトラストストア設定があり、それぞれの接続では JVM のデフォルトよりもそちらが優先されます。

どのファイルが読み込まれたかは、JSSE のデバッグ出力の 1 行目で確定できます(診断を参照してください)。

4. サーバーのルートに対して JDK が古すぎる

JDK のアップデートにはそれぞれ、その時点での信頼済みルートのスナップショットが含まれています。チェーンが新しいルートで終わる公開サイトは、そのルートより前の JDK では失敗します。Let's Encrypt の互換性のページに例があります。ISRG Root X2 が信頼されるのは、Java 8u401、11.0.22、17.0.10、21.0.2 以降です。cacerts のコピーからルートを 2 つ削除すると、古いストアで何が起きるかがわかります。

cacerts のコピーから ISRG Root X1 と X2 を削除した JDK 25
--- letsencrypt.org (chains to ISRG roots)
javax.net.ssl.SSLHandshakeException: (certificate_unknown) PKIX path building failed: sun.security.provider.certpath.SunCertPathBuilderException: unable to find valid certification path to requested target
--- www.google.com (different CA)
HTTP 200

一部の公開サイトだけが失敗する場合は、そのサイトのチェーンがつながるルートを確認し、次に JDK のアップデートレベルを確認してください。何年も前に cacerts からコピーし、一度も更新していないトラストストアでも同じことが起こります。

5. SNI がないため、サーバーが別の証明書を送信する

複数のホスト名を扱うサーバーは、ClientHello に含まれる Server Name Indication(SNI)に基づいて証明書を選びます。SNI がない場合はデフォルトの証明書を送信し、テストではそれが自己署名の default.invalid でした。この証明書はどのルートにもつながらないため、エラーはやはり PKIX になります(6 行目と 7 行目)。Java が SNI を送信しないのは、次の場合です。

  • URL に IP アドレスを使っている場合(TLS は SNI での IP リテラルを禁止)
  • -Djsse.enableSNIExtension=false が設定されている場合(多くは古い回避策の名残)
  • カスタムの SSLSocketFactory がホスト名なしでソケットを作成している場合

-servername を付けずに openssl s_client を実行すると、そうしたクライアントが受け取るものを確認できます。

診断の手順

1. サーバーが送信するチェーンを確認します。可能であれば、失敗しているマシンから確認してください。そのマシンには、プロキシが別の証明書を返している可能性があります。

サーバーが送信する内容と、各証明書に署名した者
openssl s_client -connect host.example:443 -servername host.example -showcerts </dev/null 2>/dev/null \
  | grep -E '^ *[0-9]+ s:|^ *i:'

インスペクションを行うプロキシを経由している場合、最上位の発行者は公開 CA ではなく、プロキシの CA です。それだけで、このエラーの説明がつきます。

2. どのトラストストアを読み込み、何を受信したかを Java に出力させます。デバッグ出力は長く、今回のテストでは失敗したハンドシェイク 1 回で約 5,000 行あったので、フィルターをかけてください。

JSSE のデバッグ出力(フィルター適用)
java -Djavax.net.debug=ssl:handshake:verbose -jar app.jar 2>&1 \
  | grep -E 'trustStore is|Consuming server Certificate|"subject"|"issuer"|PKIX'
JSSE のデバッグ出力の主要な行(JDK 25、プライベート CA、抜粋)
trustStore is: /opt/java/openjdk/lib/security/cacerts
...
javax.net.ssl ... Consuming server Certificate handshake message (
"issuer"             : "CN=Example Corp Issuing CA 1",
"subject"            : "CN=api.example.test",
...
"issuer"             : "CN=Example Corp Root CA",
"subject"            : "CN=Example Corp Issuing CA 1",
...
javax.net.ssl ... Fatal (CERTIFICATE_UNKNOWN): PKIX path building failed: sun.security.provider.certpath.SunCertPathBuilderException: unable to find valid certification path to requested target (

1 行目は、トラストストアのファイルを示しています。証明書のブロックは、サーバーが送信したチェーンをリーフ証明書から順に示しており、最後のブロックの発行者が、Java がルートにつなげられなかった証明書です。その発行者が、信頼されているはずのルート CA や中間 CA であれば、そのファイル自体を確認してください。

keytool:JVM が読み込んだストアにルートはあるか
$ keytool -list -cacerts -storepass changeit -alias corp-root
keytool error: java.lang.Exception: Alias <corp-root> does not exist

別のファイルを確認するには keytool -list -keystore /path/to/store.p12 を使います。-v を付けると各エントリのサブジェクトとフィンガープリントが出力されるので、エイリアスではなく証明書そのもので照合できます。

3. パス構築の詳細を見るには、-Djava.security.debug=certpath を追加します。ビルダーが検討した発行者の候補がそれぞれ出力され、AIA については取得が許可されたかどうかも出力されます。

検証を有効にしたままの対処法

中間証明書が不足している場合は、まずサーバーを修正します。nginx、Apache、ロードバランサー、Ingress で、完全なチェーン(リーフ証明書、続いて中間証明書)を設定してください。Java だけでなく、すべてのクライアントがその恩恵を受けます。

CA がプライベート CA である場合や、プロキシが通信を署名し直している場合は、ルートを追加したアプリケーション専用のトラストストアを使います。公開サイトに引き続き接続できるように、cacerts のコピーから作成してください。

アプリケーション専用のトラストストア:JDK のルートと社内のルート
cp "$JAVA_HOME/lib/security/cacerts" app-truststore.p12
keytool -importcert -noprompt -alias corp-root -file corp-root-ca.pem \
  -keystore app-truststore.p12 -storepass changeit
java -Djavax.net.ssl.trustStore=app-truststore.p12 -Djavax.net.ssl.trustStorePassword=changeit -jar app.jar

Maven、Gradle、IDE から実行するテストのように自分で直接起動しないツールには、JAVA_TOOL_OPTIONS か、そのツール独自の JVM オプションで同じプロパティを渡してください。一方、keytool -importcert -cacerts は JDK のデフォルトのストアを変更します。この変更はその JDK 上のすべてのアプリケーションに反映されますが、次に JDK をアップグレードすると消えてしまいます。

追加のルートが必要なのが 1 つの連携先だけなら、そのクライアントにだけ専用のトラストストアを持たせます。次のコードは、プライベートなサービスと公開サイトの両方に対して実行したものです。

Java:1 つの HttpClient 専用の SSLContext
KeyStore ts = KeyStore.getInstance("PKCS12");
try (InputStream in = Files.newInputStream(Path.of("app-truststore.p12"))) {
    ts.load(in, "changeit".toCharArray());
}
TrustManagerFactory tmf = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm());
tmf.init(ts);
SSLContext ssl = SSLContext.getInstance("TLS");
ssl.init(null, tmf.getTrustManagers(), null);
HttpClient client = HttpClient.newBuilder().sslContext(ssl).build();
// https://api.example.test:8443/ -> HTTP 200
// https://letsencrypt.org/ -> HTTP 200

OkHttp、Apache HttpClient、Netty、gRPC、JDBC ドライバーはいずれも、同じ方法で構築した SSLContext または TrustManager を受け付けます。

コンテナーでは、USE_SYSTEM_CA_CERTS が設定され、*.crt ファイルが /certificates/ にマウントされていれば、Eclipse Temurin のイメージが起動時に追加の CA をインポートします。

docker run -e USE_SYSTEM_CA_CERTS=1 -v ./certs:/certificates/ eclipse-temurin:25.0.4.1_1-jdk-noble(抜粋)
Adding certificate with alias Example Corp Root CA to the JVM truststore
Your keystore contains 240 entries

この処理が実行されるのは、イメージのデフォルトのエントリポイントを使う場合だけです。ほかのベースイメージでは、Dockerfile で keytool -importcert を実行してください。

macOS で JDK 23 以降を使う場合、-Djavax.net.ssl.trustStoreType=KeychainStore-ROOT を指定すると Java がシステムのルートキーチェーンを使うので、IT 部門がインストールしたルートが信頼されます(JDK-8320362)。Windows では、-Djavax.net.ssl.trustStoreType=Windows-ROOT を指定すると、Windows のルートストアで同じことができます。

公開 CA のルートが不足している場合は、JDK をアップグレードします。古い JDK の cacerts を手作業で修正しても、それ以外のセキュリティ修正は適用されないままです。

「すべての証明書を信頼する」が対処にならない理由

ネットで最もよく見かける回答は、checkServerTrusted が何もしない X509TrustManager で、多くの場合 true を返す HostnameVerifier とセットになっています。これで例外は消えますが、それは TLS を、通信相手を誰も検証しない暗号化に変えてしまうからです。接続に割り込める者なら誰でも、任意の証明書を提示し、通信を読み取り、改ざんできます。悪意のある Wi-Fi ネットワーク、侵害されたルーター、設定を誤ったプロキシ、DNS ハイジャックなどがその例です。さらに、こうしたコードは「一時的な」目的を過ぎても残り続け、次のプロジェクトにコピーされがちです。上記の対処法はどれも数分で済み、検証も有効なままです。

サーバー自身の証明書をトラストアンカーとしてインポートするのも同じです。うまくいくのは次の更新までで、証明書の有効期間の短縮が進む今では、それは数週間以内ということです。証明書そのものではなく、証明書を発行する CA を信頼してください。

チェックリスト

  1. 失敗しているホストで openssl s_client -showcerts を実行し、チェーンが揃っているか、最上位の発行者が誰かを確認します。
  2. -Djavax.net.debug=ssl:handshake:verbose で、JVM が読み込んだファイル(trustStore is: の行)を確認します。
  3. そのファイルに対して keytool -list を実行し、発行元のルートが含まれているかを確認します。
  4. 経路上に TLS インスペクションを行うプロキシがあるかを確認します。ある場合、必要なのはそのプロキシのルートです。
  5. SNI が送信されているかを確認します。IP リテラル、jsse.enableSNIExtension、カスタムのソケットファクトリをチェックしてください。
  6. JDK が、そのサイトのルートに対応できるほど新しいかを確認します。

出典

  • RFC 5280:証明書パス検証(§6)
  • Oracle、JDK 21.0.10 リリースノート:Authority Info Access 拡張に含まれる URI のフィルタールール(com.sun.security.allowedAIALocations)
  • JDK 25 に同梱されている java.security ファイル($JAVA_HOME/conf/security/java.security)の「X.509 AuthorityInfoAccess caIssuer URI Filtering」セクション
  • JDK-8320362:Keychain キーストアからトラストアンカーの証明書を読み込む機能(KeychainStore-ROOT、JDK 23)
  • Let's Encrypt、証明書の互換性
  • Eclipse Temurin のコンテナーイメージ:エントリポイントの USE_SYSTEM_CA_CERTS オプション

このページの結果はすべて、2026年10月20日に eclipse-temurin:25.0.4.1_1-jdk-noble(OpenJDK 25.0.4.1、テスト用サーバーには OpenSSL 3.0.13)で得たもので、テスト用の証明書はコンテナー内で生成しました。