😅

CDKでWAFv2を定義して、IPアドレス制限を実現する。

2023/08/31に公開

CDKを使ってWAFv2の特定のIPアドレスのみアクセスを許可する設定を作るのに、けっっこう時間かけたのでその備忘録としてこの記事を書きました。今回、アクセス制限するリソースはCloudFrontですが、IPアドレス制限する設定は他のリソースでも共通ですので参考にしてください。

前提

  • typescript で定義しています。
  • aws configureを実行していて、awsのユーザー情報をローカル内に存在している
  • aws cdk をインストール済み
  • cdk initcdk bootstrapは実行済み

WAFとCloudFrontの作成

先に完成したコードをお見せします。この後に詳しい説明を加えていきます。

lib/project-stack.ts
#!/usr/bin/env node
import "source-map-support/register";
import * as cdk from "aws-cdk-lib";
import { CdkProjectStack } from "../lib/project-stack";

const app = new cdk.App();

// WAF + Cloudfront関連のスタック
const cdkProjectStack = new CdkProjectStack(
  app,
  "GlobalAccessStack",
  {
    env: {
      region: "us-east-1", // CloudFrontのWAFはus-east-1でしか作成できないため、リージョンを指定
    },
  }
);
lib/project-stack.ts
import * as cdk from "aws-cdk-lib";
import * as cloudfront from "aws-cdk-lib/aws-cloudfront";
import * as origins from "aws-cdk-lib/aws-cloudfront-origins";
import * as wafv2 from "aws-cdk-lib/aws-wafv2";

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

    // wafのIPアドレスのホワイトリスト
    const ipWhiteList = ["XXX.XX.XX.XXX/32"];

    // IP setsの定義
    const iPSet = new wafv2.CfnIPSet(this, "SampleWhiteListIPSet", {
      name: "sample-white-list-ipset",
      addresses: ipWhiteList,
      ipAddressVersion: "IPV4",
      scope: "CLOUDFRONT",
    });

    // cloudfrontのアクセスをIPアドレス制限するwafの定義
    const webACL = new wafv2.CfnWebACL(this, "SampleWebACL", {
      name: "sample-web-acl",
      defaultAction: {
        block: {}, // デフォルトでブロックする
      },
      scope: "CLOUDFRONT",
      visibilityConfig: {
        cloudWatchMetricsEnabled: true,
        metricName: "sample-webacl-rule-metric", 
        sampledRequestsEnabled: true,
      },
      rules: [
        {
          priority: 0,
          name: "sample-webacl-rule",
          action: { allow: {} }, // IPアドレスがマッチした場合は許可する
          visibilityConfig: {
            sampledRequestsEnabled: true,
            cloudWatchMetricsEnabled: true,
            metricName: "sample-webacl-rule-metric",
          },
          statement: {
            ipSetReferenceStatement: {
              arn: iPSet.attrArn,
            },
          },
        },
      ],
    });

    // cloudfrontの定義
    const distribution = new cloudfront.Distribution(
      this,
      "SampleDistribution",
      {
        webAclId: webACL.attrArn, // webAclの設定を反映
        defaultBehavior: {
          origin: new origins.HttpOrigin(
            cdk.Fn.parseDomainName("https://xxxx")
          ),
          cachePolicy: new cloudfront.CachePolicy(
            this,
            "SampleDistributionCachePolicy",
            {
              headerBehavior:
                cloudfront.CacheHeaderBehavior.allowList("Authorization"),
            }
          ),
          responseHeadersPolicy:
            cloudfront.ResponseHeadersPolicy.CORS_ALLOW_ALL_ORIGINS,
          viewerProtocolPolicy: cloudfront.ViewerProtocolPolicy.HTTPS_ONLY,
        },
      }
    );
  }
}

CloudFront + WAF の定義

CloudFrontのアクセスを特定のIPアドレス制限をするのにWAFと連携します。

IP sets

IPアドレスを制限するにはWAFのIPsetsを作成する必要があります。下記はWAFのIPsetsを定義しています。

lib/project-stack.ts
    // wafのipアドレスのホワイトリスト
    const ipWhiteList = ["XXX.XX.XX.XXX/32"];

    // cloudfrontのアクセスをipアドレスを制限するwafの定義
    const iPSet = new wafv2.CfnIPSet(this, "SampleWhiteListIPSet", {
      name: "sample-white-list-ipset",
      addresses: ipWhiteList,
      ipAddressVersion: "IPV4",
      scope: "CLOUDFRONT",
    });

IPsetsはCfnIPSetクラスで作成しています。
const ipWhiteList = ["XXX.XX.XX.XXX"]; で定義している通り、IPsetsのIPアドレスの設定は配列で定義します。
ipAddressVersion: "IPV4", でIPアドレスのバージョンを指定しています。今回はIPV4で指定しています。
scope: "CLOUDFRONT", で、cloudFrontディストリビューション用か地域アプリケーション用かを指定しています。今回はcloudFrontのIP制限なので"CLOUDFRONT"を指定。
"CLOUDFRONT"以外にたとえば、Cognito用のIPsetを作成する場合は"REGIONAL"を指定する。)

WAFの定義

IPsetsが作成できたので、IPsetsを設定したWAFを定義します。

