AWS WAFの検証

# 前段
- AWS WAFと言うものがある
- WAFの知見が無いため独自ルールを作っての運用やメンテナンスを考えると気が重い
- だが使いたい
 - マネージドルールを使おう
- AWS WAFで何ができて何ができないのか知りたい
 - 実際に動かしてみよう 

# 環境
- OWASP Top 10に対応したマネージドルールを使用
- [OWASP Juice shop](https://www.owasp.org/index.php/OWASP_Juice_Shop_Project)を以下AMIで稼働させWAFの稼働状況を確認する
- AMI:Amazonlinx2 (ami-0c3fd0f5d33134a76)
- ドメイン取得済み、レジストラからNSをRoute53に向けている

## OWASP
- WAFのマネージドルールを見ていると出てくる
- The Open Web Application Security Projectの略称
- 日本語読みだと「オワスプ」
- OWASPとは？

> The Open Web Application Security Project (OWASP/日
> 本語: オワスプ) は、オープンなコミュニティであり、組織がア
> プリケーションやAPIを開発、調達、メンテナンスするにあたり
> それらが信頼できるようになることに専念しています。

[引用元](https://www.owasp.org/images/2/23/OWASP_Top_10-2017%28ja%29.pdf)


# 準備

## JuiceShopの稼働
・脆弱性を持った環境としてOWASPが提供している前述のJuiceShopを稼働させる

```bash
sudo su -
# Docker install
yum install docker
# Dockerサービスの有効化と起動
systemctl enable docker && systemctl start docker
# JuiceShopのコンテナイメージのpull
docker pull bkimminich/juice-shop
# フォアグラウンドプロセスでコンテナ起動
docker run --rm -p 80:3000 bkimminich/juice-shop

# 起動ログ：上手く攻撃できるとWebUIと標準出力にログが出る
[root@ip-172-16-0-29 ~]# docker run --rm -p 80:3000 bkimminich/juice-shop

> juice-shop@8.7.2 start /juice-shop
> node app

info: All dependencies in ./package.json are satisfied (OK)
info: Detected Node.js version v10.16.0 (OK)
info: Detected OS linux (OK)
info: Detected CPU x64 (OK)
info: Required file index.html is present (OK)
info: Required file main.js is present (OK)
info: Required file polyfills.js is present (OK)
info: Required file runtime.js is present (OK)
info: Required file vendor.js is present (OK)
info: Configuration default validated (OK)
info: Port 3000 is available (OK)
info: Server listening on port 3000
info: Solved challenge Error Handling (Provoke an error that is not very gracefully handled.)
```

## WAFを入れる前に攻撃して見る
・上手く脆弱性をつけると以下の画像のように緑色のウィンドウで「課題を解決したよ」とでる
・これをWAFで防ごうと言う話
・単純にこのチャレンジ面白いし勉強になる

![sqli.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/90505/c0eed6a7-3ac3-f15c-a612-5cbc491d104f.png)


## WAFを入れる
- WAFの要件としてはよく分からないけどOWASP top 10を有効にしておきたい
 - 本当はそのあたりのまとめ記事を先に書くつもりでしたが、どうしても動かしたい欲に負けました。
- WAFは設定できるサービスが限定されている（今回はALBを使用）

### 事前準備
- ACMでSSL証明書を発行(ドメイン認証)
- ALBを作成
 - ACMで作成した証明書を選択
 - ヘルスチェックパスを/にしたので、成功コードは200-304にしてリダイレクト通しました
 - ALBのSGは脆弱性たくさんのサービスを使うため自分の接続IPのみ許可しています
- Route53にALBのレコードを追加（A/Alias）
- 接続し、TLS化確認

## WAFの設定
- AWSのマネジメントコンソールのWAF & Shieldから「Goto AWS WAF」を選択
- marketplaceから「Cyber Security Cloud Managed Rules for AWS WAF -HighSecurity OWASP Set」を購入
- お値段は以下の感じ

| Units | Cost |
|:--|--:|
| Charge per month in each available region (pro-rated by the hour) |  $25.00 / unit |
| Charge per million requests in each available region | $1.20 / unit |

- 「Web ACLs」からCreateWebACL」を選択
- Step 1: Name web ACL
 - Web ACL name >>任意の名前
 - CloudWatch metric name >> 任意の名前
 - Region >> Asia Pacific (Tokyo)
 - Resource type to associate with web ACL >> ApplicationLoadBalancer
 - AWS resource to associate >> 前段で作成したALBを選択
- Step 2: Create conditions
 - マネージドルールを利用するため何もせずNext
- Step 3: Create rules
 - Rulesに購入したマネージドルールを選択しAdd
 - Actionは「No override」を選択
 - Default actionは「Allow all requests that don't match any rules」を選択

## WAFの効果を確かめる
- WAF設定後SQLiを実行すると403が返却されている

![after.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/90505/4bcdcf3c-6d74-b698-f073-2c6216c4100c.png)

- ちなみにWAF(ALB)を通さずに同じ入力をするとSQLiが通る（Adminでログイン処理ができてしまう）
![nowaf.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/90505/49f5075e-c204-086c-ed28-1d15812c642e.png)

- でもマネージドルールの中(今回だとOWASPtop10を網羅)のどのルールでブロックされたかはWAFのコンソール画面からだと、分からず、以下の画面ショットの情報しか出ない
- また直近3時間の中から[サンプルとして抽出する形](https://docs.aws.amazon.com/waf/latest/APIReference/API_GetSampledRequests.html)を取るため完全なログではなく、過去に遡っての確認等にもそのままは使えない。
- 完全なログを取得するにはやはり後述のKinesisDataFireHoseの準備が必要となる

![これくらい.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/90505/a7c09b07-9384-96ae-0489-03fafc266491.png)

![これくらい2.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/90505/79334308-f672-eeb0-7cdf-caea98fa23dc.png)



# 通知について
- CloudWathcのAlarmを利用しようと思いました
- CloudWatchのCreateAlarmからWAF用のメトリクスWAF>Region, RuleGroup, WebACL>BlockedRequestsを指定
- 閾値は1以上で、アクションは通知を選択し「アラーム状態の時にSNS経由でEmail通知」できるようにしました
- ただやはり、Blockが発生したとこと以上は分からず、かつ通常のWebサービスであれば、Block毎に通知されたら少し悩ましい程度の件数のメールがきてしまいそうです

# ログの取得
- 上述したように、「なぜブロックされたか？」と言う情報が十分ではありません
- また誤検知の際に何らか対応を打ちたい場合も少し情報としては不足しているように感じました
- 完全ログの取得(KinesisDataFireHose)を使うことでどこまで上記状態が緩和されるのかを確認したいと思います。

# KinesisFireHoseの準備
- Kinesis>KinesisFireHoseの「配信ストリームを作成」を押下
- Step 1: Name and source
 - Delivery stream name >> [aws-waf-logs-](https://docs.aws.amazon.com/ja_jp/waf/latest/developerguide/logging.html)で始まる任意の名前
 - Source >> Direct PUT or other sources
- Step 2: Process records
 - データ形式の変更やコンバートはしないため以下を選択します
     - Record transformation >> Disabled
     - Record format conversion >> Disabled
- Step 3: Choose destination
 - S3にPutしますPrefixとErrorPrefixはとりあえず設定せず直下に出してみます
     - Destination >> S3
     - Prefix >> なし
     - Error prefix >> なし
- Step 4: Configure settings
 - S3へのPutAPI呼び出しを減らす処置としてバッファやインターバルが選べそうでした
 - 今回は検証のためより小さい値でS3への書き込みを早めます
     - Buffer size >> 10MB
     - Buffer interval >> 60sec
 - データ圧縮は[AthenaがAmazon Kinesis Data Firehoseへのクエリでサポートしている](https://docs.aws.amazon.com/ja_jp/athena/latest/ug/compression-formats.html)GZIPを選択しようと思いましたがまずはそのまま出力します
 - S3 compression >> Disabled
 - S3 encryption >> Disabled
 - Error logging >> Disabled
 - IAM >> Create new or Chooseで作成されるIAM Roleをつけてしまいます

# WAFのLoggingの有効化
- WAFのコンソールからリージョンを選択してWeb ACLsを選択
- loggingタブからEnable Loggingを押下
- 作成したKinesisFireHoseを選択
- Redacted fields(ログに記録しないフィールド)は選択なしにしました

# ログの書き込み確認
- ALB(WAF)経由で検知するアクセス、しないアクセスをそれぞれ実行
- S3上の書き込みを確認します
- うまくいっていそうです

![s3.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/90505/d2dd6177-3415-3c90-b7e9-2698a5adacf6.png)

# 完全ログの確認
- サンプルログと項目だけを比較すると特に**"ruleId"**が取れていることが差としては大きいです
- ただしこのルールIDがマネージドルール(今回はOWASPtop10対応)の中のどのようなルールに対応しているかまでは分かりません。
- このidを利用して[ルールから除外する](https://docs.aws.amazon.com/ja_jp/waf/latest/developerguide/waf-managed-rule-groups.html)という対応を取ることはできるようです。


```json


{
  "timestamp": 1563889414167,
  "formatVersion": 1,
  "webaclId": "*-ff75-41c9-ad1d-32a67a6ae973",
  "terminatingRuleId": "*-3203-4bf1-8e12-2b90a0b6befe",
  "terminatingRuleType": "GROUP",
  "action": "BLOCK",
  "httpSourceName": "ALB",
  "httpSourceId": "*-app/*/*8010a552c436",
  "ruleGroupList": [
    {
      "ruleGroupId": "*-3203-4bf1-8e12-2b90a0b6befe",
      "terminatingRule": {
        "ruleId": "*-d3d4-4041-afa8-69a91418e3e3",
        "action": "BLOCK"
      },
      "nonTerminatingMatchingRules": [],
      "excludedRules": null
    }
  ],
  "rateBasedRuleList": [],
  "nonTerminatingMatchingRules": [],
  "httpRequest": {
    "clientIp": "***.***.***.***",
    "country": "JP",
    "headers": [
      {
        "name": "Host",
        "value": "***.com"
      },
      {
        "name": "Content-Length",
        "value": "37"
      },
      {
        "name": "accept",
        "value": "application/json, text/plain, */*"
      },
      {
        "name": "origin",
        "value": "https://***.com"
      },
      {
        "name": "user-agent",
        "value": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_13_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/75.0.3770.142 Safari/537.36"
      },
      {
        "name": "content-type",
        "value": "application/json"
      },
      {
        "name": "referer",
        "value": "https://***.com/"
      },
      {
        "name": "accept-encoding",
        "value": "gzip, deflate, br"
      },
      {
        "name": "accept-language",
        "value": "ja,en-US;q=0.9,en;q=0.8"
      },
      {
        "name": "cookie",
        "value": "language=en; welcome-banner-status=dismiss; token=eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdGF0dXMiOiJzdWNjZXNzIiwiZGF0YSI6eyJpZCI6MSwidXNlcm5hbWUiOiIiLCJlbWFpbCI6ImFkbWluQGp1aWNlLXNoLm9wIiwicGFzc3dvcmQiOiIwMTkyMDIzYTdiYmQ3MzI1MDUxNmYwNjlkZjE4YjUwMCIsImlzQWRtaW4iOnRydWUsImxhc3RMb2dpbklwIjoiMC4wLjAuMCIsInByb2ZpbGVJbWFnZSI6ImRlZmF1bHQuc3ZnIiwidG90cFNlY3JldCI6IiIsImlzQWN0aXZlIjp0cnVlLCJjcmVhdGVkQXQiOiIyMDE5LTA3LTIzIDEyOjIxOjAzLjczOSArMDA6MDAiLCJ1cGRhdGVkQXQiOiIyMDE5LTA3LTIzIDEyOjIxOjAzLjczOSArMDA6MDAiLCJkZWxldGVkQXQiOm51bGx9LCJpYXQiOjE1NjM4ODQ2NDUsImV4cCI6MTU2MzkwMjY0NX0.gnLl-05qxz-aisaD8wKWe5RYJGDArGY1zk2nM5a08XoAiIgo7gH9hDJSbqjy00mgH0CSSzAIz8h9AJChhRH3Aa342aGWUoMMBvhtg1j_AwaCAQuNSvFM6oUosuJSMF9fDbw4WVT5YvirRtPX04Kbfdp_5MYiVr8sqVyGl7GGXQ8; cookieconsent_status=dismiss; continueCode=2yLkV14jLobEDWOX78Znk53NvQyGmNcjO0VPqwxezm6M9algRpJY2rKBo49N; io=gsENha6I1qSPffshAAA-"
      }
    ],
    "uri": "/rest/user/login",
    "args": "",
    "httpVersion": "HTTP/2.0",
    "httpMethod": "POST",
    "requestId": null
  }
}
```

# 誤検知への対応
- 誤検知が疑われた場合、より優先度の高いルールでその誤検知となったアクセスを許可する必要があります
- 例えば特定の誤検知となったアクセスのIPアドレスを許可したい場合は以下のオペレーションを行います
 - Conditions>IP addresses>CreateConditionを選択
     - name >> 任意の名前
     - region >> 今回は東京リージョンのWebACLを利用しているのでAsiaPasific(Tokyo)を選択
     - IPversion >> 環境に合わせて選択
     - Address >> 環境に合わせて選択
- 作成したConditionをRuleに組み込みます
  - Rules>Create ruleを選択
      - Name >> 任意の名前をつけます
      - CloudWatch metric name >> 任意の名前をつけます(Nameと同じ文字列が補完されます）
      - Rule type >> Regular Rule
      - Region >> 今回は東京リージョンのWebACLを利用しているのでAsiaPasific(Tokyo)を選択
      - Add conditionsの「When a request」で
           - dose
           - Originate rom an IP address in
           - Conditionsで作成したIPの定義を選択します
- 作成したRuleをWebAclに紐付けます
  - Web ACLsのRulesタブから「Edit WebACL」を押下します
  - Rulesから先ほど作成したIP用のルールを選択し「AddRuletoWebACL」を押下します
  - 私の環境ですと「Cyber Security Cloud」のマネージドルールがOrder(優先度)1でその次にIPアドレス用のルールがきました。
 ![Before.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/90505/4fefc207-6b57-4e5c-eaa1-77e32f51e5f7.png)

 - IPのルールが先に評価されるようにOrderを入れ替えIPのルールをAllowにします
![aft.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/90505/60d3701b-5436-4bee-cf9f-5f2676b112c0.png)

 - 上記の状態で、IPアドレスが許可したものにマッチした場合、通信を許可し、後のルールは評価されなくなります。つまりIPのみを判定条件とし後の条件は評価されません。

# まとめ
- マネージドルールを使っても思っていた以上に運用は難儀な気がする
- 運用を考えた場合、サンプルログだけだと厳しい場面が出そうで、結局KinesisDataFireHoseの導入も合わせて設計や費用面も考える必要があるように感じました。
- マネージドルールはやはりブラックボックスで「何で引っかかったか？」はURIやクエリストリング等で判断する必要がありそう
- CloudWacthのAlarmを使用する場合は、高頻度の通知が予想されるため目的に合わせて閾値を設定するべきだと感じました
  - またBlockは成功しているので事後の振り返りやレポーティングで十分な場合は必ずしも必要とは感じませんでした。