nginxでクライアント証明書を処理するリバースプロキシを立てる

この記事は [株式会社ヘンリー Advent Calendar 2024](https://qiita.com/advent-calendar/2024/henry) の22日目です。前回は @horsewin さんの [2024年を終えて 〜入社して6ヶ月が立ちました〜](https://sizu.me/horsewin/posts/ox5xi5m9s35h) でした。

---

ヘンリーでは副業 SRE として主にインフラ構築に関わらせてもらっています。HTTPS 通信でクライアント証明書の検証が必要になった際、利用しているロードバランサが対応していなかったために nginx を用いたリバースプロキシ構成しました。本記事ではこのときに調べた情報を元に nginx を使ってクライアント証明書を処理する方法について紹介します。

## 要件

今回の前提となる要件を定義します。

- クライアント証明書付きの HTTPS アクセスを処理する
- 証明書内の情報をアプリケーションサーバーで使用するため `X-Client-Certificate` ヘッダとして渡す
- アプリケーションサーバーは HTTP サーバーとしての機能のみ実装されており HTTPS 終端はできない

## 概要と構成

HTTPS でクライアント証明書を扱う方法はいくつかあります。

最も簡単な構成は L7 Load Balancer の機能を利用する方法です。`app` はアプリケーションサーバーを表します。

```mermaid
flowchart LR
Client --　HTTPS --> LB[L7 Load Balancer]
LB -- HTTP --> app
```

ただし環境によっては L7 Load Balancer でクライアント証明書を処理する機能が存在せず、この構成では実現できない場合があります。この場合は L4 Load Balancer を使うことでロードバランサ側では HTTPS 終端せず、クライアント証明書を処理するためのリバースプロキシを配置します。

```mermaid
flowchart LR
Client -- HTTPS --> LB[L4 Load Balancer]
LB -- HTTPS --> proxy
proxy -- HTTP --> app
```

今回は `proxy` の部分に nginx を採用する方法を紹介します。

## nginx の設定

[mozilla SSL Configuration Generator](https://ssl-config.mozilla.org/#server=nginx&version=1.27.3&config=modern&openssl=3.4.0&guideline=5.7) をベースに必要な設定を追加します。差分のある 443 port の `server` ディレクティブのみ例示します。

```diff: nginx.conf
 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 設定

```diff
+    ssl_client_certificate /path/to/client.crt;
+    ssl_verify_client on;
```

[ssl_verify_client](http://nginx.org/en/docs/http/ngx_http_ssl_module.html#ssl_verify_client) でクライアント証明書の検証を有効化し、[ssl_client_certificate](http://nginx.org/en/docs/http/ngx_http_ssl_module.html#ssl_client_certificate) で CA 証明書のパスを指定します。

`ssl_verify_client` を `on` に設定した場合クライアント証明書のないリクエストは `496 SSL Certificate Required` エラーが返ります。エラーにしたくない場合は `optional` に設定します。

### エラーページの設定

```diff
+    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

クライアント証明書の検証の際にこれらのエラーが発生するため、必要に応じてハンドリングします。

### プロキシ設定

```diff
+    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](http://nginx.org/en/docs/http/ngx_http_ssl_module.html#var_ssl_client_escaped_cert) 変数から URL エンコードされた証明書データが取得できます。これを [proxy_set_header](http://nginx.org/en/docs/http/ngx_http_proxy_module.html#proxy_set_header) でアプリケーションサーバーへのリクエストヘッダに含めるよう設定します。

## まとめ

nginx でクライアント証明書の検証と、証明書データを後段のアプリケーションサーバーに受け渡す設定について紹介しました。

最近は有名クラウドベンダーであれば各社 mTLS 機能としてクライアント証明書に対応している例がほとんどですが、まだサーバー証明書にしか対応していないサービスもあります。今後必要性は減っていくものと考えていますが、私と同じようにクライアント証明書で困ったときの助けになればと思います。