CloudFormationでAWS WAFを構築してみた(2022年1月版)

AWS WAFのリソース作成と設定をCloudFormationでやってみました。2021年11月のアップデートにより、ログ出力をS3に直接できるようになったため以前よりシンプルなアーキテクチャで環境構築することが可能になりました。
2022.01.27

こんにちは、AWS事業本部コンサルティング部の芦沢です。

みなさん、AWS WAF使っていますか?

AWS WAFといえば、WAFからS3へ直接出力することが可能になった2021年11月のアップデートが記憶に新しいですね。

以前はWAFのログをS3へ出力するためにKinesis Data Firehoseを経由する必要がありましたが、WAFからS3へ直接ログ出力が可能になり、とても便利になりました。

アップデート以前のWAF構築方法として、以下の記事を見ていただくとどれだけ簡単になったのかを理解しやすいと思いますので合わせて確認いただければ、と思います。

今回は、CloudFormationを利用してAWS WAF環境を自動構築してみたいと思います。

構成図

本記事で構築するAWS環境は以下の構成です。

背景がピンク色の範囲が本記事で構築するリソースです。

  • AWS WAF(WebAcl)
  • S3バケット
    • WAFアクセスログ出力用
    • Athenaクエリ保存用

背景が水色の範囲は、既存環境のリソースです。

  • ALB
  • EC2(Webサーバ)

右上のリソースは、本記事では構築しませんが保存したログを分析する場合に必要なリソースです。

  • Amazon Athena

CloudFormationテンプレートの紹介

(2022/7/25追記)CloudFormationテンプレートを更新しました。
■追記箇所
・ParametersのScopeをAllowedValuesに変更
・WebACLのルールにExcludedRulesを追加
・OverrideActionをNoneに変更

実行するテンプレートはこちらです。

AWSTemplateFormatVersion: '2010-09-09'
Parameters:
  Prefix:
    Type: String
    Default: test
    Description: "Fill in the name of the system name."
  Env:
    Type: String
    Default: develop
    Description: "Fill in the name of the environment."
  Scope:
    Type: String
    Default: REGIONAL
    AllowedValues:
      - REGIONAL
      - CLOUDFRONT
    Description: "Select in the scope of waf(REGIONAL or CLOUDFRONT)"
  WebAclAssociationResourceArn:
    Type: String
    Default: "arn:aws:elasticloadbalancing:ap-northeast-1:XXXXXXXXXXXX:loadbalancer/app/XXXXXXXXXXXX"
    Description: Enter RegionalResource(ALB,APIGateway,AppSync) ARN or CloudFront ARN to associate with WEBACL.
Resources:
# ------------------------------------------------------------#
# S3
# ------------------------------------------------------------#
  S3BucketForWaflog:
    Type: AWS::S3::Bucket
    Properties:
      BucketName: !Sub aws-waf-logs-${Env}-${Prefix}-${AWS::AccountId}
      AccessControl: Private
      PublicAccessBlockConfiguration:
        BlockPublicAcls: True
        BlockPublicPolicy: True
        IgnorePublicAcls: True
        RestrictPublicBuckets: True
  S3BucketForAthenaQuery:
    Type: AWS::S3::Bucket
    Properties:
      BucketName: !Sub athena-query-results-${Env}-${Prefix}-${AWS::AccountId}
      AccessControl: Private
      PublicAccessBlockConfiguration:
        BlockPublicAcls: True
        BlockPublicPolicy: True
        IgnorePublicAcls: True
        RestrictPublicBuckets: True
