CloudFront の WAFを設定する

# はじめに

AWSで現在 WAFと組み合わせて使用できるのは、CloudFront、ALB、API Gateway と複数ありますが、ここでは CloudFront を使った WAFを設定してみます。

基本的に WAF は L7レイヤーのトラフィックを検査する HTTP Proxy サーバーがアーキテクチャーの基本なので、これらの HTTPトラフィックを扱う製品と組み合わせて使う事ができます。

CloudFront というと CDN が基本の使い方のなので、キャッシュを必ずしないといけないイメージがありますが、キャッシュをさせずに素通しのプロキシーサーバーとしての使用も可能です。つまり、キャッシュをしないシンプルな WAFとして使用する事も可能です。

また、WAFは HTTPトラフィックの中身を覗いて攻撃を判断するという特性から、負荷の高い処理であり、作成されたルールによっては大量のCPUリソースを消費します。さらに、アプリケーションの脆弱性に対する攻撃は、それ以外の単純なDDoS攻撃の中に混ぜる形で行われ、アプリケーションへの攻撃が行われている事を埋もれさせる事で、攻撃の存在自体を隠す事が一般的な手段として用いられています。そのため、WAFで防御するアプリケーションへの攻撃と、DDoS攻撃への対応は切り離す事ができません。

CloudFront は、CDNが基本アーキテクチャーであり、エッジでの分散処理がその本質です。これは、大量のリソースを必要とする処理や、DDoS の大量のトラフィックを分散して処理するための最適なアーキテクチャーです。CloudFrontでWAFの処理を行うというのは、技術的に理にかなった方法になっています。

# 前提条件

この環境の前提条件は以下のような環境です。CloudFront のリクエストのフォワード先となる Web Server(CLBでL4ロードバランシング)は既に構築されているという前提になります。

ユーザーのドメインは、ocp4.work というドメインを使用します。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/99425/0810e856-85e6-caf3-6667-c3ea1c121cfc.png)

- 上図では `test1.ocp4.work` というドメインを例にして書いてますが、CNAMEは `*.ocp4.work` に対して行っていて、CloudFront の証明書も `*.ocp4.work` のワイルドカードで取得しているので、後段のオリジンのドメインは `test2.ocp4.work` でも `api1.cop4.work` でもこの構成で対応できます。
- `CLB` はL4のLoadBalancerなので、HTTP(S)のトラフィックの中身を見ずにTCP/IPレベルで、HTTPSリクエストを最終段の`Web Server`にフォワードします。`CLB`では HTTPSは復号化されないため、`CloudFront` と `Web Server` の間 が HTTPSが終端される範囲になります。
- 最終段の `Web Server` の証明書は何らかの方法で取得されており `Web Server`に埋め込まれているものとします。この手順の中では触れません。
- `Client`から`test1.ocp4.work`向けた HTTPSリクエストは、`CloudFront` の`dfvhmf4jh47fc.cloudfront.net`や、`CLB` の`a15601ff3de5e4bc28e38e49f50d7d-1736380914.ap-northeast-1.elb.amazonaws.com` などのいろいろなドメインを通過しているように見えますが、HTTPリクエスト内の「Hostヘッダー」の値は`test1.ocp4.work`のまま変わらず最終段の`Web Server`にまで到達します。

補則：上記の絵はこの手順の説明に必要な部分だけ抽出して、簡略化して書いていますが、実際に実験に用いた構成は、以下のような Kubernetes (OpenShift)環境になります。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/99425/7a7d080a-3b3b-4568-fb1e-4522cb519aac.png)


# WAF のルール (web ACL)を作成する

最初に CloudFront の設定から参照するための WAF のルールを作成します。

メニューから WAF を検索します。「Create web ACL」をクリックします。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/99425/79b81226-9f52-c6da-975d-eacf122d5d76.png)

画像の通りに入力します。特に注意すべき点としては、これは CloudFront で使用する予定の web ACL なので
**Resource type** : CloudFront distributions 
を選択します。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/99425/3cf1daf8-c733-6e34-293b-ba77f045632e.png)

