[AWS CDK] WAF v2のAWSマネージドルールをCloudFrontに適用する

2021.05.22

こんにちは、CX事業本部の若槻です。

AWS WAFではAWS Managed Rules for AWS WAF(マネージドルール)という機能を使うことにより、独自のルールを記述することなく一般的なアプリケーションの脆弱性を保護することができます。

今回は、AWS CDKでWAF v2のAWSマネージドルールをCloudFrontに適用してみました。

やってみる

アプリの作成

AWS WAFで保護するアプリケーションとして、下記の記事を参考にAWS CDKでデプロイしたReactアプリを使用します。

注意点として、今回CloudFrontに紐付けるAWS WAF v2のWeb ACLは、対応リージョンであるus-east-1で作成する必要があるため、必要に応じてbin/aws-cdk-deploy-react.tsに下記のように記述してリージョンを指定した上でcdk bootstrapを行います。

bin/aws-cdk-deploy-react.ts

#!/usr/bin/env node
import "source-map-support/register";
import * as cdk from "@aws-cdk/core";
import { AwsCdkDeployReactStack } from "../lib/aws-cdk-deploy-react-stack";

const app = new cdk.App();
new AwsCdkDeployReactStack(app, "AwsCdkDeployReactStack", {
  /* If you don't specify 'env', this stack will be environment-agnostic.
   * Account/Region-dependent features and context lookups will not work,
   * but a single synthesized template can be deployed anywhere. */

  /* Uncomment the next line to specialize this stack for the AWS Account
   * and Region that are implied by the current CLI configuration. */
  // env: { account: process.env.CDK_DEFAULT_ACCOUNT, region: process.env.CDK_DEFAULT_REGION },

  /* Uncomment the next line if you know exactly what Account and Region you
   * want to deploy the stack to. */
  // env: { account: '123456789012', region: 'us-east-1' },
  env: {
    region: "us-east-1",
  },

  /* For more information, see https://docs.aws.amazon.com/cdk/latest/guide/environments.html */
});

CloudFrontへのマネージドルールの適用

@aws-cdk/aws-wafv2をnpmでインストールします。

% npm i -D @aws-cdk/aws-wafv2

CDKスタックを下記のように定義します。ハイライト部分がWAFに関する追記箇所です。

  • CLOUDFRONTscopeのWeb ACLを作成する
  • Web ACLのrulesに使用したいマネージドルールを記載する
  • マネージドルールではoverrideAction: { none: {} }を指定する
  • 作成したWebACLをCloudFrontディストリビューションに適用する

lib/aws-cdk-deploy-react-stack.ts

import * as cdk from "@aws-cdk/core";
import * as cloudfront from "@aws-cdk/aws-cloudfront";
import * as s3 from "@aws-cdk/aws-s3";
import * as s3deploy from "@aws-cdk/aws-s3-deployment";
import * as iam from "@aws-cdk/aws-iam";
import * as wafv2 from "@aws-cdk/aws-wafv2";

