この記事は 株式会社ヘンリー Advent Calendar 2024 の22日目です。前回は @horsewin さんの 2024年を終えて 〜入社して6ヶ月が立ちました〜 でした。
ヘンリーでは副業 SRE として主にインフラ構築に関わらせてもらっています。HTTPS 通信でクライアント証明書の検証が必要になった際、利用しているロードバランサが対応していなかったために nginx を用いたリバースプロキシ構成しました。本記事ではこのときに調べた情報を元に nginx を使ってクライアント証明書を処理する方法について紹介します。
要件
今回の前提となる要件を定義します。
- クライアント証明書付きの HTTPS アクセスを処理する
- 証明書内の情報をアプリケーションサーバーで使用するため
X-Client-Certificate
ヘッダとして渡す - アプリケーションサーバーは HTTP サーバーとしての機能のみ実装されており HTTPS 終端はできない
概要と構成
HTTPS でクライアント証明書を扱う方法はいくつかあります。
最も簡単な構成は L7 Load Balancer の機能を利用する方法です。app
はアプリケーションサーバーを表します。
ただし環境によっては L7 Load Balancer でクライアント証明書を処理する機能が存在せず、この構成では実現できない場合があります。この場合は L4 Load Balancer を使うことでロードバランサ側では HTTPS 終端せず、クライアント証明書を処理するためのリバースプロキシを配置します。
今回は proxy
の部分に nginx を採用する方法を紹介します。
nginx の設定
mozilla SSL Configuration Generator をベースに必要な設定を追加します。差分のある 443 port の server
ディレクティブのみ例示します。
server {
listen 443 ssl;
listen [::]:443 ssl;
http2 on;
ssl_certificate /path/to/signed_cert_plus_intermediates;
ssl_certificate_key /path/to/private_key;
# HSTS (ngx_http_headers_module is required) (63072000 seconds)
add_header Strict-Transport-Security "max-age=63072000" always;
+
+ ssl_client_certificate /path/to/client.crt;
+ ssl_verify_client on;
+ error_page 495 /invalid_cert.html;
+ error_page 496 /no_cert.html;
+
+ location = /no_cert.html {
+ root /path/to/nginx/no_cert.html;
+ internal;
+ }
+
+ location = /invalid_cert.html {
+ root /path/to/nginx/invalid_cert.html;
+ internal;
+ }
+
+ location / {
+ proxy_pass http://app.internal;
+ proxy_set_header X-Client-Certificate $ssl_client_escaped_cert;
+ proxy_set_header X-Real-IP $remote_addr;
+ proxy_set_header X-Forwarded-Proto https;
+ proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
+ }
}
クライアント証明書の有効化と CA 設定
+ ssl_client_certificate /path/to/client.crt;
+ ssl_verify_client on;
ssl_verify_client でクライアント証明書の検証を有効化し、ssl_client_certificate で CA 証明書のパスを指定します。
ssl_verify_client
を on
に設定した場合クライアント証明書のないリクエストは 496 SSL Certificate Required
エラーが返ります。エラーにしたくない場合は optional
に設定します。
エラーページの設定
+ error_page 495 /invalid_cert.html;
+ error_page 496 /no_cert.html;
+
+ location = /no_cert.html {
+ root /path/to/nginx/no_cert.html;
+ internal;
+ }
+
+ location = /invalid_cert.html {
+ root /path/to/nginx/invalid_cert.html;
+ internal;
+ }
nginx には証明書関連のエラーコードが独自定義されています。
- 495 SSL Certificate Error
- 496 SSL Certificate Required
クライアント証明書の検証の際にこれらのエラーが発生するため、必要に応じてハンドリングします。
プロキシ設定
+ location / {
+ proxy_pass http://app.internal;
+ proxy_set_header X-Client-Certificate $ssl_client_escaped_cert;
+ proxy_set_header X-Real-IP $remote_addr;
+ proxy_set_header X-Forwarded-Proto https;
+ proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
+ }
基本的には一般的なリバースプロキシ設定ですが、クライアントの証明書の設定を upstream のアプリケーションサーバーに渡す部分の追加が必要です。
$ssl_client_escaped_cert 変数から URL エンコードされた証明書データが取得できます。これを proxy_set_header でアプリケーションサーバーへのリクエストヘッダに含めるよう設定します。
まとめ
nginx でクライアント証明書の検証と、証明書データを後段のアプリケーションサーバーに受け渡す設定について紹介しました。
最近は有名クラウドベンダーであれば各社 mTLS 機能としてクライアント証明書に対応している例がほとんどですが、まだサーバー証明書にしか対応していないサービスもあります。今後必要性は減っていくものと考えていますが、私と同じようにクライアント証明書で困ったときの助けになればと思います。