How to create Regional Web ACL (WAFv2) with CloudFormation

Source code

Introduction

AWS WAF is a web application firewall service that lets you monitor web requests that are forwarded to an Amazon CloudFront distribution, an Amazon API Gateway REST API, an Application Load Balancer, or an AWS AppSync GraphQL API. The post describes CloudFormation template which creates WAF resources for the scenario when Application Load Balancer is used to serve content for a public website, but to block requests from attackers and to protect from OWASP Top 10 security risks. The provided template could be easily adopted to other usage scenarios.

This post is a part of post series about how to create Elastic Beanstalk application with WAF.

Background

Solution uses CloudFormation, S3, WAF v2, Web ACL, CloudWatch, ALB.

Task

To set up AWS WAF for an ALB, we need create such resources as a web ACL, a logging configuration, and an association between a web ACL and an Application Load Balancer (ALB). Application Load Balancer is created as part of the another script, so its ARN is provided as an input parameter.

Solution

CloudFormation template has the following structure:

  1. input parameters: common part of resource names and an application load balancer ARN;
  2. resources: a web ACL, a CloudWatch log group, a logging configuration and an association;
  3. output values: a web ACL ARN and a CloudWatch log group ARN.
{
  "AWSTemplateFormatVersion": "2010-09-09",
  "Description": "CloudFormation template defines Web ACL resources",
  "Metadata": {
    "AWS::CloudFormation::Interface": {
      "ParameterGroups": [
        {
          "Label": {
            "default": "Resources"
          },
          "Parameters": [
            "albARN"
          ]
        },
        {
          "Label": {
            "default": "Names"
          },
          "Parameters": [
            "tagName",
            "tagNamePrefix"
          ]
        }
      ],
      "ParameterLabels": {
        "albARN": {
          "default": "ALB ARN"
        },
        "tagName": {
          "default": "Name Tag"
        },
        "tagNamePrefix": {
          "default": "Name Prefix"
        }
      }
    }
  },
  "Parameters": {
    "albARN": {
      "Description": "ARN for the Application Load Balancer",
      "Type": "String",
      "MinLength": "30",
      "MaxLength": "180",
      "Default": "arn:aws:elasticloadbalancing:us-west-1:123456789012:loadbalancer/app/load-balancer-EXAMPLE/0123456789abcdef",
      "AllowedPattern": "^arn:(aws[a-zA-Z-]*)?:elasticloadbalancing:[a-z]{2}((-gov)|(-iso(b?)))?-[a-z]+-\\d{1}:\\d{12}:loadbalancer/app/([a-zA-Z0-9-/]{5,100})$",
      "ConstraintDescription": "must be a valid ARN of Application Load Balancer."
    },
    "tagName": {
      "Type": "String",
      "Description": "Name tag value",
      "MinLength": "5",
      "MaxLength": "25",
      "Default": "Default"
    },
    "tagNamePrefix": {
      "Description": "The prefix for use in Name tag values",
      "Type": "String",
      "MinLength": "5",
      "MaxLength": "25",
      "Default": "default"
    }
  },
  "Resources": {
    "webAcl": {
      "Type": "AWS::WAFv2::WebACL",
      "Properties": {
        "Description": "Web ACL for Application Load Balancer of Elastic Beanstalk",
        "Name": {
          "Fn::Sub": "${tagNamePrefix}-web-owasp"
        },
        "DefaultAction": {
          "Allow": {}
        },
        "Rules": [
          {
            "Name": "AWS-CRS",
            "Priority": 0,
            "Statement": {
              "ManagedRuleGroupStatement": {
                "VendorName": "AWS",
                "Name": "AWSManagedRulesCommonRuleSet",
                "ExcludedRules": []
              }
            },
            "OverrideAction": {
              "None": {}
            },
            "VisibilityConfig": {
              "SampledRequestsEnabled": true,
              "CloudWatchMetricsEnabled": true,
              "MetricName": {
                "Fn::Sub": "${tagNamePrefix}-aws-crs-metric"
              }
            }
          },
          {
            "Name": "Bad-Inputs",
            "Priority": 1,
            "Statement": {
              "ManagedRuleGroupStatement": {
                "VendorName": "AWS",
                "Name": "AWSManagedRulesKnownBadInputsRuleSet",
                "ExcludedRules": []
              }
            },
            "OverrideAction": {
              "None": {}
            },
            "VisibilityConfig": {
              "SampledRequestsEnabled": true,
              "CloudWatchMetricsEnabled": true,
              "MetricName": {
                "Fn::Sub": "${tagNamePrefix}-bad-inputs-metric"
              }
            }
          },
          {
            "Name": "Anonymous-IpList",
            "Priority": 2,
            "Statement": {
              "ManagedRuleGroupStatement": {
                "VendorName": "AWS",
                "Name": "AWSManagedRulesAnonymousIpList",
                "ExcludedRules": []
              }
            },
            "OverrideAction": {
              "None": {}
            },
            "VisibilityConfig": {
              "SampledRequestsEnabled": true,
              "CloudWatchMetricsEnabled": true,
              "MetricName": {
                "Fn::Sub": "${tagNamePrefix}-anonymous-iplist-metric"
              }
            }
          },
          {
            "Name": "Windows-RuleSet",
            "Priority": 3,
            "Statement": {
              "ManagedRuleGroupStatement": {
                "VendorName": "AWS",
                "Name": "AWSManagedRulesWindowsRuleSet"
              }
            },
            "OverrideAction": {
              "None": {}
            },
            "VisibilityConfig": {
              "SampledRequestsEnabled": true,
              "CloudWatchMetricsEnabled": true,
              "MetricName": {
                "Fn::Sub": "${tagNamePrefix}-windows-ruleset-metric"
              }
            }
          },
          {
            "Name": "SQLInject-RuleSet",
            "Priority": 4,
            "Statement": {
              "ManagedRuleGroupStatement": {
                "VendorName": "AWS",
                "Name": "AWSManagedRulesSQLiRuleSet"
              }
            },
            "OverrideAction": {
              "None": {}
            },
            "VisibilityConfig": {
              "SampledRequestsEnabled": true,
              "CloudWatchMetricsEnabled": true,
              "MetricName": {
                "Fn::Sub": "${tagNamePrefix}-SQLinjection-ruleset-metric"
              }
            }
          }
        ],
        "Scope": "REGIONAL",
        "Tags": [
          {
            "Key": "Name",
            "Value": {
              "Fn::Sub": "${tagName} OWASP Web ACL"
            }
          }
        ],
        "VisibilityConfig": {
          "SampledRequestsEnabled": true,
          "CloudWatchMetricsEnabled": true,
          "MetricName": {
            "Fn::Sub": "${tagNamePrefix}-web-owasp-metric"
          }
        }
      }
    },
    "cloudwatchLogsGroup": {
      "Type": "AWS::Logs::LogGroup",
      "Properties": {
        "LogGroupName": {
          "Fn::Sub": "aws-waf-logs-${tagNamePrefix}-web-owasp"
        },
        "RetentionInDays": 180
      }
    },
    "webAcllogging": {
      "Type": "AWS::WAFv2::LoggingConfiguration",
      "Properties": {
        "ResourceArn": {
          "Fn::GetAtt": [
            "webAcl",
            "Arn"
          ]
        },
        "LogDestinationConfigs": [
          {
            "Fn::Sub": "arn:aws:logs:${AWS::Region}:${AWS::AccountId}:log-group:aws-waf-logs-${tagNamePrefix}-web-owasp"
          }
        ],
        "LoggingFilter": {
          "DefaultBehavior": "KEEP",
          "Filters": [
            {
              "Behavior": "KEEP",
              "Conditions": [
                {
                  "ActionCondition": {
                    "Action": "BLOCK"
                  }
                }
              ],
              "Requirement": "MEETS_ANY"
            }
          ]
        },
        "RedactedFields": [
          {
            "SingleHeader": {
              "Name": "password"
            }
          }
        ]
      }
    },
    "albWebACLAssociation": {
      "Type": "AWS::WAFv2::WebACLAssociation",
      "Properties": {
        "ResourceArn": {
          "Ref": "albARN"
        },
        "WebACLArn": {
          "Fn::GetAtt": [
            "webAcl",
            "Arn"
          ]
        }
      }
    }
  },
  "Outputs": {
    "OWASPWebAclARN": {
      "Description": "ARN of WebACL",
      "Value": {
        "Fn::GetAtt": [
          "webAcl",
          "Arn"
        ]
      }
    },
    "CloudwatchLogsGroupARN": {
      "Description": "ARN of CloudWatch Logs Group",
      "Value": {
        "Fn::GetAtt": [
          "cloudwatchLogsGroup",
          "Arn"
        ]
      }
    }
  }
}

