エラーの概要
HTTPSリクエストを送信しているとき — RESTクライアント、HttpURLConnection、OkHttp、SpringのRestTemplateなど何を使っていても — Javaが次のエラーをスローします:
sun.security.validator.ValidatorException: PKIX path building failed: sun.security.provider.certpath.SunCertPathBuilderException: unable to find valid certification path to requested target
サービスは稼働しています。URLはブラウザで問題なく開けます。しかしJavaは接続を拒否します。その正確な原因と修正方法を説明します。
根本原因
Javaは独自のトラストストア — cacerts というファイル — をOSの証明書ストアとは完全に分離して管理しています。デフォルトで約150の信頼済みルートCAが含まれています。HTTPSエンドポイントに接続する際、Javaはサーバーの証明書チェーンを辿り、それらのルートのいずれかと一致するか確認します。一致しなければ例外が発生します。
次の3つの状況でこのエラーが発生します:
- サーバーがJavaのトラストストアに登録されていない自己署名証明書を使用している
- サーバーがJavaが認識していない内部/企業CAの証明書を使用している
- サーバーの証明書チェーンに中間証明書が含まれていない
修正方法1:JavaのトラストストアへCA証明書をインポートする(正しい修正方法)
本番環境での正しい対処法です。サーバーの証明書 — またはそのCAルート — をJavaのcacertsファイルに直接追加します。
ステップ1:証明書を取得する
opensslを使ってサーバーから証明書を取得します:
openssl s_client -connect your-host.example.com:443 -showcerts </dev/null 2>/dev/null \
| openssl x509 -outform PEM > server-cert.pem
サーバーがチェーンを使用している場合は、リーフ証明書だけでなく、ルートまたは中間CAの証明書が必要です。まずチェーン全体を確認しましょう:
openssl s_client -connect your-host.example.com:443 -showcerts </dev/null 2>/dev/null
1つ以上の-----BEGIN CERTIFICATE-----ブロックが表示されます。最後のブロックが通常ルートCAです — これをインポートします。
ステップ2:JVMのcacertsを見つける
# Linux / macOS
java -XshowSettings:all -version 2>&1 | grep java.home
# 次のパスを確認: $JAVA_HOME/lib/security/cacerts
# または古いJDK: $JAVA_HOME/jre/lib/security/cacerts
# 簡易検索
find /usr/lib/jvm -name cacerts 2>/dev/null
ステップ3:証明書をインポートする
keytool -import \
-alias my-server-cert \
-file server-cert.pem \
-keystore /path/to/jdk/lib/security/cacerts \
-storepass changeit
デフォルトのトラストストアパスワードはchangeitです。*「この証明書を信頼しますか?」*と表示されたら、yesと入力してください。
ステップ4:確認する
keytool -list -keystore /path/to/jdk/lib/security/cacerts \
-storepass changeit | grep my-server-cert
アプリケーションを再起動してください。PKIXエラーが解消されるはずです。
修正方法2:アプリケーション用にカスタムトラストストアを使用する
rootアクセスがない場合や、変更できない共有JVMを使用している場合は、別のトラストストアを作成してアプリをそこに向けます。
# 新しいトラストストアを作成して証明書をインポートする
keytool -import \
-alias my-server-cert \
-file server-cert.pem \
-keystore /opt/myapp/custom-truststore.jks \
-storepass mypassword
# JVMフラグでトラストストアを指定してアプリを起動する
java -Djavax.net.ssl.trustStore=/opt/myapp/custom-truststore.jks \
-Djavax.net.ssl.trustStorePassword=mypassword \
-jar your-app.jar
起動時にプログラムから設定することもできます:
System.setProperty("javax.net.ssl.trustStore", "/opt/myapp/custom-truststore.jks");
System.setProperty("javax.net.ssl.trustStorePassword", "mypassword");
修正方法3:cacertsとカスタムトラストストアをマージする
デフォルトのJava CAと独自の内部CAの両方が必要な場合があります。cacertsをコピーしてから、そのコピーに証明書をインポートします:
cp $JAVA_HOME/lib/security/cacerts /opt/myapp/custom-truststore.jks
keytool -import \
-alias my-server-cert \
-file server-cert.pem \
-keystore /opt/myapp/custom-truststore.jks \
-storepass changeit
その後、修正方法2のJVMフラグを使って設定します。アプリはデフォルトの150以上の信頼済みCAに加え、カスタムCAも信頼するようになります。
修正方法4:SSLデバッグで問題箇所を特定する
憶測で進めないでください。何かをインポートする前に、SSLデバッグ出力を有効にして正確な失敗箇所を確認しましょう:
java -Djavax.net.debug=ssl:handshake -jar your-app.jar 2>&1 | head -100
次のような出力を探します:
%% No cached client session
*** ClientHello
...
Certificate chain
chain [0] = ...
chain [1] = ...
***
FATAL ALERT: certificate_unknown
chain[N]の各エントリはサーバーが送信した証明書を示しています。トラストストアと照合して、どこにギャップがあるかを特定してください。
修正方法5:SSL検証を無効にする(内部開発/テスト環境のみ)
内部サービスに対してテストしていて、今すぐブロックを解除したい場合、この方法はすべての証明書検証をスキップします。本番環境には絶対に使用しないでください。
import javax.net.ssl.*;
import java.security.cert.X509Certificate;
public static void disableSSLVerification() throws Exception {
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("TLS");
sc.init(null, trustAllCerts, new java.security.SecureRandom());
HttpsURLConnection.setDefaultSSLSocketFactory(sc.getSocketFactory());
HttpsURLConnection.setDefaultHostnameVerifier((hostname, session) -> true);
}
このようなコードがすでにコードベースにコミットされているのを見つけた場合は、修正方法1または2に置き換えてください。これはワークアラウンドではなく、セキュリティホールです。
Docker/コンテナ環境
コンテナはJVMのcacertsをベースイメージに焼き込んでいます。実行時にインポートすることも可能ですが、イメージにビルドする方がクリーンで、起動時のオーバーヘッドも避けられます:
# オプションA:entrypoint.shでコンテナ起動時にインポートする
keytool -import -noprompt \
-alias internal-ca \
-file /certs/internal-ca.crt \
-keystore $JAVA_HOME/lib/security/cacerts \
-storepass changeit
# オプションB:イメージにビルドする(推奨)
FROM eclipse-temurin:17
COPY internal-ca.crt /tmp/
RUN keytool -import -noprompt \
-alias internal-ca \
-file /tmp/internal-ca.crt \
-keystore $JAVA_HOME/lib/security/cacerts \
-storepass changeit
予防策
- **サーバーを管理している場合は、**公開CAの証明書を使用してください — Let's Encryptは無料で自動更新されます。または内部CAルートをDockerのベースイメージに組み込んで、すべてのコンテナが最初から信頼するようにしましょう。
- **マイクロサービスを運用している場合は、**IstioやLinkerdのようなサービスメッシュがサイドカーレベルでmTLS終端を処理するため、JavaコードがRaw証明書を扱う必要がなくなります。
- **証明書インポートを自動化しましょう。**AnsibleのPlaybook、TerraformのUserdataスクリプト、またはDockerfileに
keytool -importを追加してください — そうしないと、深夜2時のインシデント対応中に誰かが手動ステップをスキップしてしまいます。 - **有効期限を監視しましょう。**実行中に証明書が期限切れになると、まったく同じエラーが発生します。
certbot renewフック、Kubernetesのcert-manager、または最低でも期限の30日前に警告を出すcronジョブを設定してください。

