AWS WAF in 2026: Rate Limiting, Bot Protection, OWASP Rules, and Terraform
Configure AWS WAF for production security: managed rule groups, custom rate limiting, bot control, IP reputation lists, OWASP Top 10 protection, and complete Terraform setup.
AWS WAF in 2026: Rate Limiting, Bot Protection, OWASP Rules, and Terraform
AWS WAF sits in front of your CloudFront distribution or Application Load Balancer and filters malicious traffic before it reaches your application. Done right, it handles the OWASP Top 10, stops credential stuffing, blocks scrapers, and rate-limits abusive IPs—without you writing a single line of application firewall logic.
Done wrong, it's a bill with opaque metrics, false positives blocking real users, and security gaps because the managed rules weren't tuned for your traffic patterns.
This post covers the production WAF setup we deploy for client applications at Viprasol: managed rule groups, custom rate limiting by IP and by endpoint, bot control, geo-restrictions, and the Terraform that manages it with proper logging.
WAF Architecture Decision: CloudFront vs ALB
| Placement | Use When | Advantage |
|---|---|---|
| CloudFront | Next.js, static sites, global users | Blocks at the edge (cheapest, fastest) |
| ALB | Direct API traffic, ECS/EC2 | Protects backend regardless of frontend |
| Both | Maximum protection | Layer defense |
For most Next.js SaaS products: WAF on CloudFront. For APIs accessed directly (mobile apps, third-party integrations): WAF on ALB as well.
Terraform Module
# modules/waf/variables.tf
variable "name" {
type = string
}
variable "environment" {
type = string
}
variable "scope" {
description = "CLOUDFRONT or REGIONAL (for ALB)"
type = string
default = "CLOUDFRONT"
# Note: CLOUDFRONT WAF must be in us-east-1 regardless of app region
}
variable "cloudfront_distribution_arn" {
description = "ARN of CloudFront distribution to protect"
type = string
default = ""
}
variable "alb_arn" {
description = "ARN of ALB to protect (for REGIONAL scope)"
type = string
default = ""
}
variable "rate_limit_per_ip" {
description = "Max requests per 5 minutes per IP (0 = disabled)"
type = number
default = 2000
}
variable "api_rate_limit" {
description = "Stricter rate limit for /api/ paths"
type = number
default = 200
}
variable "blocked_countries" {
description = "ISO 3166-1 alpha-2 country codes to block"
type = list(string)
default = []
}
variable "allowed_countries" {
description = "If set, only allow traffic from these countries"
type = list(string)
default = []
}
variable "enable_bot_control" {
description = "Enable AWS Bot Control managed rule group (adds cost)"
type = bool
default = true
}
variable "log_group_arn" {
type = string
default = ""
}
variable "sns_alert_arn" {
type = string
default = ""
}
variable "tags" {
type = map(string)
default = {}
}
# modules/waf/main.tf
locals {
full_name = "${var.name}-${var.environment}"
common_tags = merge(var.tags, {
Module = "waf"
Environment = var.environment
ManagedBy = "terraform"
})
}
resource "aws_wafv2_web_acl" "main" {
name = local.full_name
description = "WAF for ${local.full_name}"
scope = var.scope
default_action {
allow {} # Allow by default; rules below block/count specific patterns
}
# ─── Rule 1: AWS Managed – Core Rule Set (OWASP Top 10) ───────────────────
rule {
name = "AWSManagedRulesCommonRuleSet"
priority = 10
override_action {
none {} # Use managed rules' actions (block/count/challenge)
}
statement {
managed_rule_group_statement {
name = "AWSManagedRulesCommonRuleSet"
vendor_name = "AWS"
# Override specific rules to count (not block) during tuning period
rule_action_override {
name = "SizeRestrictions_BODY"
action_to_use {
count {} # Count first, then switch to block after validating
}
}
}
}
visibility_config {
cloudwatch_metrics_enabled = true
metric_name = "${local.full_name}-common-ruleset"
sampled_requests_enabled = true
}
}
# ─── Rule 2: Known Bad Inputs (SQLi, XSS, log4j) ─────────────────────────
rule {
name = "AWSManagedRulesKnownBadInputsRuleSet"
priority = 20
override_action {
none {}
}
statement {
managed_rule_group_statement {
name = "AWSManagedRulesKnownBadInputsRuleSet"
vendor_name = "AWS"
}
}
visibility_config {
cloudwatch_metrics_enabled = true
metric_name = "${local.full_name}-bad-inputs"
sampled_requests_enabled = true
}
}
# ─── Rule 3: SQL Injection Protection ────────────────────────────────────
rule {
name = "AWSManagedRulesSQLiRuleSet"
priority = 30
override_action {
none {}
}
statement {
managed_rule_group_statement {
name = "AWSManagedRulesSQLiRuleSet"
vendor_name = "AWS"
}
}
visibility_config {
cloudwatch_metrics_enabled = true
metric_name = "${local.full_name}-sqli"
sampled_requests_enabled = true
}
}
# ─── Rule 4: IP Reputation List (known malicious IPs) ────────────────────
rule {
name = "AWSManagedRulesAmazonIpReputationList"
priority = 40
override_action {
none {}
}
statement {
managed_rule_group_statement {
name = "AWSManagedRulesAmazonIpReputationList"
vendor_name = "AWS"
}
}
visibility_config {
cloudwatch_metrics_enabled = true
metric_name = "${local.full_name}-ip-reputation"
sampled_requests_enabled = true
}
}
# ─── Rule 5: Bot Control ──────────────────────────────────────────────────
dynamic "rule" {
for_each = var.enable_bot_control ? [1] : []
content {
name = "AWSManagedRulesBotControlRuleSet"
priority = 50
override_action {
none {}
}
statement {
managed_rule_group_statement {
name = "AWSManagedRulesBotControlRuleSet"
vendor_name = "AWS"
managed_rule_group_configs {
aws_managed_rules_bot_control_rule_set {
inspection_level = "COMMON" # TARGETED costs more but blocks more
enable_machine_learning = true
}
}
# Allow legitimate crawlers: Google, Bing, etc.
scope_down_statement {
not_statement {
statement {
byte_match_statement {
field_to_match { uri_path {} }
positional_constraint = "STARTS_WITH"
search_string = "/api/"
text_transformation {
priority = 0
type = "LOWERCASE"
}
}
}
}
}
}
}
visibility_config {
cloudwatch_metrics_enabled = true
metric_name = "${local.full_name}-bot-control"
sampled_requests_enabled = true
}
}
}
# ─── Rule 6: Global Rate Limit (per IP) ──────────────────────────────────
dynamic "rule" {
for_each = var.rate_limit_per_ip > 0 ? [1] : []
content {
name = "RateLimitPerIP"
priority = 60
action {
block {
custom_response {
response_code = 429
response_header {
name = "Retry-After"
value = "300"
}
}
}
}
statement {
rate_based_statement {
limit = var.rate_limit_per_ip
aggregate_key_type = "IP"
}
}
visibility_config {
cloudwatch_metrics_enabled = true
metric_name = "${local.full_name}-rate-limit-ip"
sampled_requests_enabled = true
}
}
}
# ─── Rule 7: Stricter API Rate Limit ─────────────────────────────────────
dynamic "rule" {
for_each = var.api_rate_limit > 0 ? [1] : []
content {
name = "RateLimitAPI"
priority = 70
action {
block {
custom_response {
response_code = 429
response_header {
name = "Retry-After"
value = "300"
}
}
}
}
statement {
rate_based_statement {
limit = var.api_rate_limit
aggregate_key_type = "IP"
# Only apply to /api/ paths
scope_down_statement {
byte_match_statement {
field_to_match { uri_path {} }
positional_constraint = "STARTS_WITH"
search_string = "/api/"
text_transformation {
priority = 0
type = "LOWERCASE"
}
}
}
}
}
visibility_config {
cloudwatch_metrics_enabled = true
metric_name = "${local.full_name}-rate-limit-api"
sampled_requests_enabled = true
}
}
}
# ─── Rule 8: Geo Restriction (block countries) ───────────────────────────
dynamic "rule" {
for_each = length(var.blocked_countries) > 0 ? [1] : []
content {
name = "GeoBlock"
priority = 80
action {
block {}
}
statement {
geo_match_statement {
country_codes = var.blocked_countries
}
}
visibility_config {
cloudwatch_metrics_enabled = true
metric_name = "${local.full_name}-geo-block"
sampled_requests_enabled = true
}
}
}
# ─── Rule 9: Geo Allow-List (only allow specific countries) ──────────────
dynamic "rule" {
for_each = length(var.allowed_countries) > 0 ? [1] : []
content {
name = "GeoAllowList"
priority = 85
action {
block {}
}
statement {
not_statement {
statement {
geo_match_statement {
country_codes = var.allowed_countries
}
}
}
}
visibility_config {
cloudwatch_metrics_enabled = true
metric_name = "${local.full_name}-geo-allowlist"
sampled_requests_enabled = true
}
}
}
visibility_config {
cloudwatch_metrics_enabled = true
metric_name = local.full_name
sampled_requests_enabled = true
}
tags = local.common_tags
}
# ─── Associate with CloudFront ────────────────────────────────────────────────
resource "aws_wafv2_web_acl_association" "cloudfront" {
count = var.cloudfront_distribution_arn != "" ? 1 : 0
resource_arn = var.cloudfront_distribution_arn
web_acl_arn = aws_wafv2_web_acl.main.arn
}
# ─── Associate with ALB ───────────────────────────────────────────────────────
resource "aws_wafv2_web_acl_association" "alb" {
count = var.alb_arn != "" ? 1 : 0
resource_arn = var.alb_arn
web_acl_arn = aws_wafv2_web_acl.main.arn
}
# ─── WAF Logging ──────────────────────────────────────────────────────────────
resource "aws_cloudwatch_log_group" "waf" {
# WAF log group name MUST start with aws-waf-logs-
name = "aws-waf-logs-${local.full_name}"
retention_in_days = 30
tags = local.common_tags
}
resource "aws_wafv2_web_acl_logging_configuration" "main" {
log_destination_configs = [aws_cloudwatch_log_group.waf.arn]
resource_arn = aws_wafv2_web_acl.main.arn
# Redact sensitive headers from logs
redacted_fields {
single_header { name = "authorization" }
}
redacted_fields {
single_header { name = "cookie" }
}
}
☁️ Is Your Cloud Costing Too Much?
Most teams overspend 30–40% on cloud — wrong instance types, no reserved pricing, bloated storage. We audit, right-size, and automate your infrastructure.
- AWS, GCP, Azure certified engineers
- Infrastructure as Code (Terraform, CDK)
- Docker, Kubernetes, GitHub Actions CI/CD
- Typical audit recovers $500–$3,000/month in savings
Custom IP Block List
# modules/waf/ip-sets.tf
# Manual IP block list (update via Terraform or AWS CLI)
resource "aws_wafv2_ip_set" "blocked_ips" {
name = "${local.full_name}-blocked-ips"
description = "Manually blocked IP addresses"
scope = var.scope
ip_address_version = "IPV4"
addresses = var.blocked_ip_cidrs # ["1.2.3.4/32", "5.6.7.8/32"]
tags = local.common_tags
}
# Add a rule to the ACL referencing the IP set
# (Add inside the aws_wafv2_web_acl resource at priority 5 — before everything else)
rule {
name = "BlockListedIPs"
priority = 5
action {
block {}
}
statement {
ip_set_reference_statement {
arn = aws_wafv2_ip_set.blocked_ips.arn
}
}
visibility_config {
cloudwatch_metrics_enabled = true
metric_name = "${local.full_name}-blocked-ips"
sampled_requests_enabled = true
}
}
CloudWatch Alarms for WAF
resource "aws_cloudwatch_metric_alarm" "blocked_requests_spike" {
count = var.sns_alert_arn != "" ? 1 : 0
alarm_name = "${local.full_name}-waf-blocks-spike"
alarm_description = "Spike in WAF blocked requests — possible attack"
comparison_operator = "GreaterThanThreshold"
evaluation_periods = 2
metric_name = "BlockedRequests"
namespace = "AWS/WAFV2"
period = 300
statistic = "Sum"
threshold = 100
dimensions = {
WebACL = aws_wafv2_web_acl.main.name
Region = data.aws_region.current.name
Rule = "ALL"
}
alarm_actions = [var.sns_alert_arn]
tags = local.common_tags
}
data "aws_region" "current" {}
⚙️ DevOps Done Right — Zero Downtime, Full Automation
Ship faster without breaking things. We build CI/CD pipelines, monitoring stacks, and auto-scaling infrastructure that your team can actually maintain.
- Staging + production environments with feature flags
- Automated security scanning in the pipeline
- Uptime monitoring + alerting + runbook automation
- On-call support handover docs included
Usage Example
# environments/prod/waf.tf
module "waf" {
source = "../../modules/waf"
# IMPORTANT: CloudFront WAF must be in us-east-1
providers = {
aws = aws.us_east_1
}
name = "myapp"
environment = "prod"
scope = "CLOUDFRONT"
cloudfront_distribution_arn = module.cdn.distribution_arn
rate_limit_per_ip = 3000 # ~10 req/sec per IP
api_rate_limit = 300 # ~1 req/sec on /api/ paths
enable_bot_control = true
# Block countries with high fraud rates (tune for your product)
blocked_countries = []
sns_alert_arn = aws_sns_topic.security_alerts.arn
tags = { Project = "myapp", CostCenter = "security" }
}
Tuning Strategy
Week 1: Count mode only
Start with count {} actions on all managed rules instead of block {}. Review CloudWatch logs to identify false positives before blocking real users.
# Query WAF logs to see what would have been blocked
aws logs start-query \
--log-group-name "aws-waf-logs-myapp-prod" \
--start-time $(date -d '1 hour ago' +%s) \
--end-time $(date +%s) \
--query-string 'fields @timestamp, httpRequest.uri, terminatingRuleId, action
| filter action = "BLOCK" or action = "COUNT"
| stats count() by terminatingRuleId
| sort count desc'
Week 2: Switch to block, add exceptions
# Add rule exception for legitimate large payloads (e.g., file uploads)
rule_action_override {
name = "SizeRestrictions_BODY"
action_to_use {
allow {} # Your upload endpoint sends large bodies
}
}
Cost Estimates (2026, us-east-1)
| Component | Monthly Cost |
|---|---|
| WAF Web ACL | $5.00/month |
| Per rule (managed groups count as 1 rule each) | $1.00/rule/month |
| Request processing | $0.60/million requests |
| Bot Control (COMMON) | $10.00/million requests |
| Bot Control (TARGETED) | $40.00/million requests |
| Typical SaaS (5M req/month, no bot control) | ~$20–$30/month |
| Typical SaaS (5M req/month, bot control COMMON) | ~$70–$80/month |
Engineering cost estimates:
| Scope | Timeline | Engineering Cost |
|---|---|---|
| Basic WAF + OWASP rules | 0.5–1 day | $400–$800 |
| Rate limiting + IP sets | 0.5–1 day | $400–$800 |
| Bot control + geo rules | 0.5–1 day | $400–$800 |
| Full Terraform + logging + alarms | 1–2 days | $800–$1,600 |
| Tuning period (2 weeks monitoring) | Ongoing | $1,000–$2,000 |
See Also
- AWS CloudFront Edge — CDN that WAF protects
- AWS CloudWatch Observability — Monitoring WAF metrics
- Terraform State Management — Managing WAF state (remember: CloudFront WAF is us-east-1 only)
- SaaS Audit Logging — Application-level security event logging
Working With Viprasol
We implement AWS WAF configurations for SaaS products handling compliance requirements, DDoS mitigation, and bot protection. Our security team has tuned WAF rules for products serving millions of requests per day across diverse global traffic patterns.
What we deliver:
- Complete Terraform WAF module with managed rules and custom rate limits
- Tuning period with CloudWatch log analysis to eliminate false positives
- Bot control configuration with legitimate crawler allowlists
- Security incident response runbooks
- Ongoing WAF rule monitoring and updates
See our cloud infrastructure services or contact us to discuss your security requirements.
About the Author
Viprasol Tech Team
Custom Software Development Specialists
The Viprasol Tech team specialises in algorithmic trading software, AI agent systems, and SaaS development. With 100+ projects delivered across MT4/MT5 EAs, fintech platforms, and production AI systems, the team brings deep technical experience to every engagement. Based in India, serving clients globally.
Need DevOps & Cloud Expertise?
Scale your infrastructure with confidence. AWS, GCP, Azure certified team.
Free consultation • No commitment • Response within 24 hours
Making sense of your data at scale?
Viprasol builds end-to-end big data analytics solutions — ETL pipelines, data warehouses on Snowflake or BigQuery, and self-service BI dashboards. One reliable source of truth for your entire organisation.