Web ACL

Web ACL could use custom or managed rule sets, and purchase it at AWS marketplace. As the post isn’t about how to set up custom rule set, webAcl resource uses AWS Managed Rules rule groups which protect against various security risks including those from OWASP Top 10 list. In addition, Elastic Beanstalk application which is behind ALB is .Net Framework web application runs on Windows Server instances, so Web ACL uses the following rule sets:

  • Core rule set AWSManagedRulesCommonRuleSet contains rules that are generally applicable to web applications. This provides protection against exploitation of a wide range of vulnerabilities, including those described in OWASP publications.
  • Known bad inputs AWSManagedRulesKnownBadInputsRuleSet contains rules that block request patterns that are known to be invalid and are associated with exploitation or discovery of vulnerabilities. This can help reduce the risk of a malicious actor discovering a vulnerable application.
  • Anonymous IP list AWSManagedRulesAnonymousIpList contains rules that block requests from services that allow obfuscation of viewer identity. This can include request originating from VPN, proxies, Tor nodes, and hosting providers. This is useful if you want to filter out viewers that may be trying to hide their identity from your application.
  • Admin protection managed rule group AWSManagedRulesAdminProtectionRuleSet contains rules that block external access to exposed admin pages. This may be useful if you are running third-party software or would like to reduce the risk of a malicious actor gaining administrative access to your application.
  • Windows operating system AWSManagedRulesWindowsRuleSet contains rules that block request patterns associated with exploiting vulnerabilities specific to Windows, (e.g., PowerShell commands). This can help prevent exploits that allow attacker to run unauthorized commands or execute malicious code.
  • SQL database AWSManagedRulesSQLiRuleSet contains rules that allow you to block request patterns associated with exploitation of SQL databases, like SQL injection attacks. This can help prevent remote injection of unauthorized queries.