# ------------------------------------------------------------#
# WAF v2
# ------------------------------------------------------------#
  WebAcl:
    Type: AWS::WAFv2::WebACL
    Properties: 
      Name: !Sub ${Env}-${Prefix}-web-acl
      Scope: !Ref Scope
      DefaultAction:
        Allow: {}
      VisibilityConfig:
        CloudWatchMetricsEnabled: true
        SampledRequestsEnabled: true
        MetricName: !Sub ${Env}-${Prefix}-web-acl
      Rules:
        -
          Name: AWS-AWSManagedRulesCommonRuleSet
          Priority: 1
          Statement:
            ManagedRuleGroupStatement:
              VendorName: AWS
              Name: AWSManagedRulesCommonRuleSet
              ExcludedRules:
                - Name: NoUserAgent_HEADER
                - Name: UserAgent_BadBots_HEADER
                - Name: SizeRestrictions_QUERYSTRING
                - Name: SizeRestrictions_Cookie_HEADER
                - Name: SizeRestrictions_BODY
                - Name: SizeRestrictions_URIPATH
                - Name: EC2MetaDataSSRF_BODY
                - Name: EC2MetaDataSSRF_COOKIE
                - Name: EC2MetaDataSSRF_URIPATH
                - Name: EC2MetaDataSSRF_QUERYARGUMENTS
                - Name: GenericLFI_QUERYARGUMENTS
                - Name: GenericLFI_URIPATH
                - Name: GenericLFI_BODY
                - Name: RestrictedExtensions_URIPATH
                - Name: RestrictedExtensions_QUERYARGUMENTS
                - Name: GenericRFI_QUERYARGUMENTS
                - Name: GenericRFI_BODY
                - Name: GenericRFI_URIPATH
                - Name: CrossSiteScripting_COOKIE
                - Name: CrossSiteScripting_QUERYARGUMENTS
                - Name: CrossSiteScripting_BODY
                - Name: CrossSiteScripting_URIPATH
          OverrideAction:
            None: {}
          VisibilityConfig:
            CloudWatchMetricsEnabled: true
            SampledRequestsEnabled: true
            MetricName: AWS-AWSManagedRulesCommonRuleSet
  WAFLogConfig:
    Type: AWS::WAFv2::LoggingConfiguration
    Properties:
      LogDestinationConfigs:
        - !GetAtt S3BucketForWaflog.Arn
      ResourceArn: !GetAtt WebAcl.Arn
  WebACLAssociation:
    Type: AWS::WAFv2::WebACLAssociation
    Properties:
      ResourceArn: !Ref WebAclAssociationResourceArn
      WebACLArn: !GetAtt WebAcl.Arn

テンプレート内の定義について簡単に説明します。

Parameters

CloudFormationスタック作成時に入力できるパラメータを定義しています。

詳細については、後述のパラメータ入力画面で説明しています。

## テンプレートから抜粋
Parameters:
  Prefix:
    Type: String
    Default: test
    Description: "Fill in the name of the system name."
  Env:
    Type: String
    Default: develop
    Description: "Fill in the name of the environment."
  Scope:
    Type: String
    Default: REGIONAL
    AllowedValues:
      - REGIONAL
      - CLOUDFRONT
    Description: "Select in the scope of waf(REGIONAL or CLOUDFRONT)"
  WebAclAssociationResourceArn:
    Type: String
    Default: "arn:aws:elasticloadbalancing:ap-northeast-1:XXXXXXXXXXXX:loadbalancer/app/XXXXXXXXXXXX"
    Description: Enter RegionalResource(ALB,APIGateway,AppSync) ARN or CloudFront ARN to associate with WEBACL.

Resource

作成するリソースを定義します。 今回作成するリソースは以下です。

  • S3
    • WAFログ保存用バケット
    • Athenaクエリ実行結果保存用バケット
  • AWS WAF
    • Web Acl
    • Web Aclに設定するルール
    • Web Aclに関連付けするリソースの設定
    • WAFログ出力先のS3バケットの設定

S3

S3バケットは2種類作成します。

WAFのアクセスログを出力するためのバケットと、Athenaを使用してWAFログの分析した際のクエリログを出力するためのバケットです。

## テンプレートから抜粋
# ------------------------------------------------------------#
# S3
# ------------------------------------------------------------#
  S3BucketForWaflog:
    Type: AWS::S3::Bucket
    Properties:
      BucketName: !Sub aws-waf-logs-${Env}-${Prefix}-${AWS::AccountId}
      AccessControl: Private
      PublicAccessBlockConfiguration:
        BlockPublicAcls: True
        BlockPublicPolicy: True
        IgnorePublicAcls: True
        RestrictPublicBuckets: True
  S3BucketForAthenaQuery:
    Type: AWS::S3::Bucket
    Properties:
      BucketName: !Sub athena-query-results-${Env}-${Prefix}-${AWS::AccountId}
      AccessControl: Private
      PublicAccessBlockConfiguration:
        BlockPublicAcls: True
        BlockPublicPolicy: True
        IgnorePublicAcls: True
        RestrictPublicBuckets: True

