michimani.net

AWS WAF v1 と v2 それぞれで WebACL を CloudFormation で作成したときにハマった話

2020-03-18

CloudFront と S3 で公開する静的サイトに IP 制限をかけるために AWS WAF で WebACL を CloudFormation で作成しようとしたところ、 WAF の v1 と v2 で挙動が異なる部分がありました。がっつりハマって解決までに苦戦したので、今回はその話です。

目次

概要

下図のような構成を構築する CloudFormation テンプレートを作成したいと思います。

S3 + CloudFront + WAF

とてもシンプルでありがちな構成ですが、一点今回のポイントともなるのが、S3 バケットは東京 (ap-northeast-1) リージョンに作成するという点です。 IP 制限については、 WAF の IPSet で指定した IP のみアクセス可能なホワイトリスト形式での制限を考えます。

このアーキテクチャで、 WAF の WebACL を v1 と v2 のそれぞれで作成してみようとしたところ、 v1 と v2 で挙動が異なる部分がありました。その内容と、解決方法について書いていきます。

CloudFormation テンプレート作成

では、上図の構成を構築する CloudFormation テンプレートを作成していきます。 CloudFront と S3 については WAF のバージョンがどちらでも大きく変わらないので、 WAF のリソースを作成する部分を見ていきます。S3 と CloudFront も含めたテンプレート全体については GitHub に置いているので、そちらを参照してください。

WAF v1 の場合

  # AWS WAF v1
  WAFv1IPSet:
    Type: "AWS::WAF::IPSet"
    Properties:
      Name: "MyWAFv1IPSet"
      IPSetDescriptors:
        - Type: "IPV4"
          Value: 192.0.2.44/32
        - Type: "IPV6"
          Value: 1111:0000:0000:0000:0000:0000:0000:0111/128

  WAFv1Rule:
    Type: "AWS::WAF::Rule"
    Properties:
      Name: "MyWAFv1Rule"
      MetricName: "MyWAFv1RuleMetric"
      Predicates:
        - DataId: !Ref WAFv1IPSet
          Negated: false
          Type: "IPMatch"

  WAFv1WebACL:
    Type: "AWS::WAF::WebACL"
    Properties:
      Name: "MyWAFv1WebACL"
      MetricName: "MyWAFv1WebACLMetric"
      DefaultAction:
        Type: "BLOCK"
      Rules:
        - RuleId: !Ref WAFv1Rule
          Priority: 0
          Action:
            Type: "ALLOW"

WAF v1 では IPSet 、 Rule 、 WebACL の 3 つのリソースを定義する必要があります。IPv4 と IPv6 は同じリソース内で定義できます。

WAF v2 の場合

  # AWS WAF v2
  WAFv2IPSet:
    Type: "AWS::WAFv2::IPSet"
    Properties:
      Addresses:
        - 192.0.2.44/32
      IPAddressVersion: IPV4
      Name: "MyWAFv2IPSet"
      Scope: "CLOUDFRONT"

  WAFv2IPSetV6:
    Type: "AWS::WAFv2::IPSet"
    Properties:
      Addresses:
        - 1111:0000:0000:0000:0000:0000:0000:0111/128
      IPAddressVersion: IPV6
      Name: "MyWAFv2IPSetV6"
      Scope: "CLOUDFRONT"

  WAFv2WebACL:
    Type: "AWS::WAFv2::WebACL"
    Properties:
      DefaultAction:
        Block: {}
      Name: MyWAFv2WebACLI
      Rules:
        - Name: "MyWAFv2WebACLRuleIPSet"
          Action:
              Allow: {}
          Priority: 0
          Statement:
            IPSetReferenceStatement:
              Arn: !GetAtt WAFv2IPSet.Arn
          VisibilityConfig:
            CloudWatchMetricsEnabled: true
            MetricName: "MyWAFv2WebACLRuleIPSetMetric"
            SampledRequestsEnabled: false
        - Name: "MyWAFv2WebACLRuleIPSetV6"
          Action:
              Allow: {}
          Priority: 1
          Statement:
            IPSetReferenceStatement:
              Arn: !GetAtt WAFv2IPSetV6.Arn
          VisibilityConfig:
            CloudWatchMetricsEnabled: true
            MetricName: "MyWAFv2WebACLRuleIPSetV6Metric"
            SampledRequestsEnabled: false
      Scope: "CLOUDFRONT"
      VisibilityConfig:
        CloudWatchMetricsEnabled: true
        MetricName: "MyWAFWebACLMetrics"
        SampledRequestsEnabled: false