次に「Add rules」-> 「Add managed route groups」からルールを選択します。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/99425/2577fb9c-ad6f-ed81-9f8c-bcb1332c3c01.png)



いろいろ選択できますが、ここではあくまで腕ならしとして「AWS managed roule group」から適当に選択してみます。(本来はアプリケーションに合わせた適切なルールを選択する必要があります）
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/99425/9e6aec08-3abc-82e7-e37a-7bc36c288552.png)

ここでは、「SQL database」を選択します。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/99425/07f78d9e-e345-b231-b187-c86f90dac118.png)
実際には保護対象のWebサービスに合わせた必要最小限のものを選択するようにしましよう。例えばSQLを使うサービスを持ってないのに「SQL database」向けのルールを選択する必要はありません。また正規表現を使ったルールはCPUを過大に消費する事があるので十分に気を付ける必要があります。WAFのルールが原因でアプリケーションのレスポンスの悪化を引き起こす可能性があります。


「Default web ACL action for requests that don't match any rules」は、「Allow」を選択します。これは前述のルール設定にマッチしないトラフィックを許可する。という意味です。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/99425/848a2fd1-4167-5348-d844-40df23509921.png)

ルールの評価順を変更する画面です。今は一つしか無いので、デフォルトのまま「Next」で大丈夫です。必要であれば後からも変更できます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/99425/c4ef2f76-58b4-4017-81c8-046f26bc2c79.png)


監視用のMetrixの設定なので、ここはとりあえず全てチェックを入れて「Next」をクリックしておきます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/99425/7d841583-8a65-d5f2-06fd-800a2d80c40c.png)


これまでの設定の最終確認画面です。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/99425/3b489a43-09ca-e179-3c54-07aba23fd200.png)

「Create web ACL」をクリックすれば完成です。

## IP Set を作成する

オプショナルですが、良くある要件なので、作成した web ACL に、アクセスを許可する IPアドレスを追加してみます。そのためにはまず「IP Sets」を作成します。
AWS WAF - IP sets から、「IP Sets」を作成します。

ここでは以下の画像の通りに設定しました。もちろん環境によって異なります。
この例では IP address に関しては私の自宅のISP (Internet Service Provider) のネットワークを登録しています。このISPのネットワークからだけアクセスできるような設定を作成します。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/99425/133badaf-baf2-cd47-cb4c-6b36bcca96f1.png)
「Create IP set」をクリックすれば完成です。

以下のような 「IP Sets」が完成しました。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/99425/e49efb67-70ba-6900-6911-d31cd367cda8.png)

## IP Set をweb ACLに追加する

今度は先ほど作成した 「web ACL」である「my-web-acl」の画面に戻ります。「Rules」のタブより「Add rules」=>「Add my own rules and rule groups」をクリックします。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/99425/b2405b30-e5aa-9b75-3cb7-e69f540c513f.png)


IP Set を使用するための新しいルールを作成します。指定は以下の画像の通りです。先ほど作成した IP Set「my-providers-ips」を指定して Allow しています。完成した「Add rule」をクリックします。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/99425/8573c83d-8c50-48b4-b9e2-7453f8de7d10.png)

次は優先順位の変更です。順番は環境によって適切なものが異なりますが、ここでは「allow-ip-list」が、最後に評価されるように指定します。「Move down」で一番下に動かします。「Save」をクリックします。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/99425/bd94a169-edb0-15d9-7f5b-083f01227288.png)

これで作成した WAFのRule (web ACL)、「my-web-acl」に、特定のIPアドレスを許可するルールが追加されました。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/99425/3e6f0697-4fc6-1b20-1204-499ac08c35ba.png)

作成した web ACL「my-web-acl」は、「AWS-AWSManagedRulesSQLiRuleSet」ルールを使ってトラフィックの評価を行い、攻撃パターンとマッチしなけれれば、次に「allow-ip-list」のIPリストと照合を行います。「allow-ip-list」のIPリストは許可リストなので、このリストのIPレンジと一致するようであれば、トラフィックをオリジンに通過させます。

