はじめに
「ルートCAがAmazonに変わるらしいけど、cacertsには入ってるから大丈夫でしょ」
SSL証明書の更新通知を受け取ったとき、こう判断して終わりにした経験はないでしょうか。筆者も最初はそうでした。しかし実際に調査を進めると、ステージング環境と本番環境でまったく異なるトラストストアを使っていたという事実が判明し、「大丈夫なはず」が根拠のない思い込みだったことに気づかされました。
本記事では、外部APIのSSL証明書がAmazon Trust Servicesへ移行するケースを題材に、以下を解説します。
- JavaのJVMがトラストストアを選ぶ3段階の優先順位と、よくある誤解
- OV→DV(組織認証→ドメイン認証)変更がアプリに与える影響の判断軸
-
curlの疎通確認とJava実行環境の間にあるギャップ - PKCS12形式の
keytoolが踏みやすい日付表示の罠 - AIを「監査役」として使い、自分の判断の盲点を補完するワークフロー
最終的な結論は「追加対応不要」でした。しかし大事なのは結論ではなく、その結論を根拠ある確信に変えるまでのプロセスです。
1. 問題の始まり:「Amazon Trust Servicesに変わります」
ある日、連携している外部サービスベンダーからこんな通知が届きました。
2026年11月中旬より、弊社APIサーバーのSSL証明書を更新します。ルート認証局がAmazon Trust Servicesに変更されます。必要に応じてご対応ください。
通知の内容はシンプル。しかし「必要に応じて」の判断がじつは難しく、以下のような疑問が積み重なっていきます。
- JDKのcacertsにAmazon Root CAは元から入っているはず——でも本当にそのcacertsを使っているのか?
- OV→DVという認証タイプの変更も含まれているが、これはアプリに影響するのか?
- ステージングで疎通確認できれば本番も大丈夫か?
ひとつひとつ解きほぐしていきます。
2. 落とし穴①:JVMはどのトラストストアを使うのか
3段階の参照優先順位
Javaアプリケーションがトラストストアを選ぶ際、以下の優先順位があります。
優先度 1: -Djavax.net.ssl.trustStore(JVM起動オプションで明示指定)
優先度 2: $JAVA_HOME/lib/security/jssecacerts(ファイルが存在する場合のみ)
優先度 3: $JAVA_HOME/lib/security/cacerts(デフォルト)
「cacertsに入ってるから大丈夫」が言えるのは、優先度1・2が適用されていない場合だけです。
実環境で起きていたこと:ステージングと本番で違うファイルを使っていた
実際にJavaプロセスの起動オプション(ps -ef)を確認したところ、環境によって構成がまったく異なっていました。
| 項目 | ステージング環境 | 本番環境 |
|---|---|---|
-Djavax.net.ssl.trustStore |
あり(カスタムPKCS12ファイル) | なし |
| 実際に使うトラストストア | カスタムファイル | JVMデフォルト cacerts |
ステージング環境は -Djavax.net.ssl.trustStore でカスタムのPKCS12ファイルを明示指定していました。本番環境にはその指定がなく、jssecacerts も存在しなかったため、デフォルトの cacerts が使われていました。
「ステージングで確認したから大丈夫」が成り立たない典型例です。本番環境で独立して確認しなければ、この差異には永遠に気づけませんでした。
useSystemProperties() の誤解
コードには次の記述がありました。
HttpClients.custom().useSystemProperties()
「useSystemProperties() = システムの cacerts を使う」と最初は解釈しましたが、これは誤りです。正確にはJavaシステムプロパティ(-D オプション)の値をHTTPクライアントの設定に反映するという意味です。
つまり、-Djavax.net.ssl.trustStore が設定されていればそちらが優先されます。useSystemProperties() は「どのトラストストアを使うか」を決める命令ではなく、「JVM起動オプションの内容に素直に従う」という宣言です。
3. 落とし穴②:OV→DV変更、アプリへの影響は?
今回の通知には、ルートCA変更だけでなく認証タイプのOV→DV変更も含まれていました。
OVとDVの違い
| 項目 | OV(Organization Validation) | DV(Domain Validation) |
|---|---|---|
| 検証内容 | ドメイン所有 + 組織実在性 | ドメイン所有のみ |
| 証明書のSubject | CN・O(組織名)・C(国)などを含む | CNのみ(組織情報なし) |
OVからDVに変わると、証明書のSubjectから組織名(O フィールド)等が削除されます。ブラウザの錠前マークに表示される企業名が消えるのはこのためです。
影響があるかどうかは3点チェックで判断できる
① 証明書ピン留め(Certificate Pinning)の有無
接続先証明書のフィンガープリントや特定フィールドをコードにハードコードしていないか確認します。
// こういうコードがあると影響あり(例)
if (!cert.getSubjectX500Principal().getName().contains("O=ExampleCorp")) {
throw new CertificateException("Unexpected issuer organization");
}
② Subject の O(組織名)フィールドへの依存
接続先証明書の O フィールドを読み取って条件分岐するロジックがないか確認します。DVにはこのフィールドが含まれません。
③ issuer の OU(組織単位)フィールドへの依存
同様に、発行元の OU フィールドに依存したロジックも影響を受ける可能性があります。
今回のコードは HttpClients.custom().useSystemProperties() のみで、カスタムSSLContextなし・証明書ピン留めなし。3点すべて該当なしで影響ゼロと判断できました。
4. curl の疎通確認とJava実行の間にあるギャップ
「curl -v https://api.example.com で SSL certificate verify ok が出た——Javaも通るはず」
これが成立しない理由を整理します。
curlとJavaは別々の証明書ストアを使う
curl → /etc/pki/tls/certs/ca-bundle.crt(OSの証明書ストア)
Java → cacerts または -Djavax.net.ssl.trustStore で指定したファイル
curlはOSの証明書ストアを参照します。Javaは独自のトラストストアを持ちます。OSにルートCAが登録されていても、Javaのトラストストアにないケースは十分起こりえます。逆もまた然りです。
curlの疎通確認が有効なのは「プロキシ経路の確認」と「証明書チェーンが届いているかの確認」です。「Javaアプリが通るか」の根拠にはなりません。
プロキシ環境では TLSインスペクションの有無も確認する
システムがプロキシ経由で外部接続している場合、**TLSインスペクション(SSLインターセプト)**の有無を確認する必要があります。
TLSインスペクションを行うプロキシは接続先の証明書をいったん終端し、自己署名証明書で再発行します。この場合、アプリが実際に検証する証明書はベンダーのものではなく社内プロキシが発行したものになり、ルートCA変更の影響が「透過的に見えなくなる」場合があります。
# プロキシ経由で issuer を確認
curl -v --proxy http://proxy.example.internal:8080 https://api.example.com 2>&1 | grep "issuer"
# TLSインスペクションなし → issuer がベンダーのCA(Amazon等)
# TLSインスペクションあり → issuer が社内プロキシのCA
今回の環境では issuer: Amazon RSA 2048 M01 が確認でき、TLSインスペクションなしを確認しました。
HTTP 404 は「疎通成功」のサイン
確認用エンドポイントへのアクセスで HTTP/2 404 が返ってきたとき、「つながらなかった?」と思う方もいるかもしれません。しかしこれは成功です。
HTTPS通信は2段階で構成されます。
- SSLハンドシェイク(証明書検証)
- HTTPリクエスト/レスポンス
SSL certificate verify ok → HTTP 404 という流れは、①を正常に通過した上でサーバーが「このパスは存在しない」と応答したことを意味します。SSLエラーがあれば SSL certificate problem でハンドシェイク自体が失敗し、HTTPレスポンスは何も返ってきません。
5. keytool + PKCS12 の日付表示トラップ
カスタムトラストストア(PKCS12形式)の内容確認でつまずいたポイントがあります。
keytool -list -keystore truststore.p12 -storetype PKCS12 -storepass changeit
出力されたエントリ日が、すべて「本日の日付」になっていたのです。「いつ追加されたルートCAかわからない」と一瞬焦りましたが、これはPKCS12形式の仕様です。
JKS(Java KeyStore)形式は各エントリに作成日時を内部保持しますが、PKCS12形式は保持しません。 そのため keytool は実行時の日付をそのまま表示します。
実際の更新日を知りたければ、ファイル自体のタイムスタンプを確認します。
stat /opt/java/ssl/truststore.p12
# → Modify: 2026-02-26 17:44:xx ← これが実際の更新日
「keytoolのエントリ日 = 証明書が追加された日」と思い込むと、判断を誤ります。
6. AIを「監査役」として使う
ここまでの調査で「おそらく対応不要」という結論が見えてきました。しかし「おそらく」のまま本番判断を下すのは心許ない。そこで活用したのがAIによる多角的レビューです。
「答えを聞く」ではなく「穴を探させる」
人間は自分が正しいと思っている仮説に沿って証拠を集めがちです(確証バイアス)。AIはフラットに応答するため、自分が見落とした視点を補完するのに向いています。
プロンプトの骨格はこれだけです。
以下は私がSSL証明書変更対応として調査した内容と結論です。
この判断に穴や見落としがあれば指摘してください。
確認できていない項目があれば追加で教えてください。
【調査結果】
- Javaプロセスの起動オプションを確認。本番環境は -Djavax.net.ssl.trustStore の指定なし
- $JAVA_HOME/lib/security/jssecacerts の不在を確認
- keytool で cacerts に Amazon Root CA 1〜4 の収録を確認
- curl --proxy でプロキシ経由の疎通確認完了(SSL verify ok)
- コードに証明書ピン留めなし
【結論】
追加のトラストストア更新は不要
「大丈夫ですか?」と聞くのではなく「穴を探してほしい」と依頼するのがポイントです。前者は肯定的な回答を引き出しやすく、後者は批判的な視点を引き出せます。
実際に指摘された3つの盲点
盲点①:keytoolのエントリ日はPKCS12では信用できない
「keytoolで確認したら全エントリの日付が今日になっていた」と伝えると、「PKCS12形式はエントリの作成日を保持しない。keytool実行日が表示されているだけ。ファイルのModifyタイムスタンプを確認すべき」という指摘を受けました(→ 前セクションの内容です)。
盲点②:openssl s_client には -servername オプションが必要なケースがある
SNI(Server Name Indication)を使用しているサーバーでは、-servername を省略すると正しい証明書が返ってこない場合があります。
# SNI対応サーバーへは servername を明示する
openssl s_client -connect api.example.com:443 \
-servername api.example.com 2>/dev/null | \
openssl x509 -noout -issuer -dates
盲点③:ステージング・本番間のトラストストア同一性確認
「本番のcacertsにAmazon Root CAが入っている」を確認した後、「ステージングと本番のcacertsが本当に同一ファイルか」を確認するよう指摘されました。
# SHA256で環境間を比較
sha256sum $JAVA_HOME/lib/security/cacerts
今回は両環境でハッシュが一致し、Amazon Root CA 1の有効期限(〜2038年)も確認できました。これで「対応不要」の根拠が揃いました。
AIを使う際の注意点
AIが「大丈夫です」と言っても、それは最終判断ではありません。あくまで「自分が見落としていないか」を確認するためのセカンドオピニオンです。AIの出力は必ず自分でソースを当たって検証してください。
7. まとめ:SSL証明書更新時のチェックリスト
本記事で扱った内容を、再現可能な手順としてまとめます。
Step 1:JVMが使うトラストストアを特定する
# Javaプロセスの起動オプションを確認
ps -ef | grep java | grep -v grep
# -Djavax.net.ssl.trustStore=xxx があればそのファイルを使う
# なければ jssecacerts の存在確認へ
find $JAVA_HOME -name "jssecacerts" 2>/dev/null
# 存在しなければ cacerts を使う
Step 2:トラストストアにルートCAが収録されているか確認する
# cacerts の場合
keytool -list -v -keystore $JAVA_HOME/lib/security/cacerts \
-storepass changeit | grep -A 5 "Amazon Root CA 1"
# PKCS12カスタムファイルの場合
keytool -list -v -keystore /path/to/truststore.p12 \
-storetype PKCS12 -storepass changeit | grep -A 5 "Amazon Root CA 1"
Step 3:証明書チェーンの完全性を確認する
# 直接接続できる場合
openssl s_client -connect api.example.com:443 \
-servername api.example.com 2>/dev/null
# → Verify return code: 0 (ok) が出れば正常
# プロキシ経由の場合
openssl s_client -connect api.example.com:443 \
-servername api.example.com \
-proxy proxy.example.internal:8080 2>/dev/null
Step 4:プロキシのTLSインスペクション有無を確認する
curl -v --proxy http://proxy.example.internal:8080 \
https://api.example.com 2>&1 | grep "issuer"
# issuer がベンダーのCA → TLSインスペクションなし(正常)
# issuer が社内CA → TLSインスペクションあり(要追加確認)
Step 5:OV→DV変更の影響有無をコードレビューする
□ カスタム SSLContext / X509TrustManager の実装がないか
□ 証明書の Subject・Issuer を読み取るロジックがないか
□ 証明書フィンガープリントのハードコードがないか
→ 3点すべてなし:影響なし
おわりに
今回の調査を通じて得た教訓を一言で言うと、「ステージングで確認済み」は「本番で安全」を意味しないということです。
起動オプションの有無というシンプルな差異が、使われるトラストストアをまったく別のものにしていました。こういった差異は、本番環境を直接確認しにいかなければ永遠に見えません。
また、AIを「監査役」として使うことで、確証バイアスが生む盲点を補完できました。「この判断は正しいか?」ではなく「この判断に穴はないか?」という問いかけ方が、有益なフィードバックを引き出すコツです。
SSL証明書の更新通知は、有効期限だけを見て終わりにしがちです。次回の通知を受け取ったとき、このチェックリストを思い出してもらえれば幸いです。