How to Set Up AWS WAF for Web Application Protection

Complete guide to AWS WAF configuration including managed rules, rate limiting, bot protection, custom rules, and integration with CloudFront, ALB, and API Gateway for comprehensive web application security.

10 min readUpdated 2026-01-13

AWS WAF (Web Application Firewall) protects your web applications from common web exploits, bots, and DDoS attacks. It filters malicious traffic before it reaches your application, providing protection against SQL injection, cross-site scripting (XSS), and other OWASP Top 10 vulnerabilities.

This article is part of our comprehensive Cloud Security Tips for 2026 guide covering essential practices for protecting your cloud environment.

AWS WAF Components

ComponentPurpose
Web ACLContainer for rules, attached to resources
RulesConditions that match web requests
Rule GroupsReusable collections of rules
IP SetsCollections of IP addresses for filtering
Regex Pattern SetsRegular expressions for matching
Managed RulesPre-built rule sets from AWS/Marketplace

Create a Web ACL with AWS Console

  1. Open the AWS WAF Console
  2. Click Create web ACL
  3. Enter a name (e.g., "production-web-acl")
  4. Select the resource type:
    • CloudFront distributions - For global scope
    • Regional resources - For ALB, API Gateway, AppSync
  5. Select the AWS resources to protect
  6. Add managed rule groups (recommended starting point)
  7. Set the default action (Allow or Block)
  8. Review and create the Web ACL

Create Web ACL with AWS CLI

# Create Web ACL for regional resources (ALB, API Gateway)
aws wafv2 create-web-acl \
  --name production-web-acl \
  --scope REGIONAL \
  --default-action Allow={} \
  --visibility-config SampledRequestsEnabled=true,CloudWatchMetricsEnabled=true,MetricName=production-web-acl \
  --rules '[
    {
      "Name": "AWS-AWSManagedRulesCommonRuleSet",
      "Priority": 1,
      "OverrideAction": {"None": {}},
      "Statement": {
        "ManagedRuleGroupStatement": {
          "VendorName": "AWS",
          "Name": "AWSManagedRulesCommonRuleSet"
        }
      },
      "VisibilityConfig": {
        "SampledRequestsEnabled": true,
        "CloudWatchMetricsEnabled": true,
        "MetricName": "AWSManagedRulesCommonRuleSetMetric"
      }
    }
  ]'

# For CloudFront (global scope), use --scope CLOUDFRONT
# and run from us-east-1 region

Add AWS Managed Rule Groups

Essential Managed Rules

# Update Web ACL with recommended managed rules
aws wafv2 update-web-acl \
  --name production-web-acl \
  --scope REGIONAL \
  --id "12345678-1234-1234-1234-123456789012" \
  --lock-token "abc123..." \
  --default-action Allow={} \
  --visibility-config SampledRequestsEnabled=true,CloudWatchMetricsEnabled=true,MetricName=production-web-acl \
  --rules '[
    {
      "Name": "AWS-AWSManagedRulesCommonRuleSet",
      "Priority": 1,
      "OverrideAction": {"None": {}},
      "Statement": {
        "ManagedRuleGroupStatement": {
          "VendorName": "AWS",
          "Name": "AWSManagedRulesCommonRuleSet"
        }
      },
      "VisibilityConfig": {
        "SampledRequestsEnabled": true,
        "CloudWatchMetricsEnabled": true,
        "MetricName": "CommonRuleSet"
      }
    },
    {
      "Name": "AWS-AWSManagedRulesKnownBadInputsRuleSet",
      "Priority": 2,
      "OverrideAction": {"None": {}},
      "Statement": {
        "ManagedRuleGroupStatement": {
          "VendorName": "AWS",
          "Name": "AWSManagedRulesKnownBadInputsRuleSet"
        }
      },
      "VisibilityConfig": {
        "SampledRequestsEnabled": true,
        "CloudWatchMetricsEnabled": true,
        "MetricName": "KnownBadInputs"
      }
    },
    {
      "Name": "AWS-AWSManagedRulesSQLiRuleSet",
      "Priority": 3,
      "OverrideAction": {"None": {}},
      "Statement": {
        "ManagedRuleGroupStatement": {
          "VendorName": "AWS",
          "Name": "AWSManagedRulesSQLiRuleSet"
        }
      },
      "VisibilityConfig": {
        "SampledRequestsEnabled": true,
        "CloudWatchMetricsEnabled": true,
        "MetricName": "SQLiRuleSet"
      }
    },
    {
      "Name": "AWS-AWSManagedRulesAmazonIpReputationList",
      "Priority": 4,
      "OverrideAction": {"None": {}},
      "Statement": {
        "ManagedRuleGroupStatement": {
          "VendorName": "AWS",
          "Name": "AWSManagedRulesAmazonIpReputationList"
        }
      },
      "VisibilityConfig": {
        "SampledRequestsEnabled": true,
        "CloudWatchMetricsEnabled": true,
        "MetricName": "IpReputationList"
      }
    }
  ]'

