Journal Article

AWS WAFv2を使って国単位でアクセス許可をする方法

waf
cdk
Info
sattosh

やりたいこと

ある要件でフロントで使っているAPIを日本以外のアクセスを禁止する必要がありました. しかし,使用しているAPI Gatewayは外部のサービスからもアクセスを受けるものであり,外部サービスは国内外で冗長化されているのである程度絞ったとしてもどこの国かを制限することに対してリスクがありました.

対応

AWS WAF自体はAPI Gateway全体につける仕様であり,上記の条件を満たすためには以下の二つのうちどれかです.

  1. エンドポイントを分ける(API Gatewayをもう一つ作成)して,片方だけにWAFをかける
  2. 特定のPathだけ国別のアクセス許可をしないようにする

今回は2を採用して対応しました.

結果

AWS CDKを使って以下のようにリソースを定義しました

import * as cdk from 'aws-cdk-lib'; import { aws_apigateway as apigateway, aws_waf as waf, aws_wafv2 as wafv2, aws_wafregional as wafregional, aws_lambda_nodejs as aws_lambda_nodejs, aws_lambda as lambda, } from 'aws-cdk-lib'; import { resolve } from 'path'; import { Construct } from 'constructs'; export class SandboxApigatewayWafStack extends cdk.Stack { constructor(scope: Construct, id: string, props?: cdk.StackProps) { super(scope, id, props); // API Gateway用のLambda関数を作成 const handler = new aws_lambda_nodejs.NodejsFunction(this, 'handler', { entry: resolve(__dirname, '../lib/lambda/handler.ts'), handler: 'handler', runtime: lambda.Runtime.NODEJS_18_X, }); // API Gatewayを作成 const api = new apigateway.RestApi(this, 'api', { restApiName: 'sandbox-apigateway-waf', deployOptions: { stageName: 'prod', }, }); // API GatewayのリソースにLambda関数を紐付け const integration = new apigateway.LambdaIntegration(handler); // API Gatewayのリソースを作成 const resource1 = api.root.addResource('hello'); const resource2 = api.root.addResource('world'); resource1.addMethod('GET', integration); resource2.addMethod('GET', integration); // WAFを作成 const apiWaf = new wafv2.CfnWebACL(this, 'apiWaf', { name: 'sample-geo-restriction', defaultAction: { allow: {}, }, scope: 'REGIONAL', visibilityConfig: { cloudWatchMetricsEnabled: true, metricName: 'sample-geo-restriction', sampledRequestsEnabled: true, }, rules: [ { priority: 0, name: 'partial-access-prohibited', statement: { andStatement: { statements: [ { notStatement: { statement: { geoMatchStatement: { countryCodes: ['JP'], }, }, }, }, { notStatement: { statement: { regexMatchStatement: { fieldToMatch: { uriPath: {}, }, textTransformations: [{ priority: 0, type: 'NONE' }], regexString: '/hello', }, }, }, }, ], }, }, visibilityConfig: { cloudWatchMetricsEnabled: true, metricName: 'AWS-GeoRestrictionRule', sampledRequestsEnabled: true, }, action: { block: {}, }, }, ], }); // apiにwafを紐付け const webAclAccociation = new wafv2.CfnWebACLAssociation(this, 'apiWafAssociation', { resourceArn: api.deploymentStage.stageArn, webAclArn: apiWaf.attrArn, }); } }

今回の条件として

日本以外のアクセス かつ /hello以外のPath をBlockする

ようにしています.

WAFルールのロジック解説

上記CDKコードのWAFルールはandStatementで2つのnotStatementを組み合わせています:

  1. notStatement(geoMatchStatement(['JP'])) — アクセス元が日本ではない
  2. notStatement(regexMatchStatement('/hello')) — リクエストパスが/helloではない

この2つがAND条件で結合されているため,「日本以外からのアクセス」かつ「/hello以外のパス」の場合にBlockアクションが発動します.結果として,日本からのアクセスはすべて許可され,海外からでも/helloへのアクセスは許可されます.

実際のWAFの挙動

海外からのIPのアクセスとしてEC2を別リージョンに立ててcurlなどでアクセスしてもよかったのですが,今回はwww.webpagetest.orgというサービスを使用してUSからアクセスさせてみました.

↓実際のアクセス結果です./worldだけ他の国からのアクセスをBlockしています error

↓もちろん日本からのアクセスはどちらからも大丈夫です ok

まとめ

AWS WAFv2のandStatementnotStatementを組み合わせることで,「特定のPathだけは国別制限を除外する」という柔軟なアクセス制御が実現できます.エンドポイントを分けずに1つのAPI Gatewayで対応できるため,インフラ構成をシンプルに保てます.