AWS CDK(Typescript)でWordPress用のWAFの設定を行う

この記事では、IaCツールであるAWS CDKを使って、AWS WAFを設定する方法を解説します。

AWS WAFは比較的設定項目が多いため、CDKでコード化することで設定全体の見通しが良くなります。

設定する内容は、次の記事で解説したWAFの誤検知対策を含めたものになっています。CDKのコードは、Amazon CloudFrontと併用する前提で記載していますが、少し変えることでALBやAPI Gatewayなどでも利用可能です。

あわせて読みたい
AWS WAFでWordPressサイトを保護する設定を紹介 – 誤検知の対処法も解説します- 【AWS WAFとは?】 AWS WAF(Web Application Firewall)は、Webアプリケーションを不正アクセスや攻撃から保護するためのサービスです。AWS WAFを利用することで、SQL...

本記事作成時点ではAWS WAFはCDKのL1コンストラクトであり、抽象度が低いのでやや独特な書き方になっています。本記事の最後にログ等の設定も含めてAWS WAFを設定するコードを記載していますので、書き方が分からない場合の参考にもなります。

CDKのコンストラクトの定義について詳しく知りたい方はAWSの公式ドキュメントをご覧ください。

目次

WAF用のCDKのコード

WAFのルールから除外するIPセットを定義する

最初に、WAFのルールから除外するIPセットを定義します。これは、マネージドルールAWSManagedRulesCommonRuleSetが管理者がWordPressで投稿した際にブロックしてしまうのに対処するためです。

    const ipv4Set = new wafv2.CfnIPSet(this, "IPv4Set", {
      scope: "CLOUDFRONT",
      ipAddressVersion: "IPV4",
      addresses: [this.node.tryGetContext("ipv4Address")],
    });

    const ipv6Set = new wafv2.CfnIPSet(this, "IPv6Set", {
      scope: "CLOUDFRONT",
      ipAddressVersion: "IPV6",
      addresses: [this.node.tryGetContext("ipv6Address")],
    });

IPv4とIPv6の両方のIPセットを定義しています。 addressesには、自社のIPアドレスなど許可したいIPアドレスを入力します。この例では、実際の値はcdk.context.jsonに記載しており、this.node.tryGetContext()で呼び出しています。

こうすることでCDKのコード内にマジックナンバーが含まなくて良く、コードの再利用性も高くなります。

WAF ACL設定