Available AWS Managed Rule Groups

Rule GroupProtectionWCUs
AWSManagedRulesCommonRuleSetOWASP Top 10, common attacks700
AWSManagedRulesKnownBadInputsRuleSetKnown exploit patterns200
AWSManagedRulesSQLiRuleSetSQL injection attacks200
AWSManagedRulesLinuxRuleSetLinux-specific vulnerabilities200
AWSManagedRulesUnixRuleSetPOSIX OS vulnerabilities100
AWSManagedRulesWindowsRuleSetWindows-specific vulnerabilities200
AWSManagedRulesAmazonIpReputationListMalicious IPs from AWS threat intel25
AWSManagedRulesAnonymousIpListVPNs, proxies, Tor nodes50
AWSManagedRulesBotControlRuleSetBot detection and control50

Configure Rate Limiting

# Add rate limiting rule to prevent brute force/DDoS
aws wafv2 create-rule-group \
  --name rate-limit-rules \
  --scope REGIONAL \
  --capacity 50 \
  --visibility-config SampledRequestsEnabled=true,CloudWatchMetricsEnabled=true,MetricName=RateLimitRules \
  --rules '[
    {
      "Name": "RateLimit-All",
      "Priority": 1,
      "Action": {"Block": {}},
      "Statement": {
        "RateBasedStatement": {
          "Limit": 2000,
          "AggregateKeyType": "IP"
        }
      },
      "VisibilityConfig": {
        "SampledRequestsEnabled": true,
        "CloudWatchMetricsEnabled": true,
        "MetricName": "RateLimitAll"
      }
    },
    {
      "Name": "RateLimit-Login",
      "Priority": 2,
      "Action": {"Block": {}},
      "Statement": {
        "RateBasedStatement": {
          "Limit": 100,
          "AggregateKeyType": "IP",
          "ScopeDownStatement": {
            "ByteMatchStatement": {
              "SearchString": "/login",
              "FieldToMatch": {"UriPath": {}},
              "TextTransformations": [{"Priority": 0, "Type": "LOWERCASE"}],
              "PositionalConstraint": "STARTS_WITH"
            }
          }
        }
      },
      "VisibilityConfig": {
        "SampledRequestsEnabled": true,
        "CloudWatchMetricsEnabled": true,
        "MetricName": "RateLimitLogin"
      }
    }
  ]'

Enable Bot Control

# Add Bot Control managed rule group
{
  "Name": "AWS-AWSManagedRulesBotControlRuleSet",
  "Priority": 0,
  "OverrideAction": {"None": {}},
  "Statement": {
    "ManagedRuleGroupStatement": {
      "VendorName": "AWS",
      "Name": "AWSManagedRulesBotControlRuleSet",
      "ManagedRuleGroupConfigs": [
        {
          "AWSManagedRulesBotControlRuleSet": {
            "InspectionLevel": "COMMON"
          }
        }
      ]
    }
  },
  "VisibilityConfig": {
    "SampledRequestsEnabled": true,
    "CloudWatchMetricsEnabled": true,
    "MetricName": "BotControl"
  }
}

