65.9K
CodeProject 正在变化。 阅读更多。
Home

如何使用 PowerShell 创建区域 Web ACL (WAFv2)

emptyStarIconemptyStarIconemptyStarIconemptyStarIconemptyStarIcon

0/5 (0投票)

2022 年 9 月 24 日

CPOL

5分钟阅读

viewsIcon

1968

本文介绍了一个 PowerShell 脚本,用于在应用程序负载均衡器 (Application Load Balancer) 用于为公共网站提供内容、但需要阻止攻击者的请求并防范 OWASP Top 10 安全风险的场景下创建 WAF 资源。

引言

AWS WAF 是一种 Web 应用程序防火墙服务,可让您监控转发到 Amazon CloudFront 分发、Amazon API Gateway REST API、应用程序负载均衡器或 AWS AppSync GraphQL API 的 Web 请求。本文介绍了一个 PowerShell 脚本,用于在应用程序负载均衡器 (Application Load Balancer) 用于为公共网站提供内容、但需要阻止攻击者的请求并防范 OWASP Top 10 安全风险的场景下创建 Web ACL 资源。提供的解决方案可以轻松地适用于其他使用场景。

另一篇文章 如何使用 CloudFormation 创建区域 Web ACL (WAFv2) 使用 CloudFormation 模板解决了此任务。由于脚本创建的对象相似,因此省略了一些描述。

背景

该解决方案使用了 PowerShell Core 7.2、AWS CLI v2、WAF v2、Web ACL、CloudWatch、ALB。

任务

要为 ALB 设置 AWS WAF,我们需要创建 Web ACL、日志记录配置以及 Web ACL 与 AWS 资源之间的关联。在我们的示例中,Web ACL 与作为另一个脚本一部分创建的应用程序负载均衡器相关联,因此其 ARN 作为脚本参数提供。

解决方案

该解决方案包含以下文件

  1. create-webacl.ps1 – 主脚本,用于检查先决条件、创建附加资源并创建 Web ACL。
  2. functions.ps1 – 包含几个用于操作 CloudWatch 日志组和 Web ACL 的函数。这些函数在 使用 PowerShell Core 获取 AWS CloudWatch 日志组使用 PowerShell Core 获取 AWS Web ACL 中进行了描述。
  3. webacl-rules.json – 包含 Web ACL 规则集定义的文件的名称。使用的规则集在文章 如何使用 CloudFormation 创建区域 Web ACL (WAFv2) 中进行了描述。将这些定义保留在单独的文件中可以使代码更整洁、更短。
  4. create-webacl.example.ps1 – 包含调用主脚本示例的脚本。

特点

脚本 create-webacl.ps1 具有以下功能

  • 脚本是幂等的,它会检查现有资源,并且可以多次运行以获得相同的结果。
  • 如果所有操作都成功,脚本将返回 Web ACL ARN;否则,它将返回 $null
  • 脚本不捕获异常,而是将其抛到上层。
  • 脚本提供默认和详细的输出,包括脚本名称和持续时间。

create-webacl.ps1 列表

这是主脚本的列表

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;
}

参数

脚本具有以下参数

  • string $ResourceARN – 需要通过 Web ACL 保护流量的 AWS 资源的名称。必填参数,值不能为空;
  • string $RulesFilename – 包含规则集定义的 .json 文件的名称。文件应位于主脚本所在的文件夹中。必填参数,值不能为空;
  • string $TagName – 用于标记创建的资源的名称。必填参数,值不能为空,通常是项目定义的完整单词;
  • string $TagNamePrefix – 资源名称的前缀。必填参数,值不能为空且必须为小写;
  • string $RegionName – 创建资源的 AWS 区域 的名称。可选参数,默认为 us-west-1
  • string $AwsProfile – 来自 .aws config 文件的用户 AWS profile 名称。可选参数,默认为 default

返回值

函数返回创建的 Web ACL 的 ARN 或 $null

工作流

首先,脚本会检查现有的 Web ACL。有几种情况:

  • Web ACL ${tagNamePrefix}-web-owasp 已存在,在这种情况下脚本会停止。它在第 44-58 行通过调用 Get-WAF2WebAclARN 方法进行检查。
  • 考虑的 AWS 资源已与 Web ACL 关联。根据 文档,每个资源只能关联一个 Web ACL,因此在这种情况下脚本也会停止。它在第 60-75 行通过调用 Get-WAF2WebAclForResource 方法进行检查。

另一个先决条件是 CloudWatch 日志组。CloudWatch 日志组在第 77-91 行定义。请注意,其名称应以 aws-waf-logs- 开头,否则 Web ACL 不会将日志组视为有效的日志目标。我们使用了中等保留时间,为 6 个月或 180 天,但您可以根据您的任务使用任何值。

然后脚本创建并初始化 Web ACL。在第 94-101 行,读取 webacl-rules.json 的内容,并在第 115 行用作 Web ACL 的规则。在第 108-116 行,调用 AWS CLI 方法 aws wafv2 create-web-acl,该方法创建区域 Web ACL。解析输出,如果调用成功,则获得 Web ACL 的 ARN。

日志记录配置是独立的 AWS 资源。它在第 137-211 行创建。为了检查日志记录配置是否已与 Web ACL 关联,调用了 AWS CLI 方法 aws wafv2 list-logging-configurations 并带上查询参数

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

。如果日志记录配置不存在,则在第 200-202 行使用方法 aws wafv2 put-logging-configuration 添加新配置。

作为最后一步,在第 216-219 行使用方法 aws wafv2 associate-web-acl 将创建的 Web ACL 与提供的 AWS 资源关联。请注意,如果 $ResourceARN 错误,即使创建了 Web ACL,AWS CLI 方法也会失败,脚本会返回 $null

示例

由于脚本接受参数,它可以被另一个脚本调用。脚本 create-webacl.example.ps1 定义了几个输入参数,例如资源 ARN $ResourceARN、标签名称和 AWS 区域,然后调用 create-webacl.ps1

$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";
}

输出

这是脚本输出,其中 Web ACL 和日志组已成功创建。

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. 所有使用的 IP 地址、服务器名称、工作站名称、域名均为虚构,仅用于演示。
2. 信息按“原样”提供。

© . All rights reserved.