In total it gives 1350 WCUs that is less than the allowed maximum of 1500 WCUs. Rule sets are ordered by their priority, none subsets are excluded.

CloudWatch log group

CloudWatch log group is defined at lines 197-205. Let’s note that its name should starts with aws-waf-logs-, otherwise web ACL does not accept a log group as a valid log target. We use moderate retention time, which equals 6 months, but you may use any value that suits your tasks.

Logging Configuration

Logging configuration is defined as AWS::WAFv2::LoggingConfiguration resource which has four properties: ResourceArn, LogDestinationConfigs, LoggingFilter and RedactedFields. ResourceArn is an ARN of web ACL and it refers to ARN attribute of webACL. Similarly, LogDestinationConfigs is an ARN of CloudWatch log group and it is expected that it should refer to ARN attribute of cloudwatchLogsGroup, like

        "LogDestinationConfigs": [
          {
            "Fn::GetAtt": [
              "cloudwatchLogsGroup",
              "Arn"
            ]
          }
        ],

In this case stack failed to create and return the error message

Resource handler returned message: "Error reason: The ARN isn't valid. A valid ARN begins with arn: and includes other information separated by colons or slashes., field: LOG_DESTINATION, parameter: arn:aws:logs:us-west-1:123456789012:log-group:aws-waf-logs-default-web-owasp:* (Service: Wafv2, Status Code: 400, Request ID: xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx, Extended Request ID: null)" (RequestToken: xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx, HandlerErrorCode: InvalidRequest)

Unfortunately, the message doesn’t informative and in fact refers to that the ARN of cloudwatchLogsGroup is equal to

arn:aws:logs:${AWS::Region}:${AWS::AccountId}:log-group:aws-waf-logs-${tagNamePrefix}-web-owasp/*

which ends with /* that is not acceptable as valid ARN. To solve this issue, an ARN of cloudwatchLogsGroup is hardcoded at line 217 where the statement refers to pseudo parameters as AWS Region and AccountId:

{
    "Fn::Sub": "arn:aws:logs:${AWS::Region}:${AWS::AccountId}:log-group:aws-waf-logs-${tagNamePrefix}-web-owasp"
}

LoggingFilter defines that all blocked requests are written to CloudWatch log group, and RedactedFields defines that all request data except password header is logged.

AWS Console

CloudFormation stack could be created in various way, and for this post we use AWS Console. This stack doesn’t require any additional properties and capabilities, so the process is quite straightforward.


1. All used IP-addresses, names of servers, workstations, domains, are fictional and are used exclusively as a demonstration only.
2. Information is provided «AS IS».

Leave a comment

This site uses Akismet to reduce spam. Learn how your comment data is processed.