カミナシ エンジニアブログ

株式会社カミナシのエンジニアが色々書くブログです

AWS WAF を COUNT モードで動かしたはいいが、その後どうすればいいんだっけ?

どうも Security Engineering の西川です。好きなポケモンはクワッスです。カミナシ社内に遂にポケモンカード部ができまして、部員同士切磋琢磨し始めています。いつか企業対抗ポケモンカード大会をするのが夢です。

さてさて、皆さんは AWS WAF(Web Application Firewall、以下 WAF)を使っていますか?サービスに WAF を導入する際は一定期間 COUNT モードで運用することがセオリーとされています。では、COUNT モードから BLOCK モードに切り替える時に何をもって BLOCK モードへの切り替えを判断していますか?

本記事はつい先日リリースされたカミナシ従業員というサービスを開発しているメンバーから「WAF(Web Application Firewall) を COUNT モードで動かして一定期間経ったのだけど、どのルールを BLOCK モードにしていいのか、してはいけないのかどうやって判断したら良いの?」という質問をもらい試行錯誤した内容を書いていきます。

corp.kaminashi.jp

実のところ「やればできる」ぐらいに思っていたのですが、私自身すぐに言語化して説明できなかったので、エンジニアの誰もが同じように判断できるようになってもらうため文書化しました。また、カミナシでは AWS を使っていますが、カミナシのエンジニアの誰しもが AWS に詳しいわけではありません。そのため本記事では Amazon CloudWatch Logs に AWS WAF のログが保存されている場合に、どのようにログを検索し、どのように BLOCK の判断を行うかを含めた手順となっております。なお、カミナシでは AWS WAF を使っていますが、BLOCK への移行の考え方は他の WAF においても同様に使えるものかと思っています。

今回はそういった前提であるため「WAF とは?」や AWS WAF のマネージドルールの説明、ルールの評価順などの仕様部分については省いています。

COUNT モード、BLOCK モードとは