どちらも外部に公開する必要のないバケットのため、パブリックアクセスは全てブロックしています。

WAFからのログ出力設定は後述のWAF設定の箇所で行います。

WAF

WAFリソースの作成および設定を行なっています。

WebAcl

WAFはWeb Aclという単位でリソースの作成、設定を行います。

Nameでリソース名を入力、DefaultActionで設定したルールに一致しなかった場合のリクエストの振る舞いを定義します。

CloudWatchMetricsEnabledを"Enabled"に設定することで、WAFをCloudWatchメトリクスの監視対象にできます。

SampledRequestsEnabledを"Enabled"に設定することで、AWSコンソールから直近3時間のリクエスト履歴が確認します。

## テンプレートから抜粋
# ------------------------------------------------------------#
# WAF v2
# ------------------------------------------------------------#
  WebAcl:
    Type: AWS::WAFv2::WebACL
    Properties: 
      Name: !Sub ${Env}-${Prefix}-web-acl
      Scope: !Ref Scope
      DefaultAction:
        Allow: {}
      VisibilityConfig:
        CloudWatchMetricsEnabled: true
        SampledRequestsEnabled: true
        MetricName: !Sub ${Env}-${Prefix}-web-acl

Rule

CloudFormationテンプレレート内でルールの設定も可能です。

今回の例では、AWS提供のマネージドルールAWSManagedRulesCommonRuleSetを使用したAWS-AWSManagedRulesCommonRuleSetをルールとして作成しています。

追加でルールが必要な場合は追記してください。

ExcludedRules以下の記述で「AWSManagedRulesCommonRuleSet」のサブルールすべてをCountモードに設定しています。

マネジメントコンソールだと以下のように、ルールの設定変更画面でRule ActionをCountに切り替えるイメージです。

## テンプレートから抜粋
      Rules:
        -
          Name: AWS-AWSManagedRulesCommonRuleSet
          Priority: 1
          Statement:
            ManagedRuleGroupStatement:
              VendorName: AWS
              Name: AWSManagedRulesCommonRuleSet
              ExcludedRules:
                - Name: NoUserAgent_HEADER
                - Name: UserAgent_BadBots_HEADER
                - Name: SizeRestrictions_QUERYSTRING
                - Name: SizeRestrictions_Cookie_HEADER
                - Name: SizeRestrictions_BODY
                - Name: SizeRestrictions_URIPATH
                - Name: EC2MetaDataSSRF_BODY
                - Name: EC2MetaDataSSRF_COOKIE
                - Name: EC2MetaDataSSRF_URIPATH
                - Name: EC2MetaDataSSRF_QUERYARGUMENTS
                - Name: GenericLFI_QUERYARGUMENTS
                - Name: GenericLFI_URIPATH
                - Name: GenericLFI_BODY
                - Name: RestrictedExtensions_URIPATH
                - Name: RestrictedExtensions_QUERYARGUMENTS
                - Name: GenericRFI_QUERYARGUMENTS
                - Name: GenericRFI_BODY
                - Name: GenericRFI_URIPATH
                - Name: CrossSiteScripting_COOKIE
                - Name: CrossSiteScripting_QUERYARGUMENTS
                - Name: CrossSiteScripting_BODY
                - Name: CrossSiteScripting_URIPATH
          OverrideAction:
            None: {}
          VisibilityConfig:
            CloudWatchMetricsEnabled: true
            SampledRequestsEnabled: true
            MetricName: AWS-AWSManagedRulesCommonRuleSet

LoggingConfiguration & WebACLAssociation

ログの出力先設定(LoggingConfiguration)やWeb Aclへのリソース関連付け設定(WebACLAssociation)をここで行なっています。

LoggingConfigurationについては割と最近追加(2021年8月24日)されたCloudFormationのプロパティだそうです。1