lib/project-stack.ts
    // cloudfrontのアクセスをIPアドレス制限するwafの定義
    const webACL = new wafv2.CfnWebACL(this, "SampleWebACL", {
      name: "sample-web-acl",
      defaultAction: {
        block: {}, // デフォルトでブロックする
      },
      scope: "CLOUDFRONT",
      visibilityConfig: {
        cloudWatchMetricsEnabled: true,
        metricName: "sample-webacl-rule-metric", 
        sampledRequestsEnabled: true,
      },
      rules: [
        {
          priority: 0,
          name: "sample-webacl-rule",
          action: { allow: {} }, // IPアドレスがマッチした場合は許可する
          visibilityConfig: {
            sampledRequestsEnabled: true,
            cloudWatchMetricsEnabled: true,
            metricName: "sample-webacl-rule-metric",
          },
          statement: {
            ipSetReferenceStatement: {
              arn: iPSet.attrArn,
            },
          },
        },
      ],
    });

WAFはCfnWebACLクラスで作成しています。
重要なプロパティはdefaultActionです。ここをblockにすることでIPsetsで指定しているIPアドレスのみをアクセスできるようになります。
scope: "CLOUDFRONT",はIPsetsの時と同様で、cloudFrontを利用するので"CLOUDFRONT"を指定。
visibilityConfigは CloudWatch メトリクスやWeb リクエストのサンプル収集の定義をしています。今回は有効にしています。

IPsetsはrules:プロパティで設定しています。各IPsetsの設定はオブジェクト単位で設定しており、優先度を指定することで、複数あるIPsetsの中でもどれを重視するかを設定できます。
priority: 0, 優先度を表したプロパティです。値が低いほど優先度は高くなります。
action: { allow: {} }, ここでIPsetsで指定しているIPアドレスを許可しています。逆にここでblockにするとIPsetsで指定したIPアドレスを拒否します。

statement: {
  ipSetReferenceStatement: {
    arn: iPSet.attrArn,
  },
},

↑ここでIPsetsの指定をしています。

cloudFrontの定義

WAFの定義ができたのであとはcloudFrontを定義するだけです。

lib/project-stack.ts
    // cloudfrontの定義
    const distribution = new cloudfront.Distribution(
      this,
      "SampleDistribution",
      {
        webAclId: webACL.attrArn, // webAclの設定を反映
        defaultBehavior: {
          origin: new origins.HttpOrigin(
            cdk.Fn.parseDomainName("https://xxxx")
          ),
          cachePolicy: new cloudfront.CachePolicy(
            this,
            "SampleDistributionCachePolicy",
            {
              headerBehavior:
                cloudfront.CacheHeaderBehavior.allowList("Authorization"),
            }
          ),
          responseHeadersPolicy:
            cloudfront.ResponseHeadersPolicy.CORS_ALLOW_ALL_ORIGINS,
          viewerProtocolPolicy: cloudfront.ViewerProtocolPolicy.HTTPS_ONLY,
        },
      }
    );

webAclId: webACL.attrArn,で定義したWAFの設定と連携しています。これで、cloudFrontとWAFの連携はできたのであとはcloudFrontの設定になります。

defaultBehavior: でデフォルトビヘイビアを設定しています。追加でビヘイビアを設定するときはadditionalBehaviors:プロパティを使って定義します。
origin: でアクセスするURLを指定します。
cachePolicy: でキャッシュポリシーを作成、設定しています。今回は下記の通りに設定することで、Amplifyとかで設定するBASIC認証にも対応できるようにしています。
headerBehavior: cloudfront.CacheHeaderBehavior.allowList("Authorization"),
responseHeadersPolicy: cloudfront.ResponseHeadersPolicy.CORS_ALLOW_ALL_ORIGINS, はレスポンスヘッダーポリシーの設定を指定います。今回はSimpleCORSの設定に指定います。
viewerProtocolPolicy: cloudfront.ViewerProtocolPolicy.HTTPS_ONLY, はビューワープロトコルポリシーの設定を定義しています。

上記でcloudFront + WAFの定義ができました。あとはcdk deployすることでAWS上に作成することが出来ます。

しかし、落とし穴が一つあります。
cloudFrontにWAFをアタッチする場合、現在(2023/08/31)だとリージョンがus-east-1でしか作成できないため、リージョンを指定する必要があります。(デフォルトでus-east-1の場合は不要)
そのため、lib配下(cdk init初期時)のstackを管理しているファイルでcloudFront + WAFを定義しているstackには下記のようにリージョンを指定する必要があります。

lib/project-stack.ts
// WAF + Cloudfront関連のスタック
const cdkProjectStack = new CdkProjectStack(
  app,
  "GlobalAccessStack",
  {
    env: {
      region: "us-east-1", // CloudFrontのWAFはus-east-1でしか作成できないため、リージョンを指定
    },
  }
);

こちらで作成可能になります。今回紹介したコードでは直接デフォルトアクションを入力していますが、cdk.jsonファイルを活用して環境ごとの設定もすることができます。

注意点

本番環境をリリースするのに設定しているIPsetsをデタッチするのにたとえば、コメントアウトなどでruleから削除します。その状態でcdk deployを実行すると手動で設定したIPsetsなどの設定もデタッチされます。リリース前にヒヤヒヤする羽目になるので注意してください。

Discussion