もしこのルール順を反対にしてしまうと、「allow-ip-list」のIPリストに一致した時点で、オリジンへのアクセスが許可されてしまう事になるので、「AWS-AWSManagedRulesSQLiRuleSet」による評価は行われなくなります。ルールの評価順とその動作は良く考えて決める必要があります。


# ACM を使った CF用 DV証明書の発行

## CloudFront で HTTPS を終端する

WAFはその性質上、HTTPトラフィックの中身を把握する必要があるので、HTTPSの暗号化を必ず復号化する必要があります。そのためには、CloudFrontでHTTPSを終端する必要があります。つまり CloudFrontに証明書をインストールしておく必要があります。

ACM (AWS Certificate Manager)を使用して証明書を発行すると、CloudFront でその証明書を使用する事ができ、証明書の更新も自動化できるので、ここでは ACM を使って証明書を発行します。

ACM で発行できる証明書は、DV (Domain Validation) による証明書です。

DV 証明書は発行が簡単な分、身元を簡単に隠せるため詐欺サイトなどでも普通に用いられる証明書です。DV証明書には暗号化以上の意味はありません。使用するドメインが身元が確認された組織により運営されたものであると、証明書ベンダーに確認されている事を証明したい場合、つまり「証明書」としての意味を持たせたい場合は、OV(Organization Validation) 以上の証明書をおすすめします。(但し取得費用が必要です）

## ACM を使った証明書を発行する

AWS Certificate Manager のサービス画面を検索して、**「バージニア北部」** リージョンを選択します。CloudFront の設定から ACMの証明書を参照するためには、「バージニア北部」に作成する必要があるという制限があるためです。

「証明書をリクエスト」をクリックします。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/99425/35a1fc0e-488b-3fd5-b0f9-3af37c29d060.png)

「次へ」をクリックします。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/99425/4a9c6631-35cf-c22e-0030-10ad59a2e1dc.png)

以下では、ワイルドカードを使った「\*.ocp4.work」という証明書を取得します。(証明書をリクエストするドメイン自体は取得済みである必要があります)
**検証方法**：「DNS検証」
**キーアルゴリズム**：「RSA 2048」を選びます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/99425/f93742ac-f86f-1a2e-5046-6537f3325427.png)
「リクエスト」をクリックして証明書をリクエストします。

証明書の一覧から、証明書のリクエストの状況が確認できます。証明書IDのリンクの部分をクリックして詳細を確認します。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/99425/0a4c5c32-efec-4a43-373f-bec4a8ca2e41.png)

以下の画面の「CNAME名」と「CNAME値」の列に注目します。次の手順でこれらの値を、証明書発行対象ドメインを管理している DNS Server に CNAMEレコードの値として登録します。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/99425/7b6391c4-80b6-d196-ce2d-a945899c6752.png)

私の場合は、お名前.com で「ocp4.work」というドメインを取得しています。「ocp4.work」を管理している DNSサーバーもお名前.com の DNSサーバーを使用しているので、お名前.com の DNS管理画面に移動します。もし Route53 で取得しているドメインを管理している場合は、この作業はRoute53上で行う必要があります。この作業は、対象ドメインを管理している DNS Server (権威サーバー）上で行う必要があります。

DNSのレコードの書き替えは、使用しているサービス依存なので詳細は省略しますが、DNSサーバーの管理画面で、CNAME の DNSレコードを追加します。これらの情報は、先ほどのACMの「CNAME名」と「CNAME値」の列に表示されていた値です。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/99425/789feff8-7af8-3cf7-34d9-c85aaff1f42b.png)

このレコードが dig で確認できるまで暫く時間がかかります。以下のようにレコードが返ってくれば正しく登録されています。


```
$ dig +short _c02082522847b130a631a5aa0c9b915c.ocp4.work
$ dig +short _c02082522847b130a631a5aa0c9b915c.ocp4.work
_84dfbabaeec9a23b1414db3b330dff68.zrvsvrxrgs.acm-validations.aws.
$ 
```

