AWS WAF Classic Global Web ACL Has Logging Enabled
Overview
This check verifies that your AWS WAF Classic global Web ACLs have logging enabled. When enabled, WAF logging captures detailed information about web requests that your Web ACL evaluates, including the action taken on each request and the rules that matched.
Important: This check applies to AWS WAF Classic (the older version), not AWS WAFv2. AWS WAF Classic global Web ACLs are used with Amazon CloudFront distributions. If you are using the newer WAFv2, see the corresponding WAFv2 check instead.
Risk
Without WAF logging enabled, you lose visibility into:
- Attack patterns: SQL injection, cross-site scripting (XSS), and other web exploits targeting your applications
- Bot activity: Automated scanning, credential stuffing, and brute-force attempts
- Rule effectiveness: Which rules are matching and whether they are blocking or allowing traffic
- Forensic data: Evidence needed for incident investigation and response
This lack of visibility makes it difficult to detect ongoing attacks, tune your WAF rules, or respond effectively to security incidents.
Severity: Medium
Remediation Steps
Prerequisites
- Access to the AWS Console with permissions to modify WAF Classic and Kinesis Data Firehose
- The Firehose delivery stream must be named with the prefix
aws-waf-logs- - For global (CloudFront) Web ACLs, the Firehose stream must be created in us-east-1
Understanding WAF Classic logging requirements
AWS WAF Classic logging has specific requirements:
-
Kinesis Data Firehose only: WAF Classic can only send logs to Kinesis Data Firehose (not S3 directly, CloudWatch Logs, or other destinations)
-
Naming convention: The Firehose delivery stream name must start with
aws-waf-logs-(e.g.,aws-waf-logs-cloudfront-acl) -
Region requirements:
- For global Web ACLs (used with CloudFront): Create the Firehose stream in us-east-1
- For regional Web ACLs: Create the Firehose stream in the same region as the Web ACL
-
IAM permissions: AWS WAF creates a service-linked role automatically when you enable logging
-
Log destination: The Firehose stream can deliver logs to S3, OpenSearch, Splunk, or other supported destinations
AWS Console Method
Step 1: Create a Kinesis Data Firehose Delivery Stream
If you already have a Firehose stream named aws-waf-logs-* in us-east-1, skip to Step 2.
- Sign in to the AWS Console
- Ensure you are in the US East (N. Virginia) / us-east-1 region
- Navigate to Kinesis (search for it in the search bar)
- In the left sidebar, click Data Firehose
- Click Create Firehose stream
- Configure the stream:
- Source: Select Direct PUT
- Destination: Select Amazon S3 (or your preferred destination)
- Firehose stream name: Enter a name starting with
aws-waf-logs-(e.g.,aws-waf-logs-global-acl)
- Under Destination settings:
- Select or create an S3 bucket for log storage
- Configure optional prefix for organizing logs (e.g.,
waf-logs/year=!{timestamp:yyyy}/month=!{timestamp:MM}/)
- Click Create Firehose stream
- Wait for the stream status to become Active
Step 2: Enable Logging on the WAF Classic Web ACL
- Navigate to WAF & Shield in the AWS Console
- In the left sidebar under AWS WAF Classic, click Web ACLs
- In the Filter dropdown, select Global (CloudFront)
- Click on the Web ACL you want to enable logging for
- Click the Logging and metrics tab (or Logging tab)
- Click Enable logging
- In the Amazon Kinesis Data Firehose dropdown, select your Firehose stream (e.g.,
aws-waf-logs-global-acl) - (Optional) Configure Redacted fields if you want to exclude sensitive data from logs (e.g., query strings, headers)
- Click Enable logging
AWS CLI (optional)
List Global WAF Classic Web ACLs
First, identify your Web ACL ARN:
aws waf list-web-acls \
--region us-east-1
Example output:
{
"WebACLs": [
{
"WebACLId": "a1b2c3d4-5678-90ab-cdef-EXAMPLE11111",
"Name": "my-global-webacl"
}
]
}
Get the Web ACL ARN:
aws waf get-web-acl \
--web-acl-id a1b2c3d4-5678-90ab-cdef-EXAMPLE11111 \
--region us-east-1 \
--query 'WebACL.WebACLArn'
Create a Kinesis Data Firehose Delivery Stream
Create an S3 bucket for logs (if needed):
aws s3 mb s3://my-waf-logs-bucket-123456789012 \
--region us-east-1
Create the Firehose stream (name must start with aws-waf-logs-):
aws firehose create-delivery-stream \
--delivery-stream-name aws-waf-logs-global-acl \
--delivery-stream-type DirectPut \
--extended-s3-destination-configuration '{
"RoleARN": "arn:aws:iam::123456789012:role/firehose-waf-logs-role",
"BucketARN": "arn:aws:s3:::my-waf-logs-bucket-123456789012",
"Prefix": "waf-logs/",
"ErrorOutputPrefix": "waf-logs-errors/",
"BufferingHints": {
"SizeInMBs": 5,
"IntervalInSeconds": 300
},
"CompressionFormat": "GZIP"
}' \
--region us-east-1
Note: You need to create the IAM role firehose-waf-logs-role with permissions to write to S3 before running this command.
Enable Logging on the Web ACL
aws waf put-logging-configuration \
--logging-configuration '{
"ResourceArn": "arn:aws:waf::123456789012:webacl/a1b2c3d4-5678-90ab-cdef-EXAMPLE11111",
"LogDestinationConfigs": [
"arn:aws:firehose:us-east-1:123456789012:deliverystream/aws-waf-logs-global-acl"
]
}' \
--region us-east-1
Replace:
123456789012with your AWS account IDa1b2c3d4-5678-90ab-cdef-EXAMPLE11111with your Web ACL IDaws-waf-logs-global-aclwith your Firehose stream name
Enable Logging with Redacted Fields
To exclude sensitive data from logs:
aws waf put-logging-configuration \
--logging-configuration '{
"ResourceArn": "arn:aws:waf::123456789012:webacl/a1b2c3d4-5678-90ab-cdef-EXAMPLE11111",
"LogDestinationConfigs": [
"arn:aws:firehose:us-east-1:123456789012:deliverystream/aws-waf-logs-global-acl"
],
"RedactedFields": [
{"Type": "QUERY_STRING"},
{"Type": "HEADER", "Data": "Authorization"}
]
}' \
--region us-east-1
CloudFormation (optional)
CloudFormation Template
This template creates the Firehose delivery stream, S3 bucket, and IAM role required for WAF Classic logging.
Note: CloudFormation cannot directly configure WAF Classic logging. You must enable logging via the console or CLI after deploying this stack.
AWSTemplateFormatVersion: '2010-09-09'
Description: Kinesis Firehose delivery stream for AWS WAF Classic logging
Parameters:
FirehoseStreamName:
Type: String
Default: aws-waf-logs-global-acl
Description: Name for the Firehose stream (must start with aws-waf-logs-)
AllowedPattern: ^aws-waf-logs-.*
ConstraintDescription: Firehose stream name must start with 'aws-waf-logs-'
Resources:
WafLogsBucket:
Type: AWS::S3::Bucket
Properties:
BucketName: !Sub 'waf-logs-${AWS::AccountId}-${AWS::Region}'
BucketEncryption:
ServerSideEncryptionConfiguration:
- ServerSideEncryptionByDefault:
SSEAlgorithm: AES256
PublicAccessBlockConfiguration:
BlockPublicAcls: true
BlockPublicPolicy: true
IgnorePublicAcls: true
RestrictPublicBuckets: true
LifecycleConfiguration:
Rules:
- Id: ExpireOldLogs
Status: Enabled
ExpirationInDays: 90
FirehoseRole:
Type: AWS::IAM::Role
Properties:
RoleName: !Sub '${FirehoseStreamName}-role'
AssumeRolePolicyDocument:
Version: '2012-10-17'
Statement:
- Effect: Allow
Principal:
Service: firehose.amazonaws.com
Action: sts:AssumeRole
Policies:
- PolicyName: FirehoseS3Access
PolicyDocument:
Version: '2012-10-17'
Statement:
- Effect: Allow
Action:
- s3:PutObject
- s3:GetBucketLocation
- s3:ListBucket
Resource:
- !GetAtt WafLogsBucket.Arn
- !Sub '${WafLogsBucket.Arn}/*'
WafLogsFirehose:
Type: AWS::KinesisFirehose::DeliveryStream
Properties:
DeliveryStreamName: !Ref FirehoseStreamName
DeliveryStreamType: DirectPut
ExtendedS3DestinationConfiguration:
BucketARN: !GetAtt WafLogsBucket.Arn
RoleARN: !GetAtt FirehoseRole.Arn
Prefix: 'waf-logs/year=!{timestamp:yyyy}/month=!{timestamp:MM}/day=!{timestamp:dd}/'
ErrorOutputPrefix: 'waf-logs-errors/!{firehose:error-output-type}/year=!{timestamp:yyyy}/month=!{timestamp:MM}/day=!{timestamp:dd}/'
BufferingHints:
SizeInMBs: 5
IntervalInSeconds: 300
CompressionFormat: GZIP
Outputs:
FirehoseArn:
Description: ARN of the Firehose delivery stream (use this when enabling WAF logging)
Value: !GetAtt WafLogsFirehose.Arn
S3BucketName:
Description: S3 bucket where WAF logs are stored
Value: !Ref WafLogsBucket
NextSteps:
Description: After stack creation, enable logging on your WAF Classic Web ACL
Value: !Sub |
Run: aws waf put-logging-configuration --logging-configuration '{"ResourceArn":"<YOUR_WEBACL_ARN>","LogDestinationConfigs":["${WafLogsFirehose.Arn}"]}'
Deploy the stack in us-east-1:
aws cloudformation create-stack \
--stack-name waf-classic-logging \
--template-body file://waf-logging.yaml \
--capabilities CAPABILITY_NAMED_IAM \
--region us-east-1
After the stack is created, enable logging using the CLI command shown in the stack outputs.
Terraform (optional)
Terraform Configuration
This configuration creates the Firehose delivery stream and supporting resources for WAF Classic logging.
Note: The AWS Terraform provider does not have a native resource for WAF Classic logging configuration. This example uses a null_resource with local-exec to call the AWS CLI after creating the infrastructure.
provider "aws" {
region = "us-east-1"
}
variable "web_acl_arn" {
description = "ARN of the existing WAF Classic Web ACL"
type = string
}
# S3 bucket for WAF logs
resource "aws_s3_bucket" "waf_logs" {
bucket = "waf-logs-${data.aws_caller_identity.current.account_id}"
}
resource "aws_s3_bucket_server_side_encryption_configuration" "waf_logs" {
bucket = aws_s3_bucket.waf_logs.id
rule {
apply_server_side_encryption_by_default {
sse_algorithm = "AES256"
}
}
}
resource "aws_s3_bucket_public_access_block" "waf_logs" {
bucket = aws_s3_bucket.waf_logs.id
block_public_acls = true
block_public_policy = true
ignore_public_acls = true
restrict_public_buckets = true
}
# IAM role for Firehose
resource "aws_iam_role" "firehose" {
name = "waf-logs-firehose-role"
assume_role_policy = jsonencode({
Version = "2012-10-17"
Statement = [
{
Effect = "Allow"
Principal = {
Service = "firehose.amazonaws.com"
}
Action = "sts:AssumeRole"
}
]
})
}
resource "aws_iam_role_policy" "firehose_s3" {
name = "firehose-s3-access"
role = aws_iam_role.firehose.id
policy = jsonencode({
Version = "2012-10-17"
Statement = [
{
Effect = "Allow"
Action = [
"s3:PutObject",
"s3:GetBucketLocation",
"s3:ListBucket"
]
Resource = [
aws_s3_bucket.waf_logs.arn,
"${aws_s3_bucket.waf_logs.arn}/*"
]
}
]
})
}
# Kinesis Firehose delivery stream (name must start with aws-waf-logs-)
resource "aws_kinesis_firehose_delivery_stream" "waf_logs" {
name = "aws-waf-logs-global-acl"
destination = "extended_s3"
extended_s3_configuration {
role_arn = aws_iam_role.firehose.arn
bucket_arn = aws_s3_bucket.waf_logs.arn
prefix = "waf-logs/year=!{timestamp:yyyy}/month=!{timestamp:MM}/day=!{timestamp:dd}/"
error_output_prefix = "waf-logs-errors/!{firehose:error-output-type}/year=!{timestamp:yyyy}/month=!{timestamp:MM}/day=!{timestamp:dd}/"
buffering_size = 5
buffering_interval = 300
compression_format = "GZIP"
}
}
data "aws_caller_identity" "current" {}
# Enable WAF Classic logging using AWS CLI (no native Terraform resource exists)
resource "null_resource" "waf_logging" {
depends_on = [aws_kinesis_firehose_delivery_stream.waf_logs]
provisioner "local-exec" {
command = <<-EOT
aws waf put-logging-configuration \
--logging-configuration '{
"ResourceArn": "${var.web_acl_arn}",
"LogDestinationConfigs": ["${aws_kinesis_firehose_delivery_stream.waf_logs.arn}"]
}' \
--region us-east-1
EOT
}
# Re-run if the Web ACL ARN or Firehose stream changes
triggers = {
web_acl_arn = var.web_acl_arn
firehose_arn = aws_kinesis_firehose_delivery_stream.waf_logs.arn
}
}
# Outputs
output "firehose_arn" {
description = "ARN of the Firehose delivery stream"
value = aws_kinesis_firehose_delivery_stream.waf_logs.arn
}
output "s3_bucket" {
description = "S3 bucket where WAF logs are stored"
value = aws_s3_bucket.waf_logs.id
}
output "next_steps" {
description = "Manual verification"
value = "Verify logging is enabled: aws waf get-logging-configuration --resource-arn ${var.web_acl_arn} --region us-east-1"
}
Apply the Configuration
First, get your WAF Classic Web ACL ARN:
aws waf list-web-acls --region us-east-1
aws waf get-web-acl --web-acl-id <WEB_ACL_ID> --region us-east-1 --query 'WebACL.WebACLArn' --output text
Then apply the Terraform configuration:
terraform init
terraform plan -var="web_acl_arn=arn:aws:waf::123456789012:webacl/a1b2c3d4-5678-90ab-cdef-EXAMPLE11111"
terraform apply -var="web_acl_arn=arn:aws:waf::123456789012:webacl/a1b2c3d4-5678-90ab-cdef-EXAMPLE11111"
Verification
After enabling logging, verify the configuration:
- Open the WAF & Shield console
- In the left sidebar under AWS WAF Classic, click Web ACLs
- Filter by Global (CloudFront)
- Click on your Web ACL
- Click the Logging and metrics tab
- Confirm that Logging shows as Enabled with your Firehose stream name
To verify logs are flowing:
- Generate some test traffic to a CloudFront distribution protected by the Web ACL
- Wait a few minutes for logs to buffer and deliver
- Check your S3 bucket for log files
CLI verification commands
Check if logging is enabled on a Web ACL:
aws waf get-logging-configuration \
--resource-arn arn:aws:waf::123456789012:webacl/a1b2c3d4-5678-90ab-cdef-EXAMPLE11111 \
--region us-east-1
Expected output when logging is enabled:
{
"LoggingConfiguration": {
"ResourceArn": "arn:aws:waf::123456789012:webacl/a1b2c3d4-5678-90ab-cdef-EXAMPLE11111",
"LogDestinationConfigs": [
"arn:aws:firehose:us-east-1:123456789012:deliverystream/aws-waf-logs-global-acl"
]
}
}
List all Web ACLs and their logging status:
for acl in $(aws waf list-web-acls --query 'WebACLs[*].WebACLId' --output text --region us-east-1); do
arn=$(aws waf get-web-acl --web-acl-id "$acl" --query 'WebACL.WebACLArn' --output text --region us-east-1)
echo "Web ACL: $acl"
aws waf get-logging-configuration --resource-arn "$arn" --region us-east-1 2>/dev/null || echo " Logging: NOT ENABLED"
echo ""
done
Re-run Prowler to confirm the check passes:
prowler aws --check waf_global_webacl_logging_enabled
Additional Resources
- Logging Web ACL traffic information - AWS WAF Classic Developer Guide
- Amazon Kinesis Data Firehose - Developer Guide
- Migrating from AWS WAF Classic to AWS WAFv2
- AWS WAF Pricing
Notes
-
WAF Classic vs. WAFv2: This check applies to AWS WAF Classic, which is the older version of AWS WAF. AWS recommends migrating to WAFv2 for new deployments, as it offers more features and a unified API for both regional and global resources.
-
Firehose naming requirement: The delivery stream name must begin with
aws-waf-logs-. AWS WAF will not accept streams with other naming patterns. -
Region requirement: For global Web ACLs (used with CloudFront), both the Web ACL and the Firehose stream must be accessed via the us-east-1 region.
-
Log format: WAF Classic logs are in JSON format and include fields such as timestamp, action taken, rule matched, client IP, country, URI, and HTTP headers.
-
Cost considerations: Enabling logging incurs costs for Kinesis Data Firehose data ingestion and the destination storage (S3, OpenSearch, etc.). Review Firehose pricing and your destination service pricing.
-
Log retention: Configure S3 lifecycle policies to manage log retention and reduce storage costs for older logs.
-
Redacted fields: Consider redacting sensitive data (query strings, authorization headers) from logs to protect user privacy and comply with data protection requirements.