August 29, 2021

WAFv2 CDK

AWS CDK Doesn’t yet have a highlevel WAFv2 construct. Using the learnings I’ve recently discussed, I’ve created two constructs. One you can use for REGIONAL WAFs and one for CLOUDFRONT WAFs.

AWS CDK seems to be moving towards an approach of having cross regional resources created via custom resources, but this doesn’t exist for WAF yet, and I’ve had mixed results.

In this post we will first start with the REGIONAL solution.

Existing Stack

Let’s say you have an existing stack with a load balancer. It can be as simple as this:

from aws_cdk import (
        core as cdk,
        aws_ec2 as ec2,
        aws_elasticloadbalancingv2 as elbv2
        )

class MyAppStack(cdk.Stack):

    def __init__(self, scope: cdk.Construct, construct_id: str, **kwargs) -> None:
        super().__init__(scope, construct_id, **kwargs)

        vpc = ec2.Vpc(self, 'MyVpc',
            max_azs=3,
            subnet_configuration=[ec2.SubnetConfiguration(
                cidr_mask=24,
                name='public',
                subnet_type=ec2.SubnetType.PUBLIC
            )],
            nat_gateways=0
        )

        lb = elbv2.ApplicationLoadBalancer(self, "LB",
            vpc=vpc,
            internet_facing=True
        )

WAFv2 Construct

Take a copy of the construct below and add it to a file named wafv2.py in your stack directory e.g. myapp.

"""Implement a v2 WAF"""

from aws_cdk import (
  aws_wafv2 as waf,
  core as cdk
)

class WAFv2(cdk.Construct):
    """Implement a v2 WAF"""

    def __init__(self, scope: cdk.Construct, construct_id: str, resource_arn: str, default_action="allow", **kwargs) -> None:
        super().__init__(scope, construct_id, **kwargs)

        self.web_acl=waf.CfnWebACL(self, "WAF ACL",
          default_action={ default_action: {} },
          scope="REGIONAL",
          visibility_config={
            "sampledRequestsEnabled": True,
            "cloudWatchMetricsEnabled": True,
            "metricName": "web-acl",
          },
          rules=[
            {
              "name": "Custom-RateLimit500",
              "priority": 0,
              "action": {
                "block": {}
              },
              "visibilityConfig": {
                "sampledRequestsEnabled": True,
                "cloudWatchMetricsEnabled": True,
                "metricName": "Custom-RateLimit500"
              },
              "statement": {
                "rateBasedStatement": {
                  "limit": 500,
                  "aggregateKeyType": "IP"
                }
              }
            },
            {
              "priority": 1,
              "overrideAction": { "none": {} },
              "visibilityConfig": {
                "sampledRequestsEnabled": True,
                "cloudWatchMetricsEnabled": True,
                "metricName": "AWS-AWSManagedRulesAmazonIpReputationList",
              },
              "name": "AWS-AWSManagedRulesAmazonIpReputationList",
              "statement": {
                "managedRuleGroupStatement": {
                  "vendorName": "AWS",
                  "name": "AWSManagedRulesAmazonIpReputationList",
                },
              },
            },
            {
              "priority": 2,
              "overrideAction": { "none": {} },
              "visibilityConfig": {
                "sampledRequestsEnabled": True,
                "cloudWatchMetricsEnabled": True,
                "metricName": "AWS-AWSManagedRulesCommonRuleSet",
              },
              "name": "AWS-AWSManagedRulesCommonRuleSet",
              "statement": {
                "managedRuleGroupStatement": {
                  "vendorName": "AWS",
                  "name": "AWSManagedRulesCommonRuleSet",
                },
              },
            },
            {
              "priority": 3,
              "overrideAction": { "none": {} },
              "visibilityConfig": {
                "sampledRequestsEnabled": True,
                "cloudWatchMetricsEnabled": True,
                "metricName": "AWS-AWSManagedRulesKnownBadInputsRuleSet",
              },
              "name": "AWS-AWSManagedRulesKnownBadInputsRuleSet",
              "statement": {
                "managedRuleGroupStatement": {
                  "vendorName": "AWS",
                  "name": "AWSManagedRulesKnownBadInputsRuleSet",
                },
              },
            },
            {
              "priority": 4,
              "overrideAction": { "none": {} },
              "visibilityConfig": {
                "sampledRequestsEnabled": True,
                "cloudWatchMetricsEnabled": True,
                "metricName": "AWS-AWSManagedRulesSQLiRuleSet",
              },
              "name": "AWS-AWSManagedRulesSQLiRuleSet",
              "statement": {
                "managedRuleGroupStatement": {
                  "vendorName": "AWS",
                  "name": "AWSManagedRulesSQLiRuleSet",
                },
              },
            }
          ]
        )

        self.web_acl_association=waf.CfnWebACLAssociation(self, "ACLAssociation",
           resource_arn=resource_arn,
           web_acl_arn=self.web_acl.attr_arn
        )

    @property
    def web_acl_arn(self):
        """return the arn of the acl"""
        return self.web_acl.attr_arn

Remember to add aws-wafv2 to setup.py and running pip install.

Deploy WAFv2

Now that we have our construct and the WAFv2 library installed, we can implement it within our stack.

...
import myapp.wafv2 as wafv2
...
        wafv2.WAFv2(self, "WAF",
            default_action="block",
            resource_arn=lb.load_balancer_arn
        )

This adds a WAF to an existing stack with an application load balancer as we saw in the example at the begining of the post.

You can use any REGIONAL resource in place of the load balancer, and you can change the default_action to allow or block as required. The default if not defined, is allow.

The construct will collect statistics in CloudWatch, and will also sample requests to help resolve any issues.

© Greg Cockburn

Powered by Hugo & Kiss.