AWS Certificate Manager が、このレコードの登録を確認するまで数分～10分ほどかかると思います。
ステータスが「発行済み」になれば証明書の発行は完了です。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/99425/dc0f1914-cde3-b0b1-c8ad-7f49440e866d.png)

ACMで発行した証明書の CA(Certificate Authority) は、Amazon になります。証明書の期限は1ヶ月ですが、自動更新されるので更新の手間はありません。正確には自動更新には幾つか条件もあるので、詳細は公式ドキュメントの「<a href="https://docs.aws.amazon.com/ja_jp/acm/latest/userguide/managed-renewal.html" target=”_blank”>ACM 証明書のマネージド更新</a>」を確認しておくのが良いと思います。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/99425/425fc353-7e87-b8e1-5a5a-72184970f3ab.png)

**AWS以外のCAが発行した証明書を使用したい場合**

一方で、ドメインの信頼性を担保するために、組織証明をした上で発行される OV 証明書や、金融機関等ではさらに厳しい基準で発行される EV 証明書を使用しなければいけない場合もあります。その場合は「証明書をインポート」から外部のCAで発行された証明書をインポートする事が可能です。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/99425/525cfb36-753f-3b79-bc03-50fc08883198.png)
こちらも詳細については、公式ドキュメントの「<a href="https://docs.aws.amazon.com/ja_jp/acm/latest/userguide/import-certificate-api-cli.html"  target=”_blank” >証明書をインポートする</a>」に記載があります。

# CloudFront の Distribution を作成する

ここからようやく CloudFront の設定に入ります。CloudFrontは設定の事を「ディストリビューション」と呼んでいます。

AWSのサービスからCloudFront を探して「CloudFront ディストリビューションを作成」をクリックします。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/99425/d3cb02ce-84aa-d226-4b6e-1356899498fd.png)

**「オリジン」**
以下の画像の通り設定しました。項目が多いので一つ一つ解説できませんが、以下の部分に特に注意します。

「オリジンドメイン」：「a156......ap-northeast-1.elb.amazon.com」 は、CLBのドメイン名です。
これはClodFront に来た HTTPリクエストをフォワードする先です。このフォワード先の事を「オリジン」と呼びます。今回の私の構成では、たまたまCLBが宛先だっただけで、環境によって異なります。オリジンとして指定できるものは[マニュアルのこちら](https://docs.aws.amazon.com/ja_jp/AmazonCloudFront/latest/DeveloperGuide/distribution-web-values-specify.html#DownloadDistValuesDomainName) に記載されています。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/99425/bd20ae8e-1c8f-ee43-dfdf-6b01b91088ce.png)


**「デフォルトのキャッシュビヘイビア」**
以下の画像の通り設定しました。項目が多いので一つ一つ解説できませんが、以下の部分に特に注意します
「キャッシュキーとオリジンリクエスト」：「Cache policy and origin request policy (recommended)」
「キャッシュポリシー」：「CachingDisabled」
「オリジンリクエストポリシー」 : 「AllViewer」
この設定をする事で CloudFrontにキャッシュを行わせず、純粋にWAF用の HTTP Proxy として動作させる事ができます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/99425/1a342bf8-ed4f-4c36-6a15-1b8342135bd2.png)

**「関数の関連付け」**
ここはデフォルトのままです。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/99425/df6fbb4f-b990-cf14-1244-7e1669e3e979.png)

**「設定」**
以下の画像の通り設定しました。項目が多いので一つ一つ解説できませんが、以下の部分に特に注意します
「AWS WAF ウェブ ACL - オプション」 : 「my-web-acl」これは事前に作成した web ACLです。
「代替ドメイン名 (CNAME) - オプション」 : 「\*.ocp4.work」ここは証明書を取得したドメイン名を入力します。
「カスタム SSL証明書 - オプション」：「\*.ocp4.work (427fe69d-18bd....f3)」これは事前に ACM で作成した \*.ocp4.work の証明書です。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/99425/57cca735-c7bb-1f85-0b0e-1ed3980d6851.png)
設定が完了したら「ディストリビューションを作成」をクリックします。

暫く待って「ステータス」の部分が有効になれば完了です。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/99425/64cf58dd-d84a-7909-f817-d8618f254396.png)