手動で設定しなくても良いのは便利ですね。

  WAFLogConfig:
    Type: AWS::WAFv2::LoggingConfiguration
    Properties:
      LogDestinationConfigs:
        - !GetAtt S3BucketForWaflog.Arn
      ResourceArn: !GetAtt WebAcl.Arn
  WebACLAssociation:
    Type: AWS::WAFv2::WebACLAssociation
    Properties:
      ResourceArn: !Ref WebAclAssociationResourceArn
      WebACLArn: !GetAtt WebAcl.Arn

実際にやってみた

CloudFormationテンプレートの実行

まずはCloudFormationテンプレートを使ってリソースを作成しましょう。

スタックは東京(ap-northeast-1)リージョンで作成します。

パラメータには以下のように記載します。

説明
スタックの名前 任意の名前
Env AWSリソースの接頭語として付与できる文字列(環境名)を設定できます。
Prefix AWSリソースの接頭語として付与できる文字列(プロジェクト名など)を設定できます。
Scope WebAclに関連付けするAWSリソースの種類を指定します。
関連付けするリソースが、リージョナルリソース(ALBなど)の場合は、REGIONALと記載(デフォルトのまま)、Cloudfrontを適用したい場合は、CLOUDFRONTに書き換えてください。
WebAclAssociationResourceArn WebAclに関連付けするAWSリソースのARNを指定します。
AWSリソースの例) ALB,Cloudfront,API Gateway,AppSync

スタックを作成し、実行結果がCREATE_COMPLETEになることを見届けます。

実行後確認

テンプレートで定義したリソースが正常に作成・設定できているか確認します。

まずはWAFです。

WAF & Shieldコンソール → Web Aclへアクセスします。

今回は東京リージョンでALB用のWAFを作成したので、リージョン選択欄にAsia Pacific (Tokyo)を指定します。

※WAF適用の対象リソースがCloudFrontの場合はGlobal (CloudFront)を指定します。

作成したWeb Aclを選択し、RulesAssociated AWS resourcesLogging and metricsを確認しましょう。

S3も確認します。

S3のコンソールから2つのバケット(WAFログ用バケットおよびAthenaクエリ実行用バケット)が作成できていることが確認できます。

WAFログ用バケットの中身を確認すると、以下フォルダ内にWAFログが保存されていることが確認できました

{{S3バケット名}}}}/AWSLogs/{{AWSアカウントID}}/WAFLogs/{{リージョン名}}/{{WebAcl名}}/YYYY/MM/DD/HH(UTC)/MM/
## HHは、UTCになっているので注意(日本時間であれば+9時間する必要があります)

検知テスト実施

設定したAWSマネージドルールに検知するリクエストをテストで送ってみたいと思います。

検知テストで使用する擬似攻撃リクエストのサンプルは以下のブログが参考になりました。

今回はCore rule setを使用しているので以下のテストリクエストをWAFと関連付けしたALBにリクエストを送信します。

curl -H 'User-Agent: ' http://[test_domain]

実行後、Overview欄でSampled requests欄にAWS#AWSManagedRulesCommonRuleSet#NoUserAgent_HEADERのルールでCount検知できていることがわかります。

S3バケットにも該当のログが保存されていました。

(ログの中身を見たところ、同時くらいの時刻に保存された別のログも含まれていたことも関係あるのか?ログファイルのの更新時刻は検知した時間から少しずれていました)

なおマネジメントコンソールのOverviewからでは3時間以内のSampled requestsしか確認できません。

以前のログを確認する場合はAthenaを使います。 AthenaでS3に保存したログを分析する方法は以下のブログが参考になりました

以上で完了です、お疲れ様でした。

まとめ

CloudFormationを使ってAWS WAF構築/各種設定(WAFルール設定、関連リソース設定、ログ出力設定)を行いました、

直接S3にログ出力ができなかった以前と比べるとかなりテンプレートが簡単になっており、かつ管理するリソースも減っているのでシンプルな構成でWAFのログ出力設定ができるようになったと実感できました。

さいごに

皆さんもこれでWAF構築が簡単にできますね。

WAFWAFしていきましょう。

以上、AWS事業本部コンサルティング部の芦沢がお送りしました。


  1. AWSドキュメントGitHubのHistory から確認しました