AWS CDKでWAFv2を構築しIPアドレス制限を試してみた

AWS CDKでWAFv2とAPI Gateway/Lambdaを構築し、WAFに設定したルールが動作しているか検証します。
2020.10.01

こんにちは、CX事業本部のうらわです。

本記事ではAPI GatewayにWAFv2を設定するAWS CDK(以下、CDK)のコードをご紹介します。最終的にはデプロイしたAPI Gatewayのエンドポイントに対してcurlでGETリクエストを送り、WAFが動作していることを確認します。

既に弊社ブログ記事でWAFv2にマネージドルールを設定してCDKでデプロイする記事があるため、本記事では別のルール、具体的にはIPアドレスによるアクセス制限のルールを設定する例をご紹介します。

AWS WAFv2をCDKで構築してみた

本記事のサンプルコードは以下のGitHubリポジトリに格納してあります。CDKのコードはTypeScriptで書いています。

https://github.com/urawa72/cdk_samples/tree/master/wafv2-apigateway

API GatewayのStack

このスタックではAPI Gatewayとテスト用のLambda関数を定義しています。テスト用なのでシンプルな構成です。

new apigw.RestApi()で生成したインスタンスをメンバー変数に設定することで他スタックでも参照できるようにしています。

lib/apigw-lambda-stack.ts

import * as cdk from '@aws-cdk/core';
import * as apigw from '@aws-cdk/aws-apigateway';
import * as lambda from '@aws-cdk/aws-lambda';

export class ApigwLambdaStack extends cdk.Stack {
  public restApi: apigw.RestApi;

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

    // メンバー変数に設定しWAFのスタックでIDを参照できるようにする
    this.restApi = new apigw.RestApi(this, 'TestRestApi', {
      restApiName: 'test',
      deployOptions: {
        stageName: 'dev'
      },
      defaultCorsPreflightOptions: {
        allowOrigins: apigw.Cors.ALL_ORIGINS,
        allowMethods: ['GET', 'OPTIONS'],
        statusCode: 200,
      }
    });

    // テスト用関数
    const testFunction = new lambda.Function(this, 'TestFunction', {
      code: lambda.Code.fromAsset('lambda'),
      functionName: 'testFunction',
      handler: 'index.handler',
      runtime: lambda.Runtime.NODEJS_12_X,
      memorySize: 256
    });

    // トリガーにAPI Gatewayを設定する
    const testIntegration = new apigw.LambdaIntegration(testFunction);
    const testResource = this.restApi.root.addResource('test');
    testResource.addMethod('GET', testIntegration);
  }
}

WAFv2のStack

  1. IPアドレスセットを作成します。addressesに配列でIPアドレスをCIDR表示で記載します。今回はIPv4のIPアドレスとしています。

  2. WebACL自体の定義です。このWebACLではデフォルトアクションをblock=アクセス拒否とし、rulesに設定するルールに当てはまる場合はallow=アクセス許可としています。

  3. WebACLのARNとAPI GatewayのARNを用いて両者を関連づけます。

lib/wafv2-stack.ts

import * as cdk from '@aws-cdk/core';
import * as apigw from '@aws-cdk/aws-apigateway';
import * as waf from '@aws-cdk/aws-wafv2';

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

    // 1. IPアドレス制限のためのIPアドレスセット
    const wafIPSet = new waf.CfnIPSet(this, 'TestWafIPSet', {
      name: 'TestWafIpSet',
      ipAddressVersion: 'IPV4',
      scope: 'REGIONAL',
      addresses: ['192.168.0.1/32']
    });

    // 2. WebACL本体
    const webAcl = new waf.CfnWebACL(this, 'TestWebAcl', {
      defaultAction: { block: {} },
      name: 'TestWafWebAcl',
      scope: 'REGIONAL',
      visibilityConfig: {
        cloudWatchMetricsEnabled: true,
        sampledRequestsEnabled: true,
        metricName: 'TestWafWebAcl',
      },
      rules: [
        {
          priority: 1,
          name: 'TestWafWebAclIpSetRule',
          action: { allow: {} },
          visibilityConfig: {
            sampledRequestsEnabled: true,
            cloudWatchMetricsEnabled: true,
            metricName: 'TestWafWebAclIpSetRule',
          },
          statement: {
            ipSetReferenceStatement: {
              arn: wafIPSet.attrArn,
            },
          },
        },
      ],
    });

    // 3. WAFにAPI Gatewayを関連づける
    const arn = `arn:aws:apigateway:ap-northeast-1::/restapis/${restApiId}/stages/dev`;
    const association = new waf.CfnWebACLAssociation(this, 'TestWebAclAssociation', {
      resourceArn: arn,
      webAclArn: webAcl.attrArn
    });
  }
}

デプロイ

API GatewayとLambdaのスタック, WAFv2のスタックのインスタンスを生成します。

bin/wafv2-apigateway.ts

#!/usr/bin/env node
import 'source-map-support/register';
import * as cdk from '@aws-cdk/core';
import { ApigwLambdaStack } from '../lib/apigw-lambda-stack';
import { Wafv2Stack } from '../lib/wafv2-stack';

const app = new cdk.App();

const stageName: string = app.node.tryGetContext('stageName');

const api = new ApigwLambdaStack(app, 'ApigwLambdaStack');
new Wafv2Stack(app, 'Wafv2Stack', api.restApi.restApiId);

WAFのスタックをデプロイします。API GatewayとLambdaのスタックもデプロイされます。

# Lambda関数のコードをビルド
$ npm run build ./lambda/index.ts
# デプロイ
$ npm run cdk deploy Wafv2Stack

テスト

今回はIPアドレスセットで自身のグローバルIPアドレスを設定してみました。そのため、ローカルから通常通りcurlをすればリクエストは成功します。

$ curl -X GET https://xxxxxxxx.execute-api.ap-northeast-1.amazonaws.com/dev/test
Test Function Response!%

今度は接続しているインターネット回線を切り替えて(グローバルIPアドレスを変えて)リクエストを送ってみます。allowしたIPアドレスとは異なるため、以下のようにWAFによってアクセスが拒否されます。

$ curl -X GET https://xxxxxxxx.execute-api.ap-northeast-1.amazonaws.com/dev/test
{"message":"Forbidden"}%

まとめ

WAF classicとはCDKの書き方が異なるため最初は少し苦労しました。 AWSコンソールで設定内容を把握しつつ、APIドキュメントやCDKのコードを読んで必要な設定値を埋めていくのが良いかと思います。