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.
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:
- Input parameters: common part of resource names and an application load balancer ARN;
- Resources: a web ACL, a CloudWatch log group, a logging configuration and an association;
- 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 isn’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 ways, and for this post, we use AWS Console. This stack doesn’t require any additional properties and capabilities, so the process is quite straightforward.
Create stack
Set stack parameters
Stack options
Stack failed
Create complete
Web ACL
CloudFormation stack
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».