OpenSSL CLI:本番環境のHTTPSデバッグとプライベートCAの構築

Security tutorial - IT technology blog
Security tutorial - IT technology blog

午前2時14分の本番アラート

午前2時14分、ナイトスタンドの上でスマートフォンが振動しました。ロードバランサーが502 Bad Gatewayエラーを吐き出し、ログはSSL_ERROR_BAD_CERT_DOMAINで埋め尽くされていました。フロントエンドチームは証明書を更新したと断言していましたが、ブラウザの表示はそれを否定していました。こうした状況では、UIダッシュボードの情報は往々にしてあてになりません。OpenSSL CLIだけが提供できる「生のデータ」が必要になります。

先月、一連のSSHブルートフォース攻撃に対処して以来、私は「動くはずだ」という言葉を鵜呑みにするのをやめました。すべてを検証します。OpenSSLは、設定スクリプトの内容や管理コンソールの表示を気にしません。ハンドシェイク中にサーバーが実際に提示しているものを正確に表示してくれるため、ノイズを排除して問題を特定するための究極のツールとなります。

証明書が失敗する3つの理由

ほとんどの接続失敗は、3つの原因に集約されます。証明書の期限切れ、秘密鍵と公開証明書の不一致、または中間証明書チェーンの欠落です。ジュニア管理者がメインの証明書だけをコピーし、中間証明書(バンドル)を忘れてしまうケースをよく見かけます。これは中間証明書をキャッシュしている一部のブラウザでは動作しますが、モバイルユーザーやAPIクライアントでは無残に失敗します。

マイクロサービス化によって、この問題はさらに複雑になります。Nginx、HAProxy、Traefikを経由して20以上の異なるサブドメインにトラフィックをルーティングしている場合、たった一つの誤った.pemファイルが、数千人のユーザーに対して「この接続ではプライバシーが保護されません」という警告を表示させることになります。パスの入力ミス一つで、本番環境のゲートウェイがダウンすることもあるのです。

適切なデバッグツールの選択

SSL Labsのようなオンラインスキャナーは非常に便利ですが、ファイアウォールの内側を見ることはできません。ステージング環境やVPN内の内部APIをデバッグしている場合、それらのツールは役に立ちません。curl -vを試すこともできますが、ハンドシェイクが失敗した具体的な理由が抑制されてしまうことがよくあります。

OpenSSL CLIを直接使用するのが、最も信頼できる方法です。実際のブラウザ接続をシミュレートし、デプロイ前にファイルを検査し、さらには内部ラボ用の独自の認証局(CA)として機能させることも可能です。

フェーズ1:稼働中の接続のデバッグ

サイトがダウンしたときは、まずサーバーが実際に何を送信しているかを確認することから始めます。ここで重宝するのがs_clientコマンドです。Server Name Indication (SNI) フラグを忘れないようにしてください。1つのIPで複数のサイトをホストしている場合、-servernameフラグがないと、サーバーはどの証明書を表示すべきか判断できません。

# リモートサーバーの証明書を確認する
openssl s_client -connect itfromzero.com:443 -servername itfromzero.com

出力の中の “Certificate chain” と “Verification” セクションを確認してください。もし “Verification error: unable to get local issuer certificate” と表示されていれば、サーバーに中間証明書バンドルが不足しています。大量のテキストをスキップして有効期限だけを確認したい場合は、出力をx509ユーティリティにパイプします。

# 有効期限のみを抽出する
openssl s_client -connect google.com:443 -servername google.com 2>/dev/null | openssl x509 -noout -dates

フェーズ2:デプロイ前のファイル検証

Nginxをリロードして本番環境の停止リスクを冒す前に、ローカルファイルを検証しましょう。エンジニアが4時間も設定ファイルのデバッグに費やした結果、単なるキーペアの不一致だったというケースを何度も見てきました。証明書が秘密鍵と一致しない場合、Webサーバーは起動に失敗するか、すべてのハンドシェイク要求を拒否します。

証明書のメタデータの検査

誰が証明書を発行したかを確認し、サブジェクト代替名 (SAN) をチェックするには、以下のコマンドを実行します。

openssl x509 -in server.crt -text -noout

モジュラス一致テスト

これはデプロイ前の必須チェックです。証明書内の公開鍵は、秘密鍵と完全に一致している必要があります。これは、それぞれの「モジュラス(modulus)」のMD5ハッシュを比較することで検証できます。これら2つの文字列が同一でない場合は、デプロイしてはいけません。

# 証明書のモジュラスハッシュを確認する
openssl x509 -noout -modulus -in server.crt | openssl md5

# 秘密鍵のモジュラスハッシュを確認する
openssl rsa -noout -modulus -in server.key | openssl md5

フェーズ3:内部ラボ用のプライベートCA構築

開発環境で自己署名証明書の警告が出るたびに「詳細設定 -> 続行」をクリックするのはもうやめましょう。プロフェッショナルなアプローチは、プライベートCAを構築することです。1つのルート証明書を作成し、それをチームのマシンで信頼させれば、ローカルテストに必要なだけの証明書を署名して発行できます。

1. ルートCAの生成

まず、マスターキーとルート証明書を作成します。この鍵は本番環境のパスワードと同様に扱ってください。もしこれが盗まれれば、ネットワーク上のあらゆるサイトを偽装される可能性があります。

# 4096ビットのRSA鍵を作成する
openssl genrsa -out MyInternalCA.key 4096

# ルート証明書を作成(10年間有効)
openssl req -x509 -new -nodes -key MyInternalCA.key -sha256 -days 3650 -out MyInternalCA.pem

2. サービス証明書の作成

api.dev.localというローカルサイトがある場合、秘密鍵と証明書署名要求 (CSR) が必要になります。

# サービス鍵を生成する
openssl genrsa -out api.dev.local.key 2048

# CSR(証明書署名要求)を生成する
openssl req -new -key api.dev.local.key -out api.dev.local.csr

3. 自前のCAでリクエストに署名する

ChromeやSafariなどのモダンブラウザで証明書を信頼させるには、サブジェクト代替名 (SAN) が必要です。これを含めるために、dev.extという名前の小さな設定ファイルを作成します。

authorityKeyIdentifier=keyid,issuer
basicConstraints=CA:FALSE
keyUsage = digitalSignature, nonRepudiation, keyEncipherment, dataEncipherment
subjectAltName = @alt_names

[alt_names]
DNS.1 = api.dev.local

次に、証明書に署名します。macOSやiOSでは有効期間が長い証明書が拒否されるため、期限を825日に設定している点に注意してください。

openssl x509 -req -in api.dev.local.csr -CA MyInternalCA.pem -CAkey MyInternalCA.key \
-CAcreateserial -out api.dev.local.crt -days 825 -sha256 -extfile dev.ext

MyInternalCA.pemをOSの信頼されたルート証明書ストアにインポートします。完了すると、api.dev.localには本番環境と全く同じように、クリーンな緑色の鍵アイコンが表示されるようになります。

長期的な運用のためのベストプラクティス

OpenSSLは、決して難解なフラグの羅列である必要はありません。体系的なワークフローに従えば、ほとんどのTLSの問題を数分で解決できます。

常にs_clientで信頼チェーンを確認し、リロード前には必ずモジュラスの一致を検証し、チーム内ではプライベートCAを使用して「証明書警告への慣れ」を防ぐようにしましょう。

セキュリティは習慣です。SSHを堅牢にするにせよ、TLSハンドシェイクを完璧にするにせよ、習得したツールがインフラの信頼性を定義します。CLIに慣れ親しむことで、午前2時のアラート対応もずっと楽になるはずです。

Share: