Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles / Hosted-services / AWS

How to create Regional Web ACL (WAFv2) with PowerShell

0.00/5 (No votes)
24 Sep 2022CPOL5 min read 1.7K  
The post describes PowerShell script 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 PowerShell script which creates web ACL 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 solution could be easily adopted to other usage scenarios.

Another post, How to create Regional Web ACL (WAFv2) with CloudFormation, solves this task by using CloudFormation template. As the script creates the similar objects, some descriptions are omitted.

Background

Solution uses PowerShell Core 7.2, AWS CLI v2, WAF v2, web ACL, CloudWatch, ALB.

Task

To set up AWS WAF for an ALB, we need to create such resources as a web ACL, a logging configuration, and an association between a web ACL and an AWS resource. In our example, web ACL is associated with Application Load Balancer that is created as part of the another script, so its ARN is provided as an script parameter.

Solution

The solution includes the following files:

  1. create-webacl.ps1 – the main script which checks prerequisites, creates additional resources and creates web ACL.
  2. functions.ps1 – contains several functions which manipulate CloudWatch log group and web ACL. These functions are described in Get AWS CloudWatch log group with PowerShell Core and Get AWS Web ACL with PowerShell Core.
  3. webacl-rules.json – the name of the file with definition of web ACL rule sets. Used rule sets are described in the post How to create Regional Web ACL (WAFv2) with CloudFormation. Keeping these definitions in a separate file allows keep code cleaner and shorter.
  4. create-webacl.example.ps1 – the script with an example how to call the main script.

Features

The script create-webacl.ps1 has such features:

  • Script is idempotent, it checks for the existent resources and could be run several times with the same result.
  • Script returns a web ACL ARN if all operations are successful, otherwise it returns $null.
  • Script doesn’t catch exceptions and throw them at the upper level.
  • Script provides default and verbose output including name of the script and duration.

Listing of create-webacl.ps1

This is a listing of the main script:

PowerShell
Param (
    # resource ARN
    [Parameter(Mandatory = $true, Position = 0)]
    [ValidateNotNullOrEmpty()]
    [string]$ResourceARN,

    # rule sets file name
    [Parameter(Mandatory = $true, Position = 1)]
    [ValidateNotNullOrEmpty()]
    [string]$RulesFilename,

    # Tag name
    [Parameter(Mandatory = $true, Position = 2)]
    [ValidateNotNullOrEmpty()]
    [string]$TagName,

    # Tag name prefix
    [Parameter(Mandatory = $true, Position = 3)]
    [ValidateNotNullOrEmpty()]
    [string]$TagNamePrefix,

    # AWS Region, could be set in user's credentials.
    [Parameter(Mandatory = $false)]
    [ValidateNotNullOrEmpty()]
    [string]$RegionName = "us-west-1",

    # AWS profile, default value is 'default'
    [Parameter(Mandatory = $false)]
    [ValidateNotNullOrEmpty()]
    [string]$AwsProfile = "default"
)

$startDateTime = $([DateTime]::Now);
$fileName = $(Split-Path -Path $PSCommandPath -Leaf);
Write-Host "Script $fileName start time = $([DateTime]::Now)" -ForegroundColor Blue;

$Verbose = $PSCmdlet.MyInvocation.BoundParameters["Verbose"].IsPresent;
. "$(Split-Path -Path $PSCommandPath)\functions.ps1"

try {
    $webAclName = "${tagNamePrefix}-web-owasp-2";
    $logGroupName = "aws-waf-logs-$webAclName";

    #region Check for the existent web ACL and stop the script if a web ACL exists
    $webAclARN = Get-WAF2WebAclARN `
        $webAclName `
        -regionname $RegionName -awsprofile $AwsProfile `
        -verbose:$Verbose;
    if (-not $?) {
        Write-Host "Getting web ACL failed" -ForegroundColor Red;
        return $null;
    }
    if ($webAclARN) {
        Write-Host "Web ACL '$webAclName' already exists, script is stopped";
        return $webAclARN;
    }
    # Write-Verbose "Web ACL '$webAclName' doesn't exist";
    #endregion

    #region Check the resource for the associated web ACL
    $webAclARN = Get-WAF2WebAclForResource `
        $ResourceARN `
        -regionname $RegionName -awsprofile $AwsProfile `
        -verbose:$Verbose;
    
    if (-not $?) {
        Write-Host "Getting web ACL associated with the resource failed" 
                    -ForegroundColor Red;
        return $null;
    }
    if ($webAclARN) {
        Write-Host "Web ACL for the resource is already associated, 
        script is stopped";
        return $webAclARN;
    }
    # Write-Verbose "The resource doesn't have associated web ACL";
    #endregion

    #region Create or use existent log group
    $logGroupTags = "Project=$tagNamePrefix";
    $logGroupARN = New-CloudWatchLogGroup `
        $logGroupName `
        -retentiondays 180 `
        -tags $logGroupTags `
        -regionname $RegionName -awsprofile $AwsProfile `
        -verbose:$Verbose;
    
    if ((-not $?) -or (-not $logGroupARN)) {
        Write-Host "Getting log group '$logsGroupName' failed" -ForegroundColor Red;
        return $null;
    }
    Write-Host "Log group '$logGroupName' is found, ARN=$logGroupARN";
    #endregion

    #region Create web ACL with predefined set of rule sets
    $rulesFilePath = "$(Split-Path -Path $PSCommandPath -Parent)\$($RulesFilename)";
    Write-Verbose "Rules file path: '$rulesFilePath'";
    if (-not(Test-Path -Path $rulesFilePath -PathType Leaf)) {
        Write-Host "File with rules for a web ACL is not found";
        return $null;
    }
    $rulesContent = (Get-Content $rulesFilePath -Raw) | `
        ForEach-Object { $_.replace("$tagNamePrefix", 
                         $tagNamePrefix).replace('"', '""') };

    $jsonObjects = $null;
    $strJsonObjects = $null;
    $awsObjects = $null;
    $existObject = $false;
    
    $jsonObjects = aws --output json --profile $AwsProfile 
                       --region $RegionName --color on `
        wafv2 create-web-acl `
        --name $webAclName `
        --scope REGIONAL `
        --description "Web ACL for Application Load Balancer of Elastic Beanstalk" `
        --default-action "Allow={}" `
        --visibility-config "SampledRequestsEnabled=true, 
          CloudWatchMetricsEnabled=true, MetricName=$tagNamePrefix-web-owasp-metric" `
        --rules $rulesContent `
        --tags "Key=Name,Value=$tagName OWASP Web ACL" 
               "Key=Project,Value=$tagNamePrefix";
    if (-not $?) {
        Write-Host "Creating web ACL failed" -ForegroundColor Red;
        return $null;
    }
    if ($jsonObjects) {
        $strJsonObjects = [string]$jsonObjects;
        $awsObjects = ConvertFrom-Json -InputObject $strJsonObjects;
        $existObject = ($awsObjects.Count -gt 0);
    }
    if ($existObject) {
        $webAclARN = $awsObjects.Summary.ARN;
        $webAclId = $awsObjects.Summary.Id;
    }
    else {
        Write-Host "Creating a web ACL '$webAclName' failed" -ForegroundColor Red;
        return $null;
    }
    Write-Host "Web ACL is created succesfully, Id=$webAclId, ARN=$webAclARN";
    #endregion

    #region Add Web ACL logging
    $jsonObjects = $null;
    $strJsonObjects = $null;
    $awsObjects = $null;
    $existObject = $false;

    if ($logGroupARN.EndsWith(":*")) {
        $logGroupARN = $logGroupARN.TrimEnd(":*");
    }
    $queryRequest = "LoggingConfigurations[?contains
                     (ResourceArn, ``$webAclARN``) == ``true``]";
    $jsonObjects = aws --output json --profile $AwsProfile 
                       --region $RegionName --color on `
        wafv2 list-logging-configurations `
        --scope REGIONAL `
        --query $queryRequest;
    
    if (-not $?) {
        Write-Host "Getting logging configurations for web ACLs failed" 
                    -ForegroundColor Red;
        return $null;
    }
    else {
        if ($jsonObjects) {
            $strJsonObjects = [string]$jsonObjects;
            $awsObjects = ConvertFrom-Json -InputObject $strJsonObjects;
            $existObject = ($awsObjects.Count -gt 0);
        }
        if ($existObject) {
            Write-Verbose "Web ACL '$webAclARN' already has logging configuration";
        }
    }
    if (-not $existObject) {
        Write-Verbose "Adding logging configuration to Web ACL '$webAclARN'";
        $configuration = @"
{  
    \"ResourceArn\": \"$webAclARN\",
    \"LogDestinationConfigs\": [
      \"$logGroupARN\"
    ],
    \"RedactedFields\": [
      {
        \"SingleHeader\": {
          \"Name\": \"password\"
        }
      }
    ],
    \"ManagedByFirewallManager\": false,
    \"LoggingFilter\": {
        \"DefaultBehavior\": \"KEEP\",
        \"Filters\": [
            {
                \"Behavior\": \"KEEP\",
                \"Conditions\": [
                    {
                        \"ActionCondition\": {
                            \"Action\": \"BLOCK\"
                        }
                    }
                ],
                \"Requirement\": \"MEETS_ANY\"
            }
        ]
    }
}
"@
        $loggingConfiguration = aws --output json 
         --profile $AwsProfile --region $RegionName --color on `
            wafv2 put-logging-configuration `
            --logging-configuration $configuration;
        if (-not $?) {
            Write-Host "Creating a web ACL logging configuration failed" 
                        -ForegroundColor Red;
            return $null;
        }
        else {
            Write-Verbose "Logging configuration to Web ACL '$webAclARN' is added";
        }
    }
    #endregion

    #region Add Web ACL association
    Write-Host "Pause the script until a web ACL completes initialization";
    Start-Sleep -Seconds 15;
    aws --output json --profile $AwsProfile --region $RegionName --color on `
        wafv2 associate-web-acl `
        --web-acl-arn $webAclARN `
        --resource-arn $ResourceARN;
    if (-not $?) {
        Write-Host "Web ACL association is not created" -ForegroundColor Red;
        return $null;
    }        

    Write-Host "Web ACL is created and is associated with the resource:
               `nweb ACL ARN=$webAclARN`nResource ARN=$ResourceARN";
    #endregion

    return $webAclARN;
}
finally {
    $scriptDuration = [DateTime]::Now - $startDateTime;
    $fileName = $(Split-Path -Path $PSCommandPath -Leaf);
    Write-Host "**************************************************" 
                -ForegroundColor Green;
    Write-Host "Script $fileName ends, 
    total duration $($scriptDuration.Days) day(s), 
    $($scriptDuration.Hours):$($scriptDuration.Minutes):
    $($scriptDuration.Seconds).$($scriptDuration.Milliseconds)" -ForegroundColor Blue;
}

Parameters

The script has the following parameters:

  • string $ResourceARN – the name of AWS resource such that its traffic should be protected by web ACL. Mandatory parameter with not empty value;
  • string $RulesFilename – the name of .json file with rule sets definitions. File should be located in the same folder as the main script. Mandatory parameter with not empty value;
  • string $TagName – the name to tag created resources. Mandatory parameter with not empty value, usually complete word defined by the project;
  • string $TagNamePrefix – the prefix of the resources’ names. Mandatory parameter with not empty value in lower case;
  • string $RegionName – the name of AWS Region where resources are created. Optional parameter with default value us-west-1;
  • string $AwsProfile – the name of user AWS profile name from .aws config file. Optional parameter with default value default.

Return Value

Function returns ARN of created web ACL or $null.

Workflow

At first, the script checks for the existent web ACL. There are several options:

  • web ACL ${tagNamePrefix}-web-owasp exists, and in such case the script stops. It checks at lines 44-58 by the calling Get-WAF2WebAclARN method.
  • considered AWS resource is associated with web ACL. By documentation only one web ACL could be associated with each resource, so in such case the script also stops. It checks at lines 60-75 by the calling Get-WAF2WebAclForResource method.

Another prerequisite is CloudWatch log group. CloudWatch log group is defined at lines 77-91. 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 or 180 days, but you may use any value that suits your tasks.

Then the script creates and initializes web ACL. At lines 94-101, the content of webacl-rules.json is read, and is used at line 115 as rules of the web ACL. At lines 108-116, AWS CLI method aws wafv2 create-web-acl is called, that creates regional web ACL. Output is parsed and if the call is successful ARN of web ACL is obtained.

Logging configuration is a separate AWS resource. It is created at lines 137-211. To check whether the logging configuration is already associated with web ACL, AWS CLI method aws wafv2 list-logging-configurations with query parameter

PowerShell
$queryRequest = "LoggingConfigurations[?contains
                 (ResourceArn, ``$webAclARN``) == ``true``]";

is called. If logging configuration doesn’t exist, new configuration is added by the method aws wafv2 put-logging-configuration at lines 200-202.

As the last step, created web ACL is associated with the provided AWS resource by the method aws wafv2 associate-web-acl at lines 216-219. Let’s note that if $ResourceARN is wrong AWS CLI method fails and the script returns $null despite web ACL is created.

Example

As the script accepts parameters, it could be called by the another script. The script create-webacl.example.ps1 defines several input parameters as a resource ARN $ResourceARN, tag names, and AWS region, and calls create-webacl.ps1:

PowerShell
$RegionName = "eu-west-1";
$AwsProfile = "BlogAuthor";
$ResourceARN = "arn:aws:elasticloadbalancing:$($RegionName):
                123456789012:loadbalancer/app/load-balancer-EXAMPLE/0123456789abcdef";

$result = .\create-webacl.ps1 `
    -resourcearn $ResourceARN `
    -rulesfilename "webacl-rules.json" `
    -tagname 'Blog' `
    -tagnameprefix 'blog' `
    -regionname $RegionName `
    -awsprofile $AwsProfile `
    -verbose;

if ((-not $?) -or ($null -eq $result)) {
    Write-Error "Web ACL or related resources are not created";
}

Outputs

There is script output where web ACL and log group is created successfully.

Script create-webacl.ps1 start time = 09/24/2022 18:29:17
Get-WAF2WebAclARN(webACL=blog-web-owasp-2, region=eu-west-1, profile=default) starts.
VERBOSE: Web ACL ‘blog-web-owasp-2’ doesn’t exist
Get-WAF2WebAclForResource(Resource=arn:aws:elasticloadbalancing:
eu-west-1:123456789012:loadbalancer/app/load-balancer-EXAMPLE/0123456789abcdef, 
          region=eu-west-1, profile=default) starts.
VERBOSE: The resource doesn’t have associated web ACL
New-CloudWatchLogGroup(LogGroup=aws-waf-logs-blog-web-owasp-2, 
    region=eu-west-1, profile=default) starts.
Get-CloudWatchLogGroupARN(LogGroup=aws-waf-logs-blog-web-owasp-2, 
region=eu-west-1, profile=default) starts.
VERBOSE: Log group ‘aws-waf-logs-blog-web-owasp-2’ doesn’t exist
VERBOSE: Log group ‘aws-waf-logs-blog-web-owasp-2’ doesn’t exist, let’s create it
Get-CloudWatchLogGroupARN(LogGroup=aws-waf-logs-blog-web-owasp-2, 
region=eu-west-1, profile=default) starts.
VERBOSE: Log group ‘aws-waf-logs-blog-web-owasp-2’ is found, 
ARN=arn:aws:logs:eu-west-1:123456789012:log-group:aws-waf-logs-blog-web-owasp-2:*
Log group ‘aws-waf-logs-blog-web-owasp-2’ is found, 
ARN=arn:aws:logs:eu-west-1:123456789012:log-group:aws-waf-logs-blog-web-owasp-2:*
VERBOSE: Rules file path: ‘webacl-rules.json’
Web ACL is created succesfully, Id=01234567-0123-4567-890a-01234567890a, 
ARN=arn:aws:wafv2:eu-west-1:123456789012:regional/webacl/
blog-web-owasp-2/01234567-0123-4567-890a-01234567890a
VERBOSE: Adding logging configuration to Web ACL 
‘arn:aws:wafv2:eu-west-1:123456789012:regional/webacl/
blog-web-owasp-2/01234567-0123-4567-890a-01234567890a’
VERBOSE: Logging configuration to Web ACL ‘arn:aws:wafv2:eu-west-1:
123456789012:regional/webacl/blog-web-owasp-2/
01234567-0123-4567-890a-01234567890a’ is added
Pause the script until a web ACL completes initialization
Web ACL is created and is associated with the resource:
web ACL ARN=arn:aws:wafv2:eu-west-1:123456789012:regional/
webacl/blog-web-owasp-2/01234567-0123-4567-890a-01234567890a
Resource ARN=arn:aws:elasticloadbalancing:eu-west-1:123456789012:
loadbalancer/app/load-balancer-EXAMPLE/0123456789abcdef
**************************************************
Script create-webacl.ps1 ends, total duration 0 day(s), 0:0:51.134

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».

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)