export class AwsCdkDeployReactStack extends cdk.Stack {
  constructor(scope: cdk.Construct, id: string, props?: cdk.StackProps) {
    super(scope, id, props);

    const websiteBucket = new s3.Bucket(this, "WebsiteBucket", {
      websiteErrorDocument: "index.html",
      websiteIndexDocument: "index.html",
    });

    const websiteIdentity = new cloudfront.OriginAccessIdentity(
      this,
      "WebsiteIdentity"
    );

    const webSiteBucketPolicyStatement = new iam.PolicyStatement({
      actions: ["s3:GetObject"],
      effect: iam.Effect.ALLOW,
      principals: [websiteIdentity.grantPrincipal],
      resources: [`${websiteBucket.bucketArn}/*`],
    });

    websiteBucket.addToResourcePolicy(webSiteBucketPolicyStatement);

    //AWSマネージドルールを適用するWebACLの作成
    const websiteWafV2WebAcl = new wafv2.CfnWebACL(this, "WafV2WebAcl", {
      defaultAction: { allow: {} },
      scope: "CLOUDFRONT",
      visibilityConfig: {
        cloudWatchMetricsEnabled: true,
        sampledRequestsEnabled: true,
        metricName: "websiteWafV2WebAcl",
      },
      rules: [
        {
          name: "AWSManagedRulesCommonRuleSet",
          priority: 1,
          statement: {
            managedRuleGroupStatement: {
              vendorName: "AWS",
              name: "AWSManagedRulesCommonRuleSet",
            },
          },
          overrideAction: { none: {} },
          visibilityConfig: {
            cloudWatchMetricsEnabled: true,
            sampledRequestsEnabled: true,
            metricName: "AWSManagedRulesCommonRuleSet",
          },
        },
        {
          name: "AWSManagedRulesAdminProtectionRuleSet",
          priority: 2,
          statement: {
            managedRuleGroupStatement: {
              vendorName: "AWS",
              name: "AWSManagedRulesAdminProtectionRuleSet",
            },
          },
          overrideAction: { none: {} },
          visibilityConfig: {
            cloudWatchMetricsEnabled: true,
            sampledRequestsEnabled: true,
            metricName: "AWSManagedRulesAdminProtectionRuleSet",
          },
        },
        {
          name: "AWSManagedRulesKnownBadInputsRuleSet",
          priority: 3,
          statement: {
            managedRuleGroupStatement: {
              vendorName: "AWS",
              name: "AWSManagedRulesKnownBadInputsRuleSet",
            },
          },
          overrideAction: { none: {} },
          visibilityConfig: {
            cloudWatchMetricsEnabled: true,
            sampledRequestsEnabled: true,
            metricName: "AWSManagedRulesKnownBadInputsRuleSet",
          },
        },
        {
          name: "AWSManagedRulesAmazonIpReputationList",
          priority: 4,
          statement: {
            managedRuleGroupStatement: {
              vendorName: "AWS",
              name: "AWSManagedRulesAmazonIpReputationList",
            },
          },
          overrideAction: { none: {} },
          visibilityConfig: {
            cloudWatchMetricsEnabled: true,
            sampledRequestsEnabled: true,
            metricName: "AWSManagedRulesAmazonIpReputationList",
          },
        },
        {
          name: "AWSManagedRulesAnonymousIpList",
          priority: 5,
          statement: {
            managedRuleGroupStatement: {
              vendorName: "AWS",
              name: "AWSManagedRulesAnonymousIpList",
            },
          },
          overrideAction: { none: {} },
          visibilityConfig: {
            cloudWatchMetricsEnabled: true,
            sampledRequestsEnabled: true,
            metricName: "AWSManagedRulesAnonymousIpList",
          },
        },
      ],
    });

    const websiteDistribution = new cloudfront.CloudFrontWebDistribution(
      this,
      "WebsiteDistribution",
      {
        errorConfigurations: [
          {
            errorCachingMinTtl: 300,
            errorCode: 403,
            responseCode: 200,
            responsePagePath: "/index.html",
          },
          {
            errorCachingMinTtl: 300,
            errorCode: 404,
            responseCode: 200,
            responsePagePath: "/index.html",
          },
        ],
        originConfigs: [
          {
            s3OriginSource: {
              s3BucketSource: websiteBucket,
              originAccessIdentity: websiteIdentity,
            },
            behaviors: [
              {
                isDefaultBehavior: true,
              },
            ],
          },
        ],
        priceClass: cloudfront.PriceClass.PRICE_CLASS_ALL,
        webACLId: websiteWafV2WebAcl.attrArn, //作成したWebACLをCloudFrontに適用する
      }
    );

    new s3deploy.BucketDeployment(this, "WebsiteDeploy", {
      sources: [s3deploy.Source.asset("./web/build")],
      destinationBucket: websiteBucket,
      distribution: websiteDistribution,
      distributionPaths: ["/*"],
    });
  }
}

今回はAWS提供の下記のマネージドルールを適用しています。

  • ベースラインルールグループ
    • AWSManagedRulesCommonRuleSet
    • AWSManagedRulesAdminProtectionRuleSet
    • AWSManagedRulesKnownBadInputsRuleSet
  • IP 評価ルールグループ
    • AWSManagedRulesAmazonIpReputationList
    • AWSManagedRulesAnonymousIpList

マネージドルールの一覧と詳細は下記から確認可能です。要件に応じて必要なルールを選択してください。

CDKスタックの更新をデプロイします。

% cdk deploy

動作確認

デフォルトアクションで許可された場合

CloudFrontのURLからアプリにアクセスします。Reactアプリが表示されます。

AWS WAF v2の管理コンソールからWeb ACLで記録されたサンプルリクエストを確認してみます。

どのマネージドルールでもブロックされず、デフォルトアクションでアクセスが許可されていることが分かります。

マネージドルールで拒否された場合

CloudFrontのURL + /adminにアクセスしてみます。攻撃者が管理ページへのアクセスを試みているという想定です。すると今度もReactアプリが表示されました。

しかしWeb ACLでのサンプルリクエストの記録を見てみると、マネージドルールグループAWSManagedRulesAdminProtectionRuleSetのうちAdminProtection_URIPATHルールでブロックされていることが分かります。

Web ACLでブロックされているにも関わらずページが表示されたのは、今回使用したReactアプリは既定の設定でパスによらず同じコンテンツを返すようになっているためです。/adminページへのルーティングを定義している場合はブロックにより何も表示されない動作となります。

デフォルトアクションで拒否された場合

デフォルトアクションを拒否とした場合にブロックされた場合の動作も見てみます。

下記のようにWeb ACLリソースのdefaultAction記述をblockに変更します。

lib/aws-cdk-deploy-react-stack.ts

    //AWSマネージドルールを適用するWebACLの作成
    const websiteWafV2WebAcl = new wafv2.CfnWebACL(this, "WafV2WebAcl", {
      defaultAction: { block: {} },
      scope: "CLOUDFRONT",

アプリにアクセスしてみると、CloudFrontからコンテンツの配信が行われず何も表示されません。

サンプルリクエストを見てみると、デフォルトアクションで拒否されたリクエストは、ActionはBLOCK、Rule inside rule groupは-と記録されるようです。

おわりに

AWS CDKでWAF v2のAWSマネージドルールをCloudFrontに適用してみました。

AWS CDK + WAF v2 + マネージドルールは、ネット上にもサンプルがなかなか出回っていないパターンだったので、少し苦労しましたが実装が出来てよかったです。

参考

以上