一方で v2 の場合は IPSet と WebACL の 2 種類 のリソースを定義します。ただし、 IPv4 と IPv6 は別のリソースとして定義する必要があります。

特定の IP からのアクセスを許可するという同様の動作をさせる場合でも v1 と v2 では CloudFormation テンプレートの記述方法が大きく異なります。詳しくはそれぞれの公式ドキュメントを参照してください。

スタックの作成

冒頭に書いたとおり、 S3 バケットは東京リージョンに作成したいので、東京リージョンの CloudFormation でスタックを作成していきます。

WAF v1 の場合

WAFv1Sample という名前でスタックを作成します。しばらくすると、無事にリソースが作成されました。

CFn result WAF v1
CFn result resources WAF v1

WAF v2 の場合

WAFv2Sample という名前でスタックを作成します。しばらくすると、 IPSet の作成時に下記のエラーとなり、リソースの作成に失敗しています。

Error reason: The scope is not valid., field: SCOPE_VALUE, parameter: CLOUDFRONT (Service: Wafv2, Status Code: 400, Request ID: XXXXXXXXXXXXXXXXXXXXXX)

CFn error WAF v2

WAF v2 で Scope: CLOUDFRONT を指定できるのは us-east-1 リージョンのみ

調べてみると、どうやら WAF v2 の IPSet や WebACL で必須となっている Scope パラメータに CLOUDFRONT を指定する場合、スタックを作成するリージョンは バージニア北部 (us-east-1) である必要があるようです。AWS の公式ドキュメント内にはこれに関する記述を見つける事ができなかったのですが、 AWS の Discussion Forums ではその話題に関するスレッドがありました。

As the scope has been set to “CLOUDFRONT”, you would need to deploy the stack using the “us-east-1” region. Otherwise, you will face the same error in any other region except the “us-east-1” region. I have tested the template you have attached, in the “us-east-1” region and it worked correctly.

ということで、おとなしくバージニア北部リージョンでスタックを作成したところ、無事にリソースが作成されました。

CFn result WAF v2
CFn result resources WAF v2

S3 バケットは東京、 WAF WebACL はバージニア北部 をどうにかする

では、 WAF v2 を利用する場合は今回の当初の目的であった S3 バケットは東京リージョンに作成する を諦めるしかないのでしょうか。

WAF v2 の WebACL に関してはバージニア北部リージョンで作成する必要があり、また CloudFormation で作成されるリソースは CloudFormation のリージョンに依存することも考えると、同じテンプレート内で定義することはできません。なので、 WAF 用のテンプレートと、 S3 および CloudFront 用のテンプレートを分けて作成します。

CloudFront を定義する際には WebACL の ARN が必要になるため、テンプレートを分けるとなるとその値をどこから参照するかが問題になります。そこでいくつか対応方法を考えたので、それらについて解説、というか調べた内容を書いておきます。

WAF 用のテンプレートからエクスポートして、S3 + CloudFront 用のテンプレートでインポートすればよいのでは?

いわゆるクロススタック参照ですね。
ただ、残念ながら CloudFormation のクロススタック参照は別リージョンのスタックの出力値を参照することができないため、この方法は使えません。

リージョンにわたるクロス スタックの参照を作成することはできません。Fn::ImportValue 組み込み関数は、同じリージョン内でエクスポートされた値のみインポートできます。

WAF 用のテンプレート内で SSM パラメータストアに登録すればよいのでは?

では WAF 用テンプレート内で、SSM パラメータストアに WebACL の ARN を登録する方法はどうでしょうか。CLoudFormation には SSM パラメータストアを参照して値を取得することができるというのをどこかで見たので、いけそうな気がします。

しかし、残念ながら SSM パラメータストアはリージョンごとにパラメータを持つ仕様で、さらに CloudFormation テンプレートで参照できるのは CloudFormation と同じリージョンのものだけです。なので、この方法も使えません。

WebACL の ARN をなんとかして取得して、 S3 + CloudFront 用のテンプレート デプロイ時のパラメータとして渡せばよいのでは?

じゃあもうなんとかして ARN を取得して、デプロイ時のパラメータに渡すしかないという結論に至りました。結論から言うと、この方法に落ち着きました。