WAF には通常 COUNT モードと BLOCK モードという二つのモードが存在します。もちろん BLOCK があれば ALLOW モードもあるのですが、なかなか WAF のデフォルトを BLOCK とし、特定のリクエストだけを通すという例は多くはないと思うのと、本記事の目的とはズレているので COUNT モードと BLOCK モードにのみ言及します(参考:https://docs.aws.amazon.com/ja_jp/waf/latest/developerguide/waf-rule-action.html

サービス提供時は COUNT モードで運用する理由として、最初から BLOCK モードで運用してしまうと正常なリクエストさえもブロックしてしまいかねないということがあります。そのため、一定期間は COUNT モードで動かしたあとで、ログの状況をみつつ、BLOCK モードに変更するかどうかを判断していきます。この「ログの状況をみつつ」というところが具体的に何をしたら良いかがノウハウとして共有されていないと感じたので、本記事はその共有も一つの目的としています。 話は単純で「正規のリクエストが COUNT として記録されていなければ、そのルールは BLOCK モードに変えていいですよ」ということなのですが、この単純な話に気付けるかというのも1つのポイントであると同時に、COUNT として記録された中から BLOCK していいものとそうではないものをどう見分けるかが重要なポイントです。

実のところそれは自分たちが提供しているサービスにも依存します。 例えば下記のような要素によって変動します。

  • 認証を伴うサービスか
  • 海外にサービスを展開しているか
  • API を提供していて第三者がそれを叩けるか
  • toC か toB か
  • ..etc

本記事では一般的な SaaS ではこんな感じでみていけば良いのでは?という内容をまとめています。

また、AWS WAF が出力するログにも注目する必要がありますのでまずはそれをみていきましょう。

AWS WAF のログを見る

ここでは、CloudWatch Logs Insights を使ってログをみていきます。というのは最初だけで、COUNT のログをダウンロードするためだけに利用します。なぜなら AWS WAF のログは複雑で検知内容が配列で書かれていたりするため、そのままこれをクエリでどうにかして解析をするというのは私にとっては至難の業で、ログ量によっては試行錯誤をしているとそれなりのお金が掛かってきてしまいます。さらに、User-Agent を検索したい時に User-Agent と user-agent を同時に検索したい場合、正規表現を書くこともできないので私は jq を選択しました。が、もちろんログ量によっては大量過ぎてローカルにダウンロードするのは無理とかガバナンス的にログをダウンロードはできないみたいな方は Amazon S3 にログを出力して Amazon Athena を使うのが個人的には良いと思います。

それでは、まずはログの構造を理解する必要がありますので見ていきましょう。 下記のようになっています。

{
            
    "timestamp":1533689070589,                            
    "formatVersion":1,                                   
    "webaclId":"385cb038-3a6f-4f2f-ac64-09ab912af590",  
    "terminatingRuleId":"Default_Action",                
    "terminatingRuleType":"REGULAR",                     
    "action":"ALLOW",                                    
    "httpSourceName":"CF",                               
    "httpSourceId":"i-123",                             
    "ruleGroupList":[                                    
     {  
        "ruleGroupId":"41f4eb08-4e1b-2985-92b5-e8abf434fad3",
        "terminatingRule":null,    
        "nonTerminatingMatchingRules":[                  
            {"action" : "COUNT", "ruleId" : "4659b169-2083-4a91-bbd4-08851a9aaf74"}       
        ],
        "excludedRules":[
            {"exclusionType" : "EXCLUDED_AS_COUNT", "ruleId" : "5432a230-0113-5b83-bbb2-89375c5bfa98"}
        ]                          
     }
    ],
    "rateBasedRuleList":[                                 
     {  
        "rateBasedRuleId":"7c968ef6-32ec-4fee-96cc-51198e412e7f",   
        "limitKey":"IP",
        "maxRateAllowed":100                                                                                           
     },
     {  
        "rateBasedRuleId":"462b169-2083-4a93-bbd4-08851a9aaf30",
        "limitKey":"IP",
        "maxRateAllowed":100
     }
    ],      
    "nonTerminatingMatchingRules":[                                
        {"action" : "COUNT",  "ruleId" : "4659b181-2011-4a91-bbd4-08851a9aaf52"}    
    ],                                  
    "httpRequest":{                                                             
        "clientIp":"192.10.23.23",                                           
        "country":"US",                                                         
        "headers":[                                                                 
            {  
                "name":"Host",
                "value":"127.0.0.1:1989"
             },
             {  
                "name":"User-Agent",
                "value":"curl/7.51.2"
             },
             {  
                 "name":"Accept",
                 "value":"*/*"
             }
        ],
        "uri":"REDACTED",                                                
        "args":"usernam=abc",                                         
        "httpVersion":"HTTP/1.1",
        "httpMethod":"GET",
        "requestId":"cloud front Request id"                    
    }
}

(参考:https://docs.aws.amazon.com/waf/latest/developerguide/classic-logging.html

AWS WAF の場合 COUNT モードで見るべきは nonTerminatingMatchingRules というフィールドです。ルールを ALLOW や BLOCK モードに設定していた場合はマッチした時点で処理が終了する TerminatingMatchingRules フィールドを見ていく必要がありますが、COUNT モードは非終了処理に該当し、他のルールにもマッチしていないかどうかを最後まで評価します。ですので notTerminatingMatchingRules の方を見ていきます。

ログを解析する

下記のクエリを実行することで COUNT として出力されたログを抽出することが可能です。

fields @timestamp, @message 
| filter @message like /"action":"COUNT"/ 
| sort @timestamp desc 
| display @timestamp, @message, httpRequest.clientIp, httpRequest.uri, httpRequest.country

(参考:https://aws.amazon.com/jp/blogs/news/aws-waf-log-analysis-considerations/

上記の結果を JSON でダウンロードします。対象期間は最低1ヶ月程度を推奨しますが、CloudWatch Logs Insights では一回で10,000件までしかデータが取得できないのでそこは意識しておく必要があります。

ダウンロードができたらあとは jq を使って解析をしていきます。ちなみに私が CloudWatch Logs Insightsに詳しくないだけで、色々と解析はできるようです。 (参考:https://aws.amazon.com/jp/blogs/news/analyzing-aws-waf-logs-in-amazon-cloudwatch-logs/ )

正規のリクエストかどうかの判断

ここまできたらいよいよ正規のリクエストかどうかを判断していくわけですが、認証(ログイン)があるSaaS を前提で話を進めていきます。

まずは下記のクエリを実行しましょう。先ほどにも記載のとおり nonTerminatingMatchingRules の中の ruleId をみていきます。

jq -r '.[] | .["@message"].nonTerminatingMatchingRules[] | .ruleId' logs-insights-results.json | sort | uniq -c | sort -nr 

10 AWSManagedRulesAnonymousIpList 
5   AWSManagedRulesCommonRuleSet 
5   AWSManagedRulesBotControlRuleSet 
1   AWSManagedRulesLinuxRuleSet 
1   AWSManagedRulesKnownBadInputsRuleSet

このような結果が確認できます。ここで表示されていないルールは BLOCK にしてしまって大丈夫です。通常利用で記録されていないということは記録されるタイミングは攻撃を受けたタイミングであると考えられるためです。

次にルールと URI の組み合わせを見ていきます。

jq -r '.[] | .["@message"] as $msg | $msg.nonTerminatingMatchingRules[] | "\(.ruleId) \($msg.httpRequest.uri)"' logs-insights-results.json | sort AWSManagedRulesAnonymousIpList / 

AWSManagedRulesAnonymousIpList / 
AWSManagedRulesAnonymousIpList /.env 
AWSManagedRulesAnonymousIpList /auth/favicon.ico AWSManagedRulesAnonymousIpList /auth/login 
AWSManagedRulesAnonymousIpList /auth/login 
AWSManagedRulesAnonymousIpList /auth/login 
AWSManagedRulesAnonymousIpList /auth/login 
AWSManagedRulesAnonymousIpList /wp-login.php 
AWSManagedRulesAnonymousIpList /wp-login.php 
AWSManagedRulesBotControlRuleSet / 
AWSManagedRulesBotControlRuleSet / 
AWSManagedRulesBotControlRuleSet /auth/favicon.ico AWSManagedRulesBotControlRuleSet /auth/login 
AWSManagedRulesBotControlRuleSet /auth/login 
AWSManagedRulesCommonRuleSet / 
AWSManagedRulesCommonRuleSet /news/12345
AWSManagedRulesCommonRuleSet /news/files 
AWSManagedRulesCommonRuleSet /auth/login 
AWSManagedRulesCommonRuleSet /news/sources/03a0 AWSManagedRulesKnownBadInputsRuleSet /.env 
AWSManagedRulesLinuxRuleSet /.env

ここでは下記のケースで BLOCK として判断することが可能です。

  • サービスで使用していない URI に対してのみ検知がある
    • 上記の例では下記が対象
      • AWSManagedRulesKnownBadInputsRuleSet
      • AWSManagedRulesLinuxRuleSet
  • 認証後のアクセスで検知されていない IP アドレス/Bot 系のルール
    • 認証後にも検知がなければ正規の利用ではないことがわかる
      • 不正な利用者がログインして利用できなかったということ
      • そもそも Bot のアクセスを許容しないということであれば BLOCK してしまってよい
    • ※上記の例では下記が対象
      • AWSManagedRulesAnonymousIpList
      • AWSManagedRulesBotControlRuleSet

ここで判断に自信がない場合は User-Agent を出力すると判断の助けになるでしょう。User-Agent を出力する場合は下記のように書くことができます(結果は省きます)

jq -r '.[] | .["@message"] as $msg | $msg.nonTerminatingMatchingRules[] | "\(.ruleId) \($msg.httpRequest.uri) \($msg.httpRequest.headers[] | select(.name | test("(?i)user-agent")) | .value)"' logs-insights-results.json | sort

これを見ると明らかに自分たちのユーザーではない User-Agent が見えてきたりするので、正規のリクエストではないと見なす要素とすることができます。 基本的にはここまでの調査で BLOCK にするルールが判別できます。あとの残りは何かあった時のために引き続き COUNT モードで運用することを推奨します。 ここまでで BLOCK にして良い理由としては、ログイン済みでかつ BLOCK に変更して良い例はそれほど多くなく、もし BLOCK すべき例があるとするとすでに攻撃を受けているということなのでインシデントレスポンスが始まる可能性があります。。

※Bot を検知する時の注意

AWS WAF では「AWSManagedRulesBotControlRuleSet」というマネージドルールが用意されています。これは読んで字の如し、Bot アクセスを検知するためのものです。
「うちのサービスでは Bot のアクセスを許容しないから」
という理由で BLOCK してしまうと思わぬ落とし穴があるかもしれません。これを BLOCK にする時は E2E テストのことも念頭に入れておいてください。E2E テストが Bot からのアクセスとして検知され、テストが落ちてしまい、デプロイに失敗することが考えられます。もちろん本番に上がる前に気付けるので大きな問題にはならないですが、急にデプロイが落ちるようになってしまって何が原因かわからなくて時間を消費してしまうみたいなことも考えられるので認識しておいていただければと思います。 それから、Bot に関しては toC のサービスなど誰でもアクセスが可能な場合にログを出力していると攻撃の Scan が大量に飛んでくることがあります。そうするとそのログが大量に出力されてお金がかかってしまうことがあります。そういったことも含めて BotControl を有効化する/しないを考えてみてください。

終わりに

AWS WAF のログから BLOCK して良いルールを判別する方法をまとめました。こうまとめると「そんなに大したことないのでは?」と思うかもしれませんが、どう検索したら良いかについては試行錯誤が必要でした。AWS WAF に限らずログの構造を理解する必要があると思います。

また、なぜ BLOCK して良いと言い切れるかが論理的に説明されている文章が世の中に見当たらなかったので、自分の中の思考を整理する良い機会になりました。とはいえ、これが必ずしも正解とも思っていないので「もっとこうした方がいいよ」「こういう方法があるよ」など情報をお持ちの方は教えていただけると嬉しいです。

最後まで読んでくださりありがとうございました。