Deploying a Custom WAF v2 Rule with the AWS CDK

Deploying a Custom WAF v2 Rule with the AWS CDK

·

3 min read

I'm working with a client to migrate their infrastructure to AWS and apply some modest modernizations during phase 1 of the project. During the migration process, we need to keep their new public endpoints in AWS private during the testing phase. We have a few CloudFront distributions deployed in front of ALBs. We're using the AWS CDK to script out their infrastructure. Unfortunately, WAF v2 does not have any L2 constructs for the CDK, so you have to resort to using Cfn*-style L1 constructs.

My use case is pretty straightforward: block all traffic unless it comes from a source IP listed in a WAF IP Set. The WAF CDK code looks like this:

import { CfnOutput } from "aws-cdk-lib"
import { CfnIPSet, CfnWebACL } from "aws-cdk-lib/aws-wafv2"

const migrationTestingIpSet = new CfnIPSet(this, 'MigrationTestingIpSet', {
  addresses: [
    "1.1.1.1/32", // whitelisted IPs in CIDR format
  ],
  ipAddressVersion: 'IPV4',
  scope: 'CLOUDFRONT',
  description: 'List of staff allowed to test migration endpoints',
})

const migrationWaf = new CfnWebACL(this, 'MigrationWaf', {
  defaultAction: {
    block: { }, // Block all traffic except the IP set
  },
  scope: "CLOUDFRONT",
  description: "Allows staff to test migration endpoints before cutover",
  visibilityConfig: {
    cloudWatchMetricsEnabled: true,
    metricName: 'DefaultBlocks',
    sampledRequestsEnabled: true,
  },
  rules: [{
    name: "AllowStaffIps",
    priority: 1,
    statement: {
      ipSetReferenceStatement: {
        arn: migrationTestingIpSet.attrArn,
      }
    },
    visibilityConfig: {
      cloudWatchMetricsEnabled: true,
      metricName: 'AllowsToStaffIpSet',
      sampledRequestsEnabled: true,
    },
    action: {
      allow: {}
    }
  }],
})

new CfnOutput(this, "MigrationWafArnOutput", {
  description: "Migration WAF Arn",
  value: migrationWaf.attrArn,
})

You'll see that there is a default action that blocks all traffic. Then a rule is defined that allows traffic based on the IP Set.

Next, connect your WAF to your CloudFront distribution:

import { Distribution } from "aws-cdk-lib/aws-cloudfront"

const prodAppCdn = new Distribution(this, "ProdAppCdn", {
  defaultBehavior: { /* Default Behavior here... */ },
  /* other props here */
  webAclId: "arn:aws:wafv2:us-east-1:1234567890:global/webacl/WafNameHere/WafAclGuidHere",
})

NOTE: CloudFormation requires deploying WAFs to us-east-1 if you're integrating it with CloudFront. In my case, I created a separate CDK stack since I was deploying everything else to us-east-2.

Due to the cross-reference nature of deploying to us-east-2 vs us-east-1, I included the string value for the WAF ARN instead of referencing it from the WAF stack. The WAF page in the AWS Console doesn't currently show the ARN for the ACL, but the first code snippet I shared returns it as a CfnOutput value to make it easier to copy/paste. Otherwise, you'll have to build the WAF ACL Id using a format similar to the above, filling in your account number, WAF name, and WAF Id appropriately. The WAF ACL Id is a GUID that you can find in the URL of the Console address when you visit the WAF details screen.

Helpful References

https://docs.aws.amazon.com/cdk/api/v2/docs/aws-cdk-lib.aws_wafv2-readme.html - Explains L1 constraint

https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/AWS_WAFv2.html - CloudFormation documentation in case it's helpful

https://docs.aws.amazon.com/cdk/api/v2/docs/aws-cdk-lib.aws_wafv2.CfnIPSet.html - How to create the IP Set

https://docs.aws.amazon.com/cdk/api/v2/docs/aws-cdk-lib.aws_wafv2.CfnWebACL.html - How to create the WAF ACL

https://stackoverflow.com/questions/72605879/making-the-waf-rule-by-cdk - SO post with some helpful tidbits