PEMから動くスタックへ — 第3部:三つのパスワード、二つのディレクトリ、一つの取り違えた証明書
シリーズ第3部。証明書は技術的に問題なかった。それでも何も起動しなかった。このパートは暗号の中ではなく設定の中に潜むエラーについて——「すべてが正しく見える」がゆえに最も見つけにくいものについて。
エラーがまったく変わらないとき
キーストアファイルがついに正しいフォーマットになり、本物のエントリーを保持するようになった後、サービスを再起動した——そして以前まったく同じエラーを受け取った:
ERROR: keystore password was incorrect javax.crypto.BadPaddingException: Given final block not properly padded
ここにプロジェクト全体の最も重要な教訓の一つが潜んでいる:変更後も何も変わらないなら、おそらく間違った場所を変更している。ファイルをさらにいじる前に、一歩引いてサービスが実際に編集しているファイルを読んでいるかどうか——そして想定しているパスワードで——確認しなければならない。
探偵作業:サービスが実際に期待するパスワードは何か?
スタックは互いに上書きし合う複数のenvファイルから設定を引いていた(デフォルトは*-default.env、カスタム調整は*-custom.env)。こうした階層的なシステムは便利だが、最終的にどの値が適用されるかを曖昧にする。
決定的なステップは推測ではなく、サービスに直接その末端で何が届いているか問いかけることだった:
docker compose run --rm --entrypoint sh keycloak -c \ 'printf "PW=[%s]\n" "$KC_SPI_TRUSTSTORE_FILE_PASSWORD"'
角括弧は意図的だ:値の末尾に見えないスペースやWindowsの改行(\r)が潜んでいないかを可視化する——[changeit ]や[wert\r]のようなものはそうでなければ見つけようがない。
結果は目を見開かせるものだった:サービスはファイルを構築したパスワードとは異なるパスワードを期待していた。実際に複数のパスワード変数が存在していた——異なる目的のために——そして私はずっと間違ったものを使い続けていた。
このスタックには実際に三つの異なるパスワード設定があり、どれも似たような名前だが異なるものを制御していた:
- 認証サーバーのシステムトラストストア用、
- HTTPSサーバーのキーストア(サービス自身がTLSで提示する証明書)用、
- 三番目のサービス固有のバリアント用。
さらに別のサービス(RESTのAPIミドルウェア)があり、そのトラストストアパスワードはスタートスクリプトにハードコードされており、envファイルでは設定できなかった。
証拠: サービスがファイルを共有する場合、一つのパスワードで合意しなければならない——ファイルは一つしか持てない。
解決策は技術的なものではなく組織的な決断だった:すべてを同じ値に統一する。一つのサービスのパスワードがハードコードされていて変更できなかったため、残りの変数すべてをその値に設定した:
KC_HTTPS_KEY_STORE_PASSWORD=<TRUSTSTORE_PW> KC_HTTPS_TRUST_STORE_PASSWORD=<TRUSTSTORE_PW> KC_SPI_TRUSTSTORE_FILE_PASSWORD=<TRUSTSTORE_PW>
——そしてもちろん、まさにそのパスワードですべてのキーストアファイルを再構築した。
結論: パスワード不一致の場合、問いは「ファイルのパスワードは正しいか?」だけではない。「この特定のサービスが期待するものと一致しているか?」も問うべきだ。そして:複数のサービスがファイルを共有するなら、一つのパスワードで合意する必要がある。
余談:envファイルと改行コード
値がWindows上で作られた可能性のあるenvファイルから来ているため、編集のたびに以下を実行する価値がある:
sed -i 's/\r$//' service-custom.env
パスワード値末尾の\rは見えないが、changeitをchangeit\rに——つまり別のパスワードに——変えてしまう。
パスワードが整理された後、認証サーバーは動いた。しかしミドルウェアコンポーネントは認証サーバーへのTLS接続で依然として信頼エラーを投げていた——一致するCA証明書をインポートしたにもかかわらず。
再び同じ原則が成立した:修復にもかかわらず何も変わらないなら、サービスはおそらく別のファイルを読んでいる。ではコンテナが実際にマウントしているものを確認しよう:
docker inspect <container> \
--format '{{range .Mounts}}{{.Source}} -> {{.Destination}}{{"\n"}}{{end}}'
証拠: 認証サーバーは一つのディレクトリ(/host/ssl/)から証明書を引いており、ミドルウェアはまったく別のディレクトリ(/host/installation/certs/)から引いていた。私はずっと間違った場所の正しいファイルを修正し続けていた。
結論: 「コンテナが実際に読むファイルはどこか?」は早めに明示的に答えるべき問いだ——すべてのサービスが同じパスを共有すると思い込むな。docker inspect(またはdocker compose config)が真実を示す。
プロジェクト全体で最も陰険な取り違え:トラストストアにはcorp-caというエイリアスと正しいtrustedCertEntryを持つエントリーがあった。完璧に見えた。それでも信頼チェックは失敗した。
理由:エントリーはcorp-caと名付けられていたが、その内容は実際のCAルート証明書ではなくサーバーのエンド証明書(ERPホストのもの)だった。序盤に誤ってファイルを割り当て、以来ずっと正しい名前の下に間違った証明書を引きずり続けていた。
それを暴いたのはフィンガープリントの比較だった。エイリアスは信頼できない——フィンガープリントだけが真実を語る:
証拠: キーストア内のエントリーはその内容について何も証明しない。信頼できるアイデンティティはフィンガープリントだけだ。
# What's in the truststore?
keytool -list -keystore root.p12 -storetype PKCS12 -storepass <TRUSTSTORE_PW>
# -> corp-ca ... Fingerprint (SHA-256): 79:75:09:...
# What does the server actually serve? Look at the chain:
echo | openssl s_client -connect keycloak.example.local:5443 -showcerts 2>/dev/null \
| awk '/BEGIN CERT/{c++} {print > "cert"c".pem"}'
for f in cert*.pem; do
openssl x509 -in "$f" -noout -subject -issuer -fingerprint -sha256
done
真のCAルート証明書では、subjectとissuerが同一だ(自己署名だから)。まさにそれがトラストストアに属するものだ——そしてそのフィンガープリントは私が持っていたものとはまったく別のものだった。正しい証明書(一致するフィンガープリントで確認済み)のインポートだけが信頼問題を解決した。
結論: 証明書は名前ではなくフィンガープリントで検証せよ。名前は嘘をつくが、フィンガープリントはつかない。そして信頼チェーンにはCAルート証明書(subject = issuer)が必要であり、エンド証明書ではない。
第3部で学んだこと
- 変更後もエラーが変わらなければ、変更は間違った場所にあった。一歩引け。
- どの値がその末端に届いているかサービスに問いかけよ(括弧付きの
printf "[%s]")、頭の中でenvの階層を解こうとするのではなく。 - 類似した複数のパスワード変数は古典的な罠——サービスが実際に読むものを明確にせよ。
- サービスがファイルを共有するなら、同じパスワードが必要だ。統一する方がしばしばジャグリングより実用的だ。
docker inspectがコンテナが実際にマウントするファイルを明らかにする——パスについての思い込みに頼るな。- 証明書はフィンガープリントで検証せよ、エイリアスでは決してない。名前は嘘をつくが、フィンガープリントはつかない。
- トラストストア = CAルート証明書(subject = issuer)、エンド証明書ではない。
フィナーレでは、壊れたファイルとはもはや無関係で信頼アーキテクチャ自体に関わる最後のハードルについて:一つのサービスが内部と公開の認証局を同時に信頼しなければならない場合、どうするか?