PEMから動くスタックへ — 第4部:一つのサービスが二つの世界を信頼しなければならないとき
シリーズのフィナーレ。証明書はきれいで、パスワードは統一され、パスは正しかった。それでも一つのサービスが起動しなかった——今回の理由は誤りではなく、信頼のアーキテクチャについての誤解だった。
後退に見えた前進
ミドルウェアがついに内部の認証サーバーを信頼した後、新たな信頼エラーが現れた。一見して悩ましい——PKIXが再び。しかしエラー内のIssuerが変わっていた:
Before: https://keycloak.example.local:5443/... <- the internal auth server Now: https://auth-provider.example.com/... <- a public cloud service
これは実は成功だった:内部サーバーはもはや不信任されていない。エラーは今や二番目の認証プロバイダー——有料アドオンモジュールに必要な公開クラウドサービス——に関するものだった。
教訓:頑固なエラーの詳細が変わったとき(ここではエラー内のホスト名)、それは前のハードルが突破されたサインだ。エラーメッセージを注意深く読め——差異が情報だ。
本当の問題:狭すぎるトラストストア
設定はJavaオプションでサービスを自作トラストストアに縛っていた:
-Djavax.net.ssl.trustStore=/mw/root.p12
ここにJavaの知っておくべき性質が潜んでいる:独自のトラストストアを設定した瞬間、それはデフォルトのトラストストアを完全に置き換える。つまりサービスは私の内部CAだけを信頼し——公開CAは一つも信頼しなくなった。
内部の認証サーバーへの接続には完璧だった。しかし公開クラウドプロバイダーはごく普通の公開CA(どのブラウザも知っているような)の証明書を使っている。それが今や欠けていた——つまり、信頼は壊れていた。
解決策:一つのトラストストアに二つの世界
要件は明確だった:サービスは内部CAと公開CAを同時に信頼しなければならない。きれいな解決策はJavaのバンドルされたデフォルトトラストストア(cacerts、すべての公開CAを既に含む)をベースにし、そこに独自のCAを追加することだ。
デフォルトのcacertsはJDKの中に存在し、通常はchangeitというパスワードを持つ。しかし私たちのサービスは別の値を期待するため、コピー時にストアパスワードを切り替え、その後CAをインポートしなければならない——いずれにせよcacertsとkeytoolがすでに存在するコンテナ内でやるのがベストだ:
docker compose run --rm -v /host/ssl:/out:ro --entrypoint sh rest-api -c '
CACERTS=$(find / -name cacerts 2>/dev/null | head -1)
cp "$CACERTS" /tmp/work.p12
# switch store password from "changeit" to our value
keytool -importkeystore -noprompt \
-srckeystore /tmp/work.p12 -srcstorepass changeit \
-destkeystore /tmp/root.p12 -deststorepass <TRUSTSTORE_PW> -deststoretype PKCS12
# add our own CA
keytool -importcert -noprompt -alias corp-ca \
-file /out/corp-ca-root.pem \
-keystore /tmp/root.p12 -storepass <TRUSTSTORE_PW>
cat /tmp/root.p12
' > /host/installation/certs/root.p12
検証で目標達成が確認できた:
keytool -list -keystore root.p12 -storetype PKCS12 -storepass <TRUSTSTORE_PW> \ | grep -iE 'entries|corp-ca' # -> Your keystore contains 151 entries # -> corp-ca, ..., trustedCertEntry
151エントリー:約150の公開標準CAプラス私の内部CA一つ。これでサービスは両方の世界を信頼する。そして——次の起動でついに待望の行が:
Tomcat started on port 8080 (http) with context path '/mw'
教訓:カスタムのJavaトラストストアはデフォルトを置き換える、拡張するのではない。サービスが内部と公開の両方のターゲットに到達するなら、cacertsをベースに独自のCAを加えてトラストストアを構築せよ——さもなければ誤って世界の半分を締め出すことになる。
最後のテストのように、nginxのウェブサーバーも引っかかった:
nginx: [emerg] cannot load certificate "/etc/nginx/certs/issued.crt": PEM_read_bio_X509_AUX() failed ... wrong tag ... nested asn1 error
第1部で見たパターンだ:二重Base64エンコードされたファイル。しかしここでの本当の教訓は別にある——nginxはJavaサービスと根本的に異なる動きをする。
証拠: KeycloakとミドルウェアがパスワードつきのPKCS#12コンテナを求めるのに対し、nginxはプレーンなクリアテキストのPEMを期待する:証明書は-----BEGIN CERTIFICATE-----ブロック、鍵は-----BEGIN PRIVATE KEY-----として、パスワードなしでコンテナパッケージングなしで。
幸い、すでに動いているPKCS#12サーバーキーストアから両方を取り出せた:
# full chain (server certificate + CA) as PEM openssl pkcs12 -in issued.p12 -passin pass:<TRUSTSTORE_PW> -nokeys -out full_chain.pem # private key as unencrypted PEM openssl pkcs12 -in issued.p12 -passin pass:<TRUSTSTORE_PW> -nocerts -nodes -out private.key
最後の小さな落とし穴:OpenSSLはPEMブロックの前にBag Attributesのクリアテキスト行を書くことがあり、それでnginxのバージョンによってはつまずく。きれいに切り取るには:
sed -n '/-----BEGIN CERTIFICATE-----/,/-----END CERTIFICATE-----/p' full_chain_raw.pem > full_chain.pem
そして——ほぼ最も重要なこととして——証明書と鍵が一致するペアであることを常に確認せよ。二つのハッシュは同一でなければならない:
openssl x509 -in full_chain.pem -noout -modulus | openssl md5 openssl rsa -in private.key -noout -modulus | openssl md5
結論: 異なれば、読めるファイルはあるのにTLSはいずれにせよ失敗する——ハンドシェイク時にしか表面化しない誤りだ。
最大の収穫
エラーメッセージに満ちた一日の後、このプロジェクト一つを超えて通用するいくつかの原則へと本質を凝縮できる:
- 「証明書」は一様なものではない。PEM、DER、PKCS#12、トラストストア、キーストア、CAルート、エンド証明書——これらは異なるルールを持つ異なるものだ。失われた時間のほとんどはそれらを混同することから来る。
- ファイル拡張子は嘘をつく。ファイルの中身を本当に教えてくれるのは
file、head、openssl x509 -text、keytool -list——名前ではない。 - ツールが違えば真実が違う。OpenSSLがファイルを受け入れるからといってJavaも受け入れるとは限らない。最終的にファイルを使うツールで検証せよ。
- エラーメッセージはヒントであって評決ではない。「パスワードが違う」は実際には「アルゴリズムが違う」を意味していた。「接続拒否」は「サービスが動いていない」を意味していた。文字通りに読んで、技術的に何を意味するか問え。
- 変更後に何も変わらなければ、変更は間違った場所にあった。この一つの気づきで何時間も節約できたはずだ。同じ修復を繰り返す前にパス、ファイル、値を確認せよ。
- 名前ではなくフィンガープリントで検証せよ。エイリアスやファイル名は何も証明しない。証明書のフィンガープリントだけが信頼できるアイデンティティだ。
- コマンドだけでなく信頼アーキテクチャを理解せよ。カスタムトラストストアはデフォルトを置き換える。トラストストアにはエンド証明書ではなくCAが必要だ。サービスが異なれば期待するフォーマットも異なる。モデルを理解した者はコマンドをほとんど暗記する必要がない。
最終的に、完全なスタックが動いた:認証サーバー、二つの信頼アンカーを持つミドルウェア、ウェブサーバー。一つの.pemファイルから何十もの相互作用するコンテナへの旅——そしてすべてのハードルが、次回は数分しかかからない何かを教えてくれた。
シリーズ完。最後まで読んでくれてありがとう——次の証明書エラーが実り多いものであることを願って。