Bot Control Inspection Levels

LevelDescriptionCost
COMMONBasic bot detection using request signaturesBase rate
TARGETEDAdvanced detection using ML and browser fingerprintingHigher rate

Create Custom Rules

Block Specific Countries

{
  "Name": "GeoBlock-HighRiskCountries",
  "Priority": 5,
  "Action": {"Block": {}},
  "Statement": {
    "GeoMatchStatement": {
      "CountryCodes": ["RU", "CN", "KP", "IR"]
    }
  },
  "VisibilityConfig": {
    "SampledRequestsEnabled": true,
    "CloudWatchMetricsEnabled": true,
    "MetricName": "GeoBlock"
  }
}

Block Bad User Agents

# Create regex pattern set
aws wafv2 create-regex-pattern-set \
  --name bad-user-agents \
  --scope REGIONAL \
  --regular-expression-list '[
    {"RegexString": ".*curl.*"},
    {"RegexString": ".*wget.*"},
    {"RegexString": ".*python-requests.*"},
    {"RegexString": ".*sqlmap.*"},
    {"RegexString": ".*nikto.*"}
  ]'

# Use in rule
{
  "Name": "BlockBadUserAgents",
  "Priority": 6,
  "Action": {"Block": {}},
  "Statement": {
    "RegexPatternSetReferenceStatement": {
      "ARN": "arn:aws:wafv2:us-east-1:123456789012:regional/regexpatternset/bad-user-agents/12345678-1234-1234-1234-123456789012",
      "FieldToMatch": {"SingleHeader": {"Name": "user-agent"}},
      "TextTransformations": [{"Priority": 0, "Type": "LOWERCASE"}]
    }
  },
  "VisibilityConfig": {
    "SampledRequestsEnabled": true,
    "CloudWatchMetricsEnabled": true,
    "MetricName": "BlockBadUserAgents"
  }
}

Protect Admin Paths

{
  "Name": "ProtectAdminPaths",
  "Priority": 7,
  "Action": {"Block": {}},
  "Statement": {
    "AndStatement": {
      "Statements": [
        {
          "ByteMatchStatement": {
            "SearchString": "/admin",
            "FieldToMatch": {"UriPath": {}},
            "TextTransformations": [{"Priority": 0, "Type": "LOWERCASE"}],
            "PositionalConstraint": "STARTS_WITH"
          }
        },
        {
          "NotStatement": {
            "Statement": {
              "IPSetReferenceStatement": {
                "ARN": "arn:aws:wafv2:us-east-1:123456789012:regional/ipset/admin-whitelist/..."
              }
            }
          }
        }
      ]
    }
  },
  "VisibilityConfig": {
    "SampledRequestsEnabled": true,
    "CloudWatchMetricsEnabled": true,
    "MetricName": "ProtectAdminPaths"
  }
}

Terraform Configuration

resource "aws_wafv2_web_acl" "main" {
  name        = "production-web-acl"
  description = "Production WAF rules"
  scope       = "REGIONAL"

  default_action {
    allow {}
  }

  # AWS Managed Rules - Common Rule Set
  rule {
    name     = "AWSManagedRulesCommonRuleSet"
    priority = 1

    override_action {
      none {}
    }

    statement {
      managed_rule_group_statement {
        name        = "AWSManagedRulesCommonRuleSet"
        vendor_name = "AWS"
      }
    }

    visibility_config {
      cloudwatch_metrics_enabled = true
      metric_name                = "AWSManagedRulesCommonRuleSet"
      sampled_requests_enabled   = true
    }
  }

  # Rate limiting
  rule {
    name     = "RateLimit"
    priority = 10

    action {
      block {}
    }

    statement {
      rate_based_statement {
        limit              = 2000
        aggregate_key_type = "IP"
      }
    }

    visibility_config {
      cloudwatch_metrics_enabled = true
      metric_name                = "RateLimit"
      sampled_requests_enabled   = true
    }
  }

  # Geo blocking
  rule {
    name     = "GeoBlock"
    priority = 20

    action {
      block {}
    }

    statement {
      geo_match_statement {
        country_codes = ["RU", "CN", "KP", "IR"]
      }
    }

    visibility_config {
      cloudwatch_metrics_enabled = true
      metric_name                = "GeoBlock"
      sampled_requests_enabled   = true
    }
  }

  visibility_config {
    cloudwatch_metrics_enabled = true
    metric_name                = "production-web-acl"
    sampled_requests_enabled   = true
  }
}

