クラウドフレアの安全なオリジンサーバ構成方法について調べてみた

::::note info
本記事は2023年12月初旬の確認結果を元に記載しています。進化の早いクラウドサービスに関連する情報ですので、仕様は随時変化していく可能性がある点にご注意ください。
::::

# はじめに
今回はWebアプリケーション保護の観点でオリジンサーバを安全に構成するための方法について検証してみました。こちらの記事を読んだことが今回の検証を思い立ったきっかけとなりました。
[Cloudflare Protection Bypass Vulnerability on Threat Actors’ Radar](https://socradar.io/cloudflare-protection-bypass-vulnerability-on-threat-actors-radar/)

補足: 前提知識
以下は`www.example.com`というサーバへのHTTP(S)リクエストがクラウドフレアを経由してオリジンサーバへ転送されるイメージ図です。
![image01.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2339154/67275c94-06cb-64a3-ba11-36265a0cf5d3.png)

# オリジン保護の構成例
クラウドフレアの公式ドキュメントにはオリジンを保護するための構成例が書かれています。
[Protect your origin server - Cloudflare Fundamentals](https://developers.cloudflare.com/fundamentals/basic-tasks/protect-your-origin-server/)

公式ドキュメントにはいろいろと書かれていますが、ざっくりまとめると以下の3点に集約できると思います。
* オリジンサーバのIPアドレスを知られないようにする。
* クラウドフレアを介した通信のみ許可する。(リクエストヘッダ検証、クライアント証明書認証など)
* オリジンサーバに直接通信されないようにする。(送信元IPアドレス制限やトンネリングなど)

第三者にオリジンサーバのIPアドレスを知られていなくとも、IPアドレス空間を総舐めするようなばら撒き型攻撃が着弾することもありますので、少なくともIPアドレスベースの通信制御を行うことが望ましいと考えられます。

最も簡単なのは公式ドキュメントに書かれているとおり、クラウドフレアのIPアドレスからのみリクエストを受け入れることかと思います。ただし、冒頭で紹介した指摘記事に記載があるとおり、第三者がクラウドフレアを使用して攻撃リクエストをProxyするように構成する(※)ことで、このアクセス制御を迂回されてしまう可能性が考えられます。
(※オリジンサーバのグローバルIPアドレスが判明していることが前提となります。)

今回はこの指摘事項が実現可能であるかを確認した上で、対応策のパターンをいくつか試してみたいと思います。

補足: クラウドフレアを介さずに直接リクエストされてしまうイメージ図
![image02.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2339154/f457020c-d640-e29a-154a-e8054c4e3e56.png)

補足: クラウドフレアのIPアドレスレンジ以外からのリクエストを拒否するイメージ図
![image03.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2339154/a6eb7feb-c0c8-9d56-92ad-928ade3e6e18.png)

# 検証
::::note warn
本記事ではシステムを安全に構成することを目的として検証を行っています。十分な準備・知識が無い場合、本記事のような検証をすることはお控えください。思わぬセキュリティ被害や他者への影響を生じさせてしまう可能性があります。
::::
## 1. Proxyの構成と検証
今回はクラウドフレアのFreeプランでどこまで実現性があるのかを検証してみました。
具体的な設定方法の紹介は控えますが、指摘記事に記載のとおり、クラウドフレアに任意のドメイン登録し標的サーバへのリクエストをProxyするように構成します。

ざっくり以下のような構成イメージとなります。正規のドメインが`example.com`で、第三者により構成されたProxyを`evil.example.org`とします。オリジンサーバから見て、いずれともクラウドフレアが送信元となったリクエストとなります。
![image04.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2339154/1a95e8ea-be08-4c85-31c5-491f7a919b1a.png)

### 検証
以下は通常の通信経路でサーバへリクエスト(`www.example.com`へリクエスト。上図の青色部分。)した例です。403がレスポンスされ、正規ドメインで構成されたクラウドフレアWAFで悪性のリクエストがブロックされることを確認できます。
```
$ curl https://www.example.com/?file=REDACTED
> GET /?file=REDACTED HTTP/2
> Host: www.example.com

< HTTP/2 403
< content-type: text/html; charset=UTF-8
< server: cloudflare
～～一部抜粋～～
<title>Attention Required! | Cloudflare</title>
```
その一方で、以下はProxyを経由(`evil.example.org`へリクエスト。上図の赤色部分。)して悪性リクエストを標的のオリジンサーバへ到達させた例です。
```
$ curl https://evil.example.org/?file=REDACTED
> GET /?file=REDACTED HTTP/2
> Host: evil.example.org

< HTTP/2 200
< content-type: text/html; charset=UTF-8
< server: cloudflare
～～一部抜粋～～
<title>Web Server</title>
```
オリジンサーバ側のアクセスログ。Proxyを経由したリクエストがオリジンサーバに到達してしまっていることが分かります。
```
192.0.2.1 - - [04/Dec/2023:15:11:53 +0000] "GET /?file=REDACTED HTTP/1.1" 200 134 "evil.example.org" - "-" "curl/7.68.0"
```

検証結果のとおり、クラウドフレアのFreeプランで提供されている機能を使用してリクエストをProxyさせることが可能であることが分かりました。(※ただし、詳細の説明はしませんが、オリジンサーバ側の構成が一部の条件を満たしている必要があります。)

## 2. ヘッダ検証によるアクセス制御
クラウドフレア公式ドキュメントに"HTTP Header Validation"として記載されている内容です。
[Protect your origin server | Application layer - Cloudflare Docs](https://developers.cloudflare.com/fundamentals/basic-tasks/protect-your-origin-server/#application-layer)

クラウドフレア(正規のドメイン。当記事で言うところの`example.com`)で受信したリクエストへ、カスタムのリクエストヘッダを追加するように設定します。
オリジンサーバ側では、指定したリクエストヘッダ・ヘッダ値がある場合にリクエストを受け付けるように構成することで、想定した経路からのリクエストをオリジンサーバで受け付けられるように構成してみるイメージです。

### ヘッダー検証の構成準備

構成イメージ図
![image05.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2339154/cbcd0a38-6dfa-2d0d-dcf4-aef065cb42f4.png)

いくつか構成方法があるかと思いますが、今回は変換ルールを使用してリクエストヘッダを追加してみます。
![image06.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2339154/2a4efd99-f461-625e-459e-db52430f233a.png)

`www.example.com`へのリクエストに対して、クラウドフレア側で秘密のヘッダに"開けゴマ"を付与するように設定します。
![image07.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2339154/f4e5bf3d-a31a-9911-d246-e87412abcf42.png)

### 検証
動作を確認してみます。クラウドフレアでヘッダが追加され、オリジンサーバへリクエストされることを確認できました。
```
$ curl https://www.example.com/
> GET / HTTP/2
> Host: www.example.com
```
```
192.0.2.1 - - [01/Dec/2023:09:47:57 +0000] "GET / HTTP/1.1" 200 134 "www.example.com" "OPENGOMA" "-" "curl/7.68.0"
```
次に、オリジンサーバ側でX-HIMITSU-HEADERの値がOPENGOMA以外の場合は403がレスポンスされるように構成します。今回は簡易的にapacheで設定を入れてみました。
```
RewriteEngine On
RewriteCond %{HTTP:X-HIMITSU-HEADER} !=OPENGOMA
RewriteRule ^ - [F]
```
Proxyを介したリクエスト(`evil.example.org`)には秘密のヘッダーが存在しないので、オリジンサーバ側でリクエストを拒否できました。
```
$ curl https://evil.example.org/?file=REDACTED
> GET /?file=REDACTED HTTP/2
> Host: evil.example.org

< HTTP/2 403
< content-type: text/html; charset=iso-8859-1
< server: cloudflare
～～一部抜粋～～
<title>403 Forbidden</title>
```
オリジンサーバ側のアクセスログでもリクエストが拒否されていることを確認できました。
```
192.0.2.1 - - [04/Dec/2023:15:34:20 +0000] "GET /?file=REDACTED HTTP/1.1" 403 199 "evil.example.org" "-" - "-" "curl/7.68.0"
```
水際での対応感は否めませんね。。。
## 3. クラウドフレアトンネルを使用した対応
クラウドフレア公式ドキュメントに"Cloudflare Tunnel (HTTP / WebSockets)"として記載されている構成方法です。クラウドフレア～オリジンサーバ間をトンネル接続することで、クラウドフレアからのHTTP(S)リクエストをトンネル経由で受け取るように構成します。
[Protect your origin server | Cloudflare Tunnel - Cloudflare Docs](https://developers.cloudflare.com/fundamentals/basic-tasks/protect-your-origin-server/#application-layer)

インターネットに80, 443番ポートを露出させる必要がなくなる(※)ため、IPアドレス直接指定でのリクエストを拒否するだけでなく、第三者によるクラウドフレアを使用したProxyでの接続も困難となります。
(※代わりに、クラウドフレアトンネル用にオリジンサーバからのOutbound通信を許可する必要があります。[Tunnel with firewall - Cloudflare Docs](https://developers.cloudflare.com/cloudflare-one/connections/connect-networks/deploy-tunnels/tunnel-with-firewall/))

### クラウドフレアトンネルの構成準備
構成イメージ図
![image08.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2339154/86bfbdff-1df0-f430-e8bf-5740ab90aef2.png)

クラウドフレアトンネルについてはたくさんの記事があるので、今回の検証では簡易的な設定に留めておきます。
[Set up a tunnel through the dashboard - Cloudflare Docs](https://developers.cloudflare.com/cloudflare-one/connections/connect-networks/get-started/create-remote-tunnel/)

1. クラウドフレアトンネルを作成します
![image09.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2339154/10b5ca04-214b-9179-bfa7-7b5fc8b86882.png)
2. オリジンサーバでcloudflaredをインストール・起動します
GUIに表示されたとおりにコマンドを叩くだけ。(以前より簡単になりましたね!!)
![image10.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2339154/e05256be-42ee-057d-3d61-2dff27632fb9.png)
cloudflaredをインストール後、すぐにトンネルが認識されました。
![image11.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2339154/bff81934-367f-80dc-5d42-2126956a7a96.png)
3. オリジンサーバローカルのWebサーバをパブリックホスト名として登録します
※本来はWebサーバ公開用のホスト名(当記事で言うところの`www.example.com`)を指定するところですが、比較検証のためにあえて別名(ここでは`tunnel.example.com`)としてパブリックホストを登録します。
![image12.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2339154/c77efbfb-4b6a-d948-73c6-4d41c1642c0a.png)
トンネルを作成すると、自動でDNSにCNAMEレコードが追加されます。
クラウドフレアで`tunnel.example.com`宛てのリクエストを受け付けると、オリジンサーバのグローバルIPアドレス宛てにリクエストを転送するのではなく、トンネル宛てに転送してくれるイメージですね。
![image13.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2339154/4b15635f-ab89-a367-94c0-28f633ada9a8.png)
4. ファイアウォールなどで80, 443番ポートを塞ぎます。
これはファイアウォール(AWSならセキュリティグループなど)など、オリジンサーバより前段で対応するイメージですね。(もちろんサーバローカルのfirewalldなどでも機能的には可能ですが。)

### 確認
クラウドフレアトンネルを経由した正規のリクエスト(`tunnel.example.com`宛て)は問題なくアクセスできます。
![image14.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2339154/934092b2-5600-90ee-5346-9f92871349fa.png)

一方で、オリジンサーバ側では直接HTTP(S)リクエストを受け付けていないため、Proxyを介したリクエスト(`evil.example.org`宛て)はクラウドフレアからオリジンサーバへリクエストを転送できず、エラーとなりました。
![image15.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2339154/c302eefc-d60e-70e1-1ff7-a10c9d2cebb9.png)

# まとめ
本当はAuthenticated Origin Pullsの検証も行いたかったのですが、準備の都合上今回は見送りました・・・。(また今度試してみます)

いずれにしても構成上の考慮点がいくらかありますが、もしまだ何も対応をしていないよという場合は、完全な対応で無いとしてもクラウドフレアのIPアドレスレンジからのみリクエストを許可するように構成することから始めると良いのかなと思いました。

環境にも依るかと思いますが、小規模な環境であればリクエストヘッダ検証や証明書認証を追加してみたり、ある程度の規模のある環境や複雑な環境であれば、専用のクラウドフレア出口IPアドレス(Cloudflare Aegis)を利用するのが構成としてもシンプルだと思いました。(ただ、企業向けのオプションサービスである点と、費用対効果もセットで考慮する必要はありそうですね。)