次に、WAFの本体であるACL全般の設定をしていきます。

    // WAF Acl
    const webAcl = new wafv2.CfnWebACL(this, "WafACL", {
      defaultAction: {
        allow: {},
      },
      scope: "CLOUDFRONT",
      visibilityConfig: {
        cloudWatchMetricsEnabled: true,
        metricName: "WebACLMetric",
        sampledRequestsEnabled: true,
      },
      rules: [ ...

defaultActionallow: {}にすることでWAFルールにマッチしないリクエストは許可されます。WAFでは、攻撃と思わしきリクエストだけブロックするのが一般的なため、通常はこのような設定になります。

その他は、AWS WAFのメトリクスをCloudWatchに連携するための設定です。

マネージドルールの設定

最後に、AWS WAFで有効にするルールを決めていきます。ここはやや記述が煩雑です。

      rules: [
        {
          name: "AWSManagedRulesCommonRuleSet",
          priority: 0,
          overrideAction: { none: {} },
          statement: {
            managedRuleGroupStatement: {
              name: "AWSManagedRulesCommonRuleSet",
              vendorName: "AWS",
              scopeDownStatement: {
                andStatement: {
                  statements: [
                    {
                      notStatement: {
                        statement: {
                          ipSetReferenceStatement: {
                            arn: ipv4Set.attrArn,
                          },
                        },
                      },
                    },
                    {
                      notStatement: {
                        statement: {
                          ipSetReferenceStatement: {
                            arn: ipv6Set.attrArn,
                          },
                        },
                      },
                    },
                  ],
                },
              },
            },
          },
          visibilityConfig: {
            cloudWatchMetricsEnabled: true,
            metricName: "AWS-AWSManagedRulesCommonRuleSet",
            sampledRequestsEnabled: true,
          },
        },

overrideActionでは、特定のルールを無効(WAFでブロックしない)にする設定を行うことができます。マネージドルールは複数のルールのグループになっていますが、特定のルールだけを無効にしたいことがあります。overrideActionにルール名とそのアクションの組み合わせを記述することでそのような設定が可能です。

statementでは、有効にするマネージドルール名を指定しています。この例だと、AWSManagedRulesCommonRuleSetです。

statementの下にあるscopeDownStatementでは、WAFの対象となるリクエストを指定しています。この例では、andStatementで2つのnotStatementを指定しているため、「IPv4アドレスのリストにない(NOT)」かつ(AND)「IPv6アドレスのリストにない(NOT)」 = 自社のIPアドレスではないという意味になっています。

最後に、visibilityConfigでCloudWatchとの連携の設定をしています。

コード全体

コード全体はこちらです。AWSManagedRulesWordPressRuleSetなど他のマネージドルールも同様にrules配列に追加しています。

一番最後のコードでは、this.webAcl = webAclとしています。こうすることでwebAclオブジェクトを他のスタックから利用できるようにしています。このwebAclの設定はCloudFront側の設定で関連付けされるため、CloudFrontを定義しているスタックに引き渡します。

import { Construct } from "constructs";
import * as cdk from "aws-cdk-lib";
import * as wafv2 from "aws-cdk-lib/aws-wafv2";
import * as s3 from "aws-cdk-lib/aws-s3";

export class WAFStack extends cdk.Stack {
  webAcl: wafv2.CfnWebACL;

  constructor(scope: Construct, id: string, props: cdk.StackProps) {
    super(scope, id, props);

    // IP Sets
    const ipv4Set = new wafv2.CfnIPSet(this, "IPv4Set", {
      scope: "CLOUDFRONT",
      ipAddressVersion: "IPV4",
      addresses: [this.node.tryGetContext("ipv4Address")],
    });

    const ipv6Set = new wafv2.CfnIPSet(this, "IPv6Set", {
      scope: "CLOUDFRONT",
      ipAddressVersion: "IPV6",
      addresses: [this.node.tryGetContext("ipv6Address")],
    });

    // WAF Acl
    const webAcl = new wafv2.CfnWebACL(this, "WafACL", {
      defaultAction: {
        allow: {},
      },
      scope: "CLOUDFRONT",
      visibilityConfig: {
        cloudWatchMetricsEnabled: true,
        metricName: "WebACLMetric",
        sampledRequestsEnabled: true,
      },
      rules: [
        {
          name: "AWSManagedRulesCommonRuleSet",
          priority: 0,
          overrideAction: { none: {} },
          statement: {
            managedRuleGroupStatement: {
              name: "AWSManagedRulesCommonRuleSet",
              vendorName: "AWS",
              scopeDownStatement: {
                andStatement: {
                  statements: [
                    {
                      notStatement: {
                        statement: {
                          ipSetReferenceStatement: {
                            arn: ipv4Set.attrArn,
                          },
                        },
                      },
                    },
                    {
                      notStatement: {
                        statement: {
                          ipSetReferenceStatement: {
                            arn: ipv6Set.attrArn,
                          },
                        },
                      },
                    },
                  ],
                },
              },
            },
          },
          visibilityConfig: {
            cloudWatchMetricsEnabled: true,
            metricName: "AWS-AWSManagedRulesCommonRuleSet",
            sampledRequestsEnabled: true,
          },
        },
        {
          name: "AWSManagedRulesWordPressRuleSet",
          priority: 1,
          overrideAction: { none: {} },
          statement: {
            managedRuleGroupStatement: {
              name: "AWSManagedRulesWordPressRuleSet",
              vendorName: "AWS",
            },
          },
          visibilityConfig: {
            cloudWatchMetricsEnabled: true,
            metricName: "AWS-AWSManagedRulesWordPressRuleSet",
            sampledRequestsEnabled: true,
          },
        },
        {
          name: "AWSManagedRulesPHPRuleSet",
          priority: 2,
          overrideAction: { none: {} },
          statement: {
            managedRuleGroupStatement: {
              name: "AWSManagedRulesPHPRuleSet",
              vendorName: "AWS",
            },
          },
          visibilityConfig: {
            cloudWatchMetricsEnabled: true,
            metricName: "AWS-AWSManagedRulesPHPRuleSet",
            sampledRequestsEnabled: true,
          },
        },
        {
          name: "AWSManagedRulesSQLiRuleSet",
          priority: 3,
          overrideAction: { none: {} },
          statement: {
            managedRuleGroupStatement: {
              name: "AWSManagedRulesSQLiRuleSet",
              vendorName: "AWS",
            },
          },
          visibilityConfig: {
            cloudWatchMetricsEnabled: true,
            metricName: "AWS-AWSManagedRulesSQLiRuleSet",
            sampledRequestsEnabled: true,
          },
        },
      ],
    });

    // S3 Bucket for logging
    const bucket = new s3.Bucket(this, "s3-waf-logging", {
      bucketName: "aws-waf-logs-cf-XXXXXXXXXXX",
    });

    new wafv2.CfnLoggingConfiguration(this, "WafLogging", {
      logDestinationConfigs: [bucket.bucketArn],
      resourceArn: webAcl.attrArn,
    });

    this.webAcl = webAcl;
  }
}

まとめ

この記事では、AWS CDNを使用してTypescriptでAWS WAFの設定を行いました。AWS WAFは設定範囲が広いため、マネージメントコンソールで設定するよりも、CDKでコード化することで管理がしやすくなります。

WAFのコードはCDKのL2コンストラクトになっていることもあり、書き味が他のサービスと違うため、最初は戸惑うかもしれません。本記事で記載したコードが参考になれば幸いです。

よかったらシェアしてね!

CD

目次