ただ、この なんとかして の部分をできるだけ簡単に、事故がないようにするために工夫してみました。

WAF 用のテンプレートで ARN の値をエクスポートする

まず、WAF 用のテンプレートで ARN の値をエクスポートするようにします。上で紹介した WAF v2 のテンプレートに下記の定義を追加します。

Outputs:
  WAFWebACLArn:
    Value: !GetAtt WAFv2WebACL.Arn
    Export:
      Name: My-WAF-v2-WebACL-Arn

これで、バージニア北部リージョンの CloudFormation のエクスポートリストから WebACL の ARN を取得できるようになりました。

AWS CLI で ARN の文字列のみを取得する

AWS CLI では、 CloudFormation のエクスポートリストを取得する cloudformation list-exports というコマンドがあります。このコマンドを使用すると、下記のようにスタックからエクスポートされた値のリストが取得できます。

$ aws cloudformation list-exports --region us-east-1
{
    "Exports": [
        {
            "ExportingStackId": "arn:aws:cloudformation:us-east-1:123456789012:stack/WAFv2Sample/2e4c7420-6856-11ea-b666-XXXXXXXXXXXX",
            "Name": "My-WAF-v2-WebACL-Arn",
            "Value": "arn:aws:wafv2:us-east-1:123456789012:global/webacl/MyWAFv2WebACLI/dca677c6-a3ae-4087-9397-XXXXXXXXXXXX"
        }
    ]
}

このレスポンスから、 WebACL の値だけ抜き出します。今回であれば対象のエクスポート値の名前は My-WAF-v2-WebACL-Arn なので、下記のコマンドで ARN のみ抜き出すことができます。

$ aws cloudformation list-exports --region us-east-1 \
| jq -r '.Exports[] | select (.Name=="My-WAF-v2-WebACL-Arn") | .Value'

arn:aws:wafv2:us-east-1:123456789012:global/webacl/MyWAFv2WebACLI/dca677c6-a3ae-4087-9397-XXXXXXXXXXXX

この値を S3 + CloudFront 用のテンプレートでパラメータとして指定します。下記のような感じ。

Parameters:
  WAFv2WebACLARN:
    Description: "ARN string of WAF v2 WebACL"
    Type: "String"

Resources:

  # S3 、 OriginAccessIdentity 部分は省略

  # CloudFront
  CloudFrontDistribution:
    Type: "AWS::CloudFront::Distribution"
    Properties:
      DistributionConfig:
        Origins:
          - Id: "s3-for-waf-v2-origin"
            DomainName: !GetAtt "S3Bucket.DomainName"
            S3OriginConfig:
              OriginAccessIdentity: !Sub "origin-access-identity/cloudfront/${OriginAccessIdentiry}"
        Enabled: true
        DefaultCacheBehavior:
          TargetOriginId: "s3-for-waf-v2-origin"
          ViewerProtocolPolicy: "redirect-to-https"
          AllowedMethods: ["GET", "HEAD"]
          ForwardedValues:
            QueryString: false
        WebACLId: !Ref WAFv2WebACLARN
        DefaultRootObject: "index.html"

テンプレートのデプロイをコンソールではなく AWS CLI で実行する場合には、下記のようなコマンドを使用して操作を簡略化することもできます。

$ aws cloudformation deploy \
--stack-name WAFv2SampleS3AndCF \
--template-file /path/to/s3_cf_template.yml \
--parameter-overrides WAFv2WebACLARN=$(aws cloudformation list-exports --region us-east-1 | jq -r '.Exports[] | select (.Name=="My-WAF-v2-WebACL-Arn") | .Value')
--region ap-northeast-1

まとめ

WAF で IP 制限を実現する WebACL を v1 と v2 それぞれで作成したら、 v2 の挙動の違いにハマった話でした。
v1 だと CloudFront 用のグローバルな WebACL を作成する場合にバージニア北部 (us-east-1) リージョンである必要がないのに、 v2 だとそこが厳しいのが辛いですね。何かしら理由があるのか、ただ単に対応されてないだけなのかはわかりませんが、ちょっと不便さを感じたのでできれば v1 と同じように Scope: CLOUDFRONT もバージニア北部リージョン以外の各リージョンで指定できるようにしてほしいなと思います。

今回と同じような状況で別の解決方法があればぜひ教えていただきたいです。


comments powered by Disqus