次に証明書を取得した「\*.ocp4.work」から、CloudFront の「ドメイン名」、「dfvhmf4jh47fc.cloudfront.net」に対して CNAME を行います。これにより「\*.ocp4.work」へのHTTPリクエストは「dfvhmf4jh47fc.cloudfront.net」に誘導されるようになります。

「ocp4.work」のドメイン名を管理している DNSサーバーで以下のように CNAMEを設定します。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/99425/b8348c60-db84-906e-0a50-07a28129c60a.png)

この設定も DNSサービスなどの環境依存ですが(DNS設定の展開速度やレコードのTTLに依存します)、インターネット上に展開されるまで数分はかかると思います。dig コマンドで設定が展開されたか確認できます。

```
$ dig +short  test.ocp4.work
$ dig +short  test.ocp4.work
dfvhmf4jh47fc.cloudfront.net.
54.192.150.8
54.192.150.85
54.192.150.75
54.192.150.65
$ 
```

「\*.ocp4.work」の「\*」の部分は、適当な文字列が使用できます。ここでは 「test.ocp4.work」に dig を打ってますが、「test」 は「\*」として解決してくれるので実際に「test.ocp4.work」がDNSレコードとして登録されている必要はありません。

上記の dig の結果では、「test.ocp4.work」が、CloudFront のドメイン名 `dfvhmf4jh47fc.cloudfront.net` に CNAME されていて、さらに `dfvhmf4jh47fc.cloudfront.net` は、4つの IPアドレスに解決されているのがわかります。

つまり「test.ocp4.work」や「test1.ocp4.work」「abc.ocp4.work」...と行った「\*.ocp4.work」にマッチする HTTPリクエストは、CloudFront の IPである、「54.192.150.8」「54.192.150.85」「54.192.150.75」「54.192.150.65」のいずれかに送信されようになりました。

以上で設定は完了です。

# テストしてみる

デプロイしているサイトに通常アクセスしてみます。シンプルに文字列を返すアプリです。

```
$ curl https://test1.ocp4.work
Hello OpenShift!
$
```

適当な引数 key=test  を付けてみます。まだ問題なくアクセスできます。
(デプロイしているアプリは引数は全く見ない作りですが、CloudFrontの設定で、引数はそのままオリジンまで送られています）

```
$  curl https://test1.ocp4.work/?key=test
Hello OpenShift!
$
```


SQL Injection として良く使われる文字列を含んだ key を付けてみます。
key=`%2710%27%20or%20%271%27%3d%271%27`　は、key='10' or '1'='1'　という意味の ASCII Code です。

```
$  curl https://test1.ocp4.work/?key=%2710%27%20or%20%271%27%3d%271%27
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd">
<HTML><HEAD><META HTTP-EQUIV="Content-Type" CONTENT="text/html; charset=iso-8859-1">
<TITLE>ERROR: The request could not be satisfied</TITLE>
</HEAD><BODY>
<H1>403 ERROR</H1>
<H2>The request could not be satisfied.</H2>
<HR noshade size="1px">
Request blocked.
We can't connect to the server for this app or website at this time. There might be too much traffic or a configuration error. Try again later, or contact the app 
or website owner.
<BR clear="all">
If you provide content to customers through CloudFront, you can find steps to troubleshoot and help prevent this error by reviewing the CloudFront documentation.  
<BR clear="all">
<HR noshade size="1px">
<PRE>
Generated by cloudfront (CloudFront)
Request ID: MyQDzV9hIpu34P0GW0VXHG_6Sq0D8pfY_ETj6ce3YrwECv9hjqhwaQ==
</PRE>
<ADDRESS>
</ADDRESS>
</BODY></HTML>
$
```

引数が原因で、WAFにブロックされました。`403` で `Request blocked` と出ています。

# 最後に

この手順ではとりあえず「動かす」事を目的に書いていますが、WAFは保護対象のWebアプリケーションの特性に合わせて、細かくチューニングをする必要がある製品です。問題なく動く事が確認できたら、細かな設定を詰めて行きましょう。