問題の概要
デプロイを一瞬で止めてしまうものといえば、不可解なmTLSエラーです。セキュアなAPIや社内管理ダッシュボードを構築しているとき、ブラウザが突然行き詰まることがあります。アプリの画面ではなく、真っ白なページにこの一行だけが表示されます:
400 Bad Request: No required SSL certificate was sent
このエラーは、サーバーがデジタルIDカードを要求したにもかかわらず、何も提示されなかったことを意味します。サーバーはクライアント証明書の提示を要求するように設定されていますが、ハンドシェイクはアプリケーションロジックに到達する前に失敗しています。
根本原因の分析
通常のHTTPSは一方向です。サーバーがクライアントに対して自身のアイデンティティを証明します。相互TLS(mTLS)はこれを双方向にします。この場合、サーバー設定(通常はNginxまたはApache)は ssl_verify_client on; に設定されています。ブラウザ、スクリプト、またはモバイルアプリが有効な証明書を提示しなければ、サーバーは即座に接続を切断します。
ほとんどの失敗はいくつかの見落としから発生します:
- クライアント証明書がブラウザのローカルストアにインストールされていない。
- 証明書が昨夜有効期限切れになったか、まだ「有効開始日」に達していない。
- サーバーの
ca.crtが、クライアント証明書に署名した認証局を認識していない。 - cURLやPostmanなどの自動化ツールが
.crtおよび.keyファイルを参照していない。
ステップ1:サーバー側の設定を確認する
まず、NginxがどのCA(認証局)を信頼すべきかを把握していることを確認します。通常 /etc/nginx/sites-available/default にあるサイト設定ファイルを開きます:
server {
listen 443 ssl;
server_name api.example.com;
ssl_certificate /etc/nginx/certs/server.crt;
ssl_certificate_key /etc/nginx/certs/server.key;
# mTLSに必要な設定
ssl_client_certificate /etc/nginx/certs/ca.crt;
ssl_verify_client on;
location / {
proxy_pass http://localhost:3000;
}
}
ssl_client_certificate のパスを慎重に確認してください。このファイルには、ユーザー証明書に署名したルートCA(および中間CA)が含まれている必要があります。最近CAを更新してこのファイルを更新し忘れた場合、すべてのリクエストが400エラーで失敗します。
ステップ2:cURLでテストする
ブラウザを排除して問題を切り分けます。curl を使うことで、キャッシュの問題や非表示のUI設定を排除できます。ターミナルに client.crt と client.key ファイルを用意してください。
curl -v --cert client.crt --key client.key https://api.example.com
ここで200 OKが返れば、サーバーは正常です。問題はブラウザへの証明書インポートにある可能性が高いです。それでも400が返る場合、証明書がNginx設定で定義されたCAによって署名されていない可能性があります。発行者を素早く確認しましょう:
openssl x509 -in client.crt -text -noout | grep Issuer
出力は、サーバーの ssl_client_certificate パスにある証明書のコモンネーム(CN)と一致している必要があります。
ステップ3:ブラウザ用に証明書を準備する
ChromeやSafariなどのブラウザは、生の .crt と .key のペアを受け付けません。OSが安全に扱えるよう、PKCS#12バンドル(.p12 または .pfx ファイル)にパッケージ化する必要があります。
次のコマンドでバンドルを作成します:
openssl pkcs12 -export -out client.p12 -inkey client.key -in client.crt -certfile ca.crt
エクスポートパスワードの入力を求められます。強力なパスワードを選択してください。このパスワードは、ダウンロードフォルダに保存されている間、秘密鍵を保護します。
ヒント: こういったバンドルのパスワードを生成する際は、ToolCraftのパスワードジェネレーターを使っています。完全にローカルブラウザ上で動作するため、セキュリティ資産がプライベートに保たれます。
client.p12 の準備ができたら、次の手順に従ってください:
- Chromeの設定 → プライバシーとセキュリティ → セキュリティ に移動します。
- 「証明書の管理」をクリックします(OSのキーチェーンまたは証明書マネージャーが開く場合があります)。
client.p12ファイルをインポートし、エクスポートパスワードを入力します。- ブラウザを完全に再起動します。サイトにアクセスすると、新しい証明書を選択するポップアップが表示されるはずです。
ステップ4:中間CAのギャップを修正する
ルートCAが直接の署名者でない場合があります。中間CAがあなたの証明書を発行した場合、Nginxには検証チェーン全体が必要です。ルートCAだけを提供すると、サーバーが信頼の「橋渡し」ができないためハンドシェイクが失敗します。
中間証明書とルート証明書を単一のバンドルにまとめます:
cat intermediate.crt root.crt > full_ca_chain.crt
Nginxの設定を更新して ssl_client_certificate をこの full_ca_chain.crt ファイルに向け、サービスをリロードします。
検証と監視
リアルタイムでログを監視して修正を確認します。mTLSハンドシェイクが成功した場合、アクセスログの見え方が通常とは異なります。
tail -f /var/log/nginx/error.log /var/log/nginx/access.log
より詳細なデバッグのために、Nginxのログフォーマットに $ssl_client_verify を追加してください。「SUCCESS」「NONE」「FAILED:subject-mismatch」などが明示的にログに記録され、ハンドシェイクが失敗した正確な理由がわかります。
予防策
深夜3時の障害を避けるために、以下の対策を実施してください:
- ライフサイクルの自動化: KubernetesのCert-ManagerやHashiCorp Vaultを使用して、有効期限が切れる前に証明書をローテーションします。
- 有効期限アラート: CAまたはクライアント証明書の有効期限の30日前、15日前、7日前に通知するモニタリングを設定します。
- オンボーディングドキュメント: チームメンバー向けにシンプルな社内ガイドを作成します。ブラウザの証明書UIは使い勝手が悪く、新しい変更を反映させるには完全な再起動が必要なことが多いです。

