Documentation Index
Fetch the complete documentation index at: https://resources.devweekends.com/llms.txt
Use this file to discover all available pages before exploring further.
Overview
AWS WAF (Web Application Firewall) helps protect your web applications from common web exploits and bots that could affect availability, compromise security, or consume excessive resources. Think of WAF as a smart bouncer for your web application — it inspects every HTTP request before it reaches your servers and blocks anything suspicious (SQL injection, XSS, bot traffic, geographic origins you do not serve). Without WAF, your ALB or API Gateway accepts every request and passes it to your application, where a single SQL injection could dump your entire database. Think of WAF as the bouncer at your nightclub who checks IDs and pat-searches every person entering — it inspects every HTTP request against a list of rules (SQL injection patterns, known bad IPs, request rate limits) and either allows, blocks, or challenges suspicious requests before they ever reach your application code. Cost tip: WAF pricing has three components — 5/monthperWebACL,1/month per rule, and 0.60permillionrequestsinspected.Atypicalproductionsetupwith1WebACL,10rules,and100Mrequests/monthcostsroughly75/month. The expensive surprise is Bot Control: the “Targeted” inspection level costs 10/Mrequestsvs1/M for “Common.” At 100M requests/month, that is 1,000vs100 — choose the level that matches your actual bot threat.Core Concepts
WAF Components
Rule Types
Creating a Web ACL
Basic Setup
# Create IP set
aws wafv2 create-ip-set \
--name blocked-ips \
--scope REGIONAL \
--ip-address-version IPV4 \
--addresses 192.0.2.0/24 203.0.113.0/24
# Create Web ACL
aws wafv2 create-web-acl \
--name my-web-acl \
--scope REGIONAL \
--default-action Allow={} \
--rules file://rules.json \
--visibility-config SampledRequestsEnabled=true,CloudWatchMetricsEnabled=true,MetricName=MyWebACL
# Associate with ALB
aws wafv2 associate-web-acl \
--web-acl-arn arn:aws:wafv2:us-east-1:123456789012:regional/webacl/my-web-acl/... \
--resource-arn arn:aws:elasticloadbalancing:us-east-1:123456789012:loadbalancer/app/my-alb/...
Terraform Configuration
# IP Set for blocking
resource "aws_wafv2_ip_set" "blocked_ips" {
name = "blocked-ips"
description = "IP addresses to block"
scope = "REGIONAL"
ip_address_version = "IPV4"
addresses = [
"192.0.2.0/24",
"203.0.113.0/24"
]
tags = {
Environment = "production"
}
}
# Regex pattern set for SQL injection
resource "aws_wafv2_regex_pattern_set" "sql_patterns" {
name = "sql-injection-patterns"
scope = "REGIONAL"
regular_expression {
regex_string = "(?i)(union.*select|insert.*into|delete.*from)"
}
regular_expression {
regex_string = "(?i)(exec.*xp_|execute.*sp_)"
}
}
# Web ACL
resource "aws_wafv2_web_acl" "main" {
name = "production-web-acl"
scope = "REGIONAL"
default_action {
allow {}
}
# Rule 1: Rate limiting -- this should ALWAYS be your first rule.
# It protects against brute force, DDoS, and runaway bots before more
# expensive rules (like regex matching) even fire. The 2000 limit means
# "block any IP sending more than 2000 requests in 5 minutes."
# Common mistake: Setting this too low (blocking legitimate users) or
# too high (not catching attacks). Start at 2000, monitor with Count
# action for a week, then switch to Block once you understand your traffic.
rule {
name = "rate-limit-rule"
priority = 0
action {
block {}
}
statement {
rate_based_statement {
limit = 2000
aggregate_key_type = "IP"
}
}
visibility_config {
cloudwatch_metrics_enabled = true
metric_name = "RateLimitRule"
sampled_requests_enabled = true
}
}
# Rule 2: Block specific IPs
rule {
name = "block-bad-ips"
priority = 1
action {
block {
custom_response {
response_code = 403
}
}
}
statement {
ip_set_reference_statement {
arn = aws_wafv2_ip_set.blocked_ips.arn
}
}
visibility_config {
cloudwatch_metrics_enabled = true
metric_name = "BlockBadIPs"
sampled_requests_enabled = true
}
}
# Rule 3: Geo blocking
rule {
name = "geo-block"
priority = 2
action {
block {}
}
statement {
geo_match_statement {
country_codes = ["KP", "IR"] # North Korea, Iran
}
}
visibility_config {
cloudwatch_metrics_enabled = true
metric_name = "GeoBlock"
sampled_requests_enabled = true
}
}
# Rule 4: AWS Managed Rules - Core Rule Set.
# This is the single most impactful rule you can add -- it covers OWASP
# Top 10 vulnerabilities (SQLi, XSS, LFI, RFI) out of the box. AWS
# maintains and updates these rules, so you get protection against new
# CVEs without changing your config.
#
# Common mistake: Deploying managed rules in "Block" mode immediately.
# Always start with "Count" mode (override_action: count) for 1-2 weeks
# to observe which rules fire on legitimate traffic. Then switch to "none"
# (which means "use the rule group's native action," i.e., block).
rule {
name = "aws-managed-core-rule-set"
priority = 3
override_action {
none {} # Use the rule group's native action (block). Set to count{} for testing.
}
statement {
managed_rule_group_statement {
vendor_name = "AWS"
name = "AWSManagedRulesCommonRuleSet"
# Exclude specific rules that produce false positives for your app.
# SizeRestrictions_BODY is commonly excluded for file upload endpoints.
rule_action_override {
action_to_use {
count {}
}
name = "SizeRestrictions_BODY"
}
}
}
visibility_config {
cloudwatch_metrics_enabled = true
metric_name = "AWSManagedRulesCommonRuleSet"
sampled_requests_enabled = true
}
}
# Rule 5: SQL Injection protection
rule {
name = "sql-injection-protection"
priority = 4
action {
block {}
}
statement {
or_statement {
statement {
sqli_match_statement {
field_to_match {
query_string {}
}
text_transformation {
priority = 0
type = "URL_DECODE"
}
text_transformation {
priority = 1
type = "HTML_ENTITY_DECODE"
}
}
}
statement {
sqli_match_statement {
field_to_match {
body {
oversize_handling = "CONTINUE"
}
}
text_transformation {
priority = 0
type = "URL_DECODE"
}
}
}
}
}
visibility_config {
cloudwatch_metrics_enabled = true
metric_name = "SQLInjectionProtection"
sampled_requests_enabled = true
}
}
# Rule 6: XSS protection
rule {
name = "xss-protection"
priority = 5
action {
block {}
}
statement {
xss_match_statement {
field_to_match {
body {
oversize_handling = "CONTINUE"
}
}
text_transformation {
priority = 0
type = "URL_DECODE"
}
text_transformation {
priority = 1
type = "HTML_ENTITY_DECODE"
}
}
}
visibility_config {
cloudwatch_metrics_enabled = true
metric_name = "XSSProtection"
sampled_requests_enabled = true
}
}
# Rule 7: Bot Control
rule {
name = "bot-control"
priority = 6
override_action {
none {}
}
statement {
managed_rule_group_statement {
vendor_name = "AWS"
name = "AWSManagedRulesBotControlRuleSet"
managed_rule_group_configs {
aws_managed_rules_bot_control_rule_set {
inspection_level = "COMMON" # or "TARGETED"
}
}
}
}
visibility_config {
cloudwatch_metrics_enabled = true
metric_name = "BotControl"
sampled_requests_enabled = true
}
}
visibility_config {
cloudwatch_metrics_enabled = true
metric_name = "ProductionWebACL"
sampled_requests_enabled = true
}
tags = {
Environment = "production"
ManagedBy = "terraform"
}
}
# 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
}
# Associate with CloudFront (requires global scope)
resource "aws_wafv2_web_acl" "cloudfront" {
provider = aws.us-east-1 # CloudFront requires us-east-1
name = "cloudfront-web-acl"
scope = "CLOUDFRONT"
default_action {
allow {}
}
# Similar rules as above...
visibility_config {
cloudwatch_metrics_enabled = true
metric_name = "CloudFrontWebACL"
sampled_requests_enabled = true
}
}
Advanced Features
Bot Control
Bot Control Use Cases:
E-commerce:
- Prevent inventory hoarding
- Stop price scraping
- Block scalper bots
APIs:
- Rate limit automated requests
- Verify API consumers
- Prevent abuse
Content Sites:
- Allow search engine crawlers
- Block content scrapers
- Protect copyrighted material
Authentication:
- Prevent credential stuffing
- Block brute force attempts
- Protect login endpoints
CAPTCHA Challenge
# CAPTCHA configuration
resource "aws_wafv2_web_acl" "with_captcha" {
name = "captcha-enabled-acl"
scope = "REGIONAL"
default_action {
allow {}
}
rule {
name = "captcha-for-suspicious-requests"
priority = 0
action {
captcha {
custom_request_handling {
insert_header {
name = "x-waf-captcha"
value = "challenged"
}
}
}
}
statement {
rate_based_statement {
limit = 100
aggregate_key_type = "IP"
}
}
captcha_config {
immunity_time_property {
immunity_time = 300 # 5 minutes immunity after solving
}
}
visibility_config {
cloudwatch_metrics_enabled = true
metric_name = "CAPTCHARule"
sampled_requests_enabled = true
}
}
visibility_config {
cloudwatch_metrics_enabled = true
metric_name = "CAPTCHAWebACL"
sampled_requests_enabled = true
}
}
Custom Response
resource "aws_wafv2_web_acl" "custom_response" {
name = "custom-response-acl"
scope = "REGIONAL"
default_action {
allow {}
}
rule {
name = "block-with-custom-response"
priority = 0
action {
block {
custom_response {
response_code = 403
custom_response_body_key = "blocked_message"
response_header {
name = "x-block-reason"
value = "rate-limit-exceeded"
}
}
}
}
statement {
rate_based_statement {
limit = 2000
aggregate_key_type = "IP"
}
}
visibility_config {
cloudwatch_metrics_enabled = true
metric_name = "CustomResponseRule"
sampled_requests_enabled = true
}
}
custom_response_body {
key = "blocked_message"
content = jsonencode({
error = "Rate limit exceeded"
message = "Too many requests. Please try again later."
code = 403
})
content_type = "APPLICATION_JSON"
}
visibility_config {
cloudwatch_metrics_enabled = true
metric_name = "CustomResponseWebACL"
sampled_requests_enabled = true
}
}
Logging and Monitoring
Enable Logging
# S3 bucket for WAF logs
resource "aws_s3_bucket" "waf_logs" {
bucket = "my-waf-logs-${data.aws_caller_identity.current.account_id}"
}
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
}
# Kinesis Data Firehose for WAF logs
resource "aws_kinesis_firehose_delivery_stream" "waf_logs" {
name = "aws-waf-logs-delivery-stream"
destination = "extended_s3"
extended_s3_configuration {
role_arn = aws_iam_role.firehose.arn
bucket_arn = aws_s3_bucket.waf_logs.arn
prefix = "waf-logs/"
compression_format = "GZIP"
cloudwatch_logging_options {
enabled = true
log_group_name = aws_cloudwatch_log_group.waf_logs.name
log_stream_name = "S3Delivery"
}
}
}
# Enable WAF logging.
# Cost tip: WAF logs can be massive -- a site with 10M requests/day generates
# ~50 GB/day of logs. At $0.50/GB for CloudWatch Logs ingestion, that is
# $750/month just for log storage. Using Kinesis Firehose to S3 (below) is
# much cheaper (~$0.029/GB for S3 Standard). Only send BLOCK actions to
# CloudWatch for alerting; send full logs to S3 for forensic analysis.
resource "aws_wafv2_web_acl_logging_configuration" "main" {
resource_arn = aws_wafv2_web_acl.main.arn
log_destination_configs = [aws_kinesis_firehose_delivery_stream.waf_logs.arn]
# Always redact sensitive headers to avoid logging credentials or tokens.
# Common mistake: Logging WAF requests without redaction and accidentally
# storing auth tokens in S3 -- a compliance violation and security risk.
redacted_fields {
single_header {
name = "authorization"
}
}
redacted_fields {
single_header {
name = "cookie"
}
}
logging_filter {
default_behavior = "KEEP"
filter {
behavior = "KEEP"
condition {
action_condition {
action = "BLOCK"
}
}
requirement = "MEETS_ANY"
}
}
}
# CloudWatch Log Group
resource "aws_cloudwatch_log_group" "waf_logs" {
name = "/aws/waf/logs"
retention_in_days = 30
}
CloudWatch Metrics and Alarms
# Alarm for high block rate
resource "aws_cloudwatch_metric_alarm" "waf_high_block_rate" {
alarm_name = "waf-high-block-rate"
comparison_operator = "GreaterThanThreshold"
evaluation_periods = "2"
metric_name = "BlockedRequests"
namespace = "AWS/WAFV2"
period = "300"
statistic = "Sum"
threshold = "1000"
alarm_description = "This metric monitors WAF blocked requests"
alarm_actions = [aws_sns_topic.security_alerts.arn]
dimensions = {
WebACL = aws_wafv2_web_acl.main.name
Region = data.aws_region.current.name
Rule = "ALL"
}
}
# Alarm for rate limit rule triggers
resource "aws_cloudwatch_metric_alarm" "rate_limit_triggers" {
alarm_name = "waf-rate-limit-triggers"
comparison_operator = "GreaterThanThreshold"
evaluation_periods = "1"
metric_name = "BlockedRequests"
namespace = "AWS/WAFV2"
period = "60"
statistic = "Sum"
threshold = "100"
alarm_description = "Rate limiting is being triggered frequently"
alarm_actions = [aws_sns_topic.security_alerts.arn]
dimensions = {
WebACL = aws_wafv2_web_acl.main.name
Region = data.aws_region.current.name
Rule = "rate-limit-rule"
}
}
Best Practices
Security Checklist
WAF Implementation Checklist:
Initial Setup:
☐ Enable AWS Managed Rules Core Rule Set
☐ Configure rate limiting
☐ Set up geo-blocking if needed
☐ Enable logging to S3/CloudWatch
☐ Create CloudWatch alarms
Testing Phase:
☐ Test all rules in Count mode first
☐ Review sampled requests
☐ Analyze false positives
☐ Tune rule sensitivity
☐ Document exceptions
Production:
☐ Switch rules to Block mode
☐ Enable bot control
☐ Configure custom responses
☐ Set up CAPTCHA for suspicious traffic
☐ Integrate with Security Hub
Ongoing:
☐ Weekly review of blocked requests
☐ Monthly rule effectiveness review
☐ Update IP sets as needed
☐ Monitor for new threat patterns
Cost Optimization
Pricing Structure:
Web ACL:
- $5.00 per month per Web ACL
Rules:
- $1.00 per month per rule
Requests:
- $0.60 per million requests
Bot Control:
- COMMON: $10.00 per million requests
- TARGETED: $16.00 per million requests
CAPTCHA:
- $0.40 per 1,000 challenge attempts
Optimization Tips:
- Consolidate rules where possible
- Use rule groups efficiently
- Start with Common bot control level
- Monitor request patterns
- Use count mode for testing
- Review unused rules monthly