# Associate with ALB
resource "aws_wafv2_web_acl_association" "alb" {
  resource_arn = aws_lb.main.arn
  web_acl_arn  = aws_wafv2_web_acl.main.arn
}

Enable WAF Logging

# Create S3 bucket for logs (must have aws-waf-logs- prefix)
aws s3api create-bucket \
  --bucket aws-waf-logs-production \
  --region us-east-1

# Enable logging
aws wafv2 put-logging-configuration \
  --logging-configuration '{
    "ResourceArn": "arn:aws:wafv2:us-east-1:123456789012:regional/webacl/production-web-acl/...",
    "LogDestinationConfigs": [
      "arn:aws:s3:::aws-waf-logs-production"
    ],
    "RedactedFields": [],
    "ManagedByFirewallManager": false
  }'

# Alternative: Send to CloudWatch Logs
aws logs create-log-group --log-group-name aws-waf-logs/production

aws wafv2 put-logging-configuration \
  --logging-configuration '{
    "ResourceArn": "arn:aws:wafv2:us-east-1:123456789012:regional/webacl/production-web-acl/...",
    "LogDestinationConfigs": [
      "arn:aws:logs:us-east-1:123456789012:log-group:aws-waf-logs/production"
    ]
  }'

Monitor WAF with CloudWatch

# Create alarm for high block rate
aws cloudwatch put-metric-alarm \
  --alarm-name waf-high-block-rate \
  --alarm-description "High WAF block rate indicates attack" \
  --metric-name BlockedRequests \
  --namespace AWS/WAFV2 \
  --statistic Sum \
  --period 300 \
  --threshold 1000 \
  --comparison-operator GreaterThanThreshold \
  --evaluation-periods 2 \
  --dimensions Name=WebACL,Value=production-web-acl Name=Region,Value=us-east-1 Name=Rule,Value=ALL \
  --alarm-actions arn:aws:sns:us-east-1:123456789012:security-alerts

# Query recent blocks
aws wafv2 get-sampled-requests \
  --web-acl-arn "arn:aws:wafv2:us-east-1:123456789012:regional/webacl/production-web-acl/..." \
  --rule-metric-name AWSManagedRulesCommonRuleSet \
  --scope REGIONAL \
  --time-window StartTime=$(date -d "1 hour ago" +%s),EndTime=$(date +%s) \
  --max-items 100

Best Practices Summary

PracticeRecommendation
Start with Managed RulesEnable AWS Core Rule Set first
Test Before BlockingUse Count mode for new rules
Enable LoggingSend to S3 or CloudWatch Logs
Rate LimitingProtect login and API endpoints
Bot ProtectionEnable Bot Control for public sites
MonitoringSet up CloudWatch alarms
Regular ReviewAnalyze blocked requests weekly

Frequently Asked Questions

Find answers to common questions

AWS WAF pricing has three components. Web ACL costs $5 per month per Web ACL. Each rule costs $1 per month per rule. Request charges are $0.60 per million requests evaluated. Managed rule groups from AWS or Marketplace have additional subscription fees, typically $1-20 per month depending on the rule set. Bot Control adds $10 per month plus $1 per million requests. For most applications, expect $20-50 per month for basic protection with managed rules.

Need Professional Help?

Our team of experts can help you implement and configure these solutions for your organization.