2026-01-27 17:35:45 -05:00
---
name : waf-setup
description : Deploy and tune Web Application Firewalls. Configure rules for OWASP Top 10 protection. Use when protecting web applications from common attacks.
license : MIT
metadata :
author : devops-skills
version : "1.0"
---
# WAF Setup
Protect web applications with Web Application Firewalls.
2026-03-24 18:02:50 -04:00
## When to Use This Skill
Use this skill when:
- Deploying a public-facing web application that needs attack protection
- Meeting compliance requirements (PCI-DSS, SOC2) for web application security
- Blocking OWASP Top 10 attack categories (SQLi, XSS, CSRF, etc.)
- Protecting APIs from abuse, injection, and rate-based attacks
- Adding a virtual patching layer while application code is being fixed
## Prerequisites
- Web application behind a load balancer or reverse proxy
- AWS account for AWS WAF, or Cloudflare account for Cloudflare WAF
- Nginx with ModSecurity module compiled for self-hosted WAF
- Access to application logs to tune rules and identify false positives
- Understanding of HTTP request/response structure
2026-01-27 17:35:45 -05:00
## AWS WAF
2026-03-24 18:02:50 -04:00
### Create Web ACL with Managed Rules
2026-01-27 17:35:45 -05:00
```bash
2026-03-24 18:02:50 -04:00
# Create Web ACL with AWS managed rules
2026-01-27 17:35:45 -05:00
aws wafv2 create-web-acl \
2026-03-24 18:02:50 -04:00
--name production-waf \
2026-01-27 17:35:45 -05:00
--scope REGIONAL \
--default-action Allow ={} \
2026-03-24 18:02:50 -04:00
--visibility-config SampledRequestsEnabled = true,CloudWatchMetricsEnabled= true,MetricName= production-waf \
--rules file://waf-rules.json
2026-01-27 17:35:45 -05:00
```
2026-03-24 18:02:50 -04:00
### AWS WAF Rules Configuration
2026-01-27 17:35:45 -05:00
2026-03-24 18:02:50 -04:00
```json
[
{
"Name" : "AWSManagedRulesCommonRuleSet" ,
"Priority" : 1 ,
"Statement" : {
"ManagedRuleGroupStatement" : {
"VendorName" : "AWS" ,
"Name" : "AWSManagedRulesCommonRuleSet" ,
"ExcludedRules" : []
}
},
"OverrideAction" : { "None" : {} },
"VisibilityConfig" : {
"SampledRequestsEnabled" : true ,
"CloudWatchMetricsEnabled" : true ,
"MetricName" : "AWSCommonRules"
}
},
{
"Name" : "AWSManagedRulesSQLiRuleSet" ,
"Priority" : 2 ,
"Statement" : {
"ManagedRuleGroupStatement" : {
"VendorName" : "AWS" ,
"Name" : "AWSManagedRulesSQLiRuleSet"
}
},
"OverrideAction" : { "None" : {} },
"VisibilityConfig" : {
"SampledRequestsEnabled" : true ,
"CloudWatchMetricsEnabled" : true ,
"MetricName" : "AWSSQLiRules"
}
},
{
"Name" : "AWSManagedRulesKnownBadInputsRuleSet" ,
"Priority" : 3 ,
"Statement" : {
"ManagedRuleGroupStatement" : {
"VendorName" : "AWS" ,
"Name" : "AWSManagedRulesKnownBadInputsRuleSet"
}
},
"OverrideAction" : { "None" : {} },
"VisibilityConfig" : {
"SampledRequestsEnabled" : true ,
"CloudWatchMetricsEnabled" : true ,
"MetricName" : "AWSBadInputRules"
}
},
{
"Name" : "RateLimitRule" ,
"Priority" : 4 ,
"Statement" : {
"RateBasedStatement" : {
"Limit" : 2000 ,
"AggregateKeyType" : "IP"
}
},
"Action" : { "Block" : {} },
"VisibilityConfig" : {
"SampledRequestsEnabled" : true ,
"CloudWatchMetricsEnabled" : true ,
"MetricName" : "RateLimit"
}
},
{
"Name" : "GeoBlockRule" ,
"Priority" : 5 ,
"Statement" : {
"GeoMatchStatement" : {
"CountryCodes" : [ "KP" , "IR" , "SY" ]
}
},
"Action" : { "Block" : {} },
"VisibilityConfig" : {
"SampledRequestsEnabled" : true ,
"CloudWatchMetricsEnabled" : true ,
"MetricName" : "GeoBlock"
}
},
{
"Name" : "BlockBadUserAgents" ,
"Priority" : 6 ,
"Statement" : {
"ByteMatchStatement" : {
"SearchString" : "sqlmap" ,
"FieldToMatch" : { "SingleHeader" : { "Name" : "user-agent" } },
"TextTransformations" : [{ "Priority" : 0 , "Type" : "LOWERCASE" }],
"PositionalConstraint" : "CONTAINS"
}
},
"Action" : { "Block" : {} },
"VisibilityConfig" : {
"SampledRequestsEnabled" : true ,
"CloudWatchMetricsEnabled" : true ,
"MetricName" : "BadUserAgent"
}
}
]
2026-01-27 17:35:45 -05:00
```
2026-03-24 18:02:50 -04:00
### Associate WAF with ALB
2026-01-27 17:35:45 -05:00
```bash
2026-03-24 18:02:50 -04:00
# Associate with Application Load Balancer
aws wafv2 associate-web-acl \
--web-acl-arn arn:aws:wafv2:us-east-1:123456789:regional/webacl/production-waf/abc123 \
--resource-arn arn:aws:elasticloadbalancing:us-east-1:123456789:loadbalancer/app/my-alb/abc123
# Associate with API Gateway
aws wafv2 associate-web-acl \
--web-acl-arn arn:aws:wafv2:us-east-1:123456789:regional/webacl/production-waf/abc123 \
--resource-arn arn:aws:apigateway:us-east-1::/restapis/abc123/stages/prod
```
### AWS WAF Terraform
```hcl
resource "aws_wafv2_web_acl" "main" {
name = "production-waf"
scope = "REGIONAL"
description = "Production WAF with OWASP protections"
default_action {
allow {}
}
rule {
name = "AWSManagedRulesCommonRuleSet"
priority = 1
override_action { none {} }
statement {
managed_rule_group_statement {
name = "AWSManagedRulesCommonRuleSet"
vendor_name = "AWS"
rule_action_override {
name = "SizeRestrictions_BODY"
action_to_use { count {} }
}
}
}
visibility_config {
cloudwatch_metrics_enabled = true
metric_name = "AWSCommonRules"
sampled_requests_enabled = true
}
}
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
}
}
visibility_config {
cloudwatch_metrics_enabled = true
metric_name = "production-waf"
sampled_requests_enabled = true
}
}
resource "aws_wafv2_web_acl_association" "alb" {
resource_arn = aws_lb . main . arn
web_acl_arn = aws_wafv2_web_acl . main . arn
}
2026-01-27 17:35:45 -05:00
```
## Cloudflare WAF
2026-03-24 18:02:50 -04:00
### API Configuration
2026-01-27 17:35:45 -05:00
```bash
2026-03-24 18:02:50 -04:00
# List available WAF rulesets
curl -s "https://api.cloudflare.com/client/v4/zones/ ${ ZONE_ID } /rulesets" \
-H "Authorization: Bearer ${ CF_TOKEN } " | jq '.result[] | {id, name, phase}'
# Create a custom WAF rule
curl -X POST "https://api.cloudflare.com/client/v4/zones/ ${ ZONE_ID } /rulesets" \
-H "Authorization: Bearer ${ CF_TOKEN } " \
-H "Content-Type: application/json" \
-d '{
"name": "Custom WAF Rules",
"kind": "zone",
"phase": "http_request_firewall_custom",
"rules": [
{
"action": "block",
"expression": "(http.request.uri.query contains \"union select\" or http.request.uri.query contains \"1=1\")",
"description": "Block SQL injection patterns in query string"
},
{
"action": "block",
"expression": "(http.request.uri.path contains \"..%2f\" or http.request.uri.path contains \"..%5c\")",
"description": "Block path traversal attempts"
},
{
"action": "challenge",
"expression": "(cf.threat_score gt 30)",
"description": "Challenge high threat score visitors"
},
{
"action": "block",
"expression": "(http.request.headers[\"user-agent\"] contains \"sqlmap\" or http.request.headers[\"user-agent\"] contains \"nikto\")",
"description": "Block known attack tools"
}
]
}'
# Configure rate limiting
curl -X POST "https://api.cloudflare.com/client/v4/zones/ ${ ZONE_ID } /rulesets" \
-H "Authorization: Bearer ${ CF_TOKEN } " \
-H "Content-Type: application/json" \
-d '{
"name": "Rate Limiting",
"kind": "zone",
"phase": "http_ratelimit",
"rules": [
{
"action": "block",
"ratelimit": {
"characteristics": ["ip.src"],
"period": 60,
"requests_per_period": 100,
"mitigation_timeout": 600
},
"expression": "(http.request.uri.path matches \"^/api/\")",
"description": "Rate limit API endpoints"
}
]
}'
2026-01-27 17:35:45 -05:00
```
2026-03-24 18:02:50 -04:00
### Cloudflare Terraform
2026-01-27 17:35:45 -05:00
2026-03-24 18:02:50 -04:00
```hcl
resource "cloudflare_ruleset" "waf_custom" {
zone_id = var . zone_id
name = "Custom WAF Rules"
kind = "zone"
phase = "http_request_firewall_custom"
rules {
action = "block"
expression = "(http.request.uri.query contains \"union select\")"
description = "Block SQL injection in query string"
}
rules {
action = "managed_challenge"
expression = "(cf.threat_score gt 30)"
description = "Challenge suspicious visitors"
}
}
2026-01-27 17:35:45 -05:00
```
2026-03-24 18:02:50 -04:00
## ModSecurity with Nginx
### Installation
```bash
# Install ModSecurity for Nginx (Ubuntu)
apt install -y libmodsecurity3 libmodsecurity-dev nginx libnginx-mod-http-modsecurity
# Or compile from source
git clone https://github.com/SpiderLabs/ModSecurity /opt/modsecurity
cd /opt/modsecurity
git submodule init && git submodule update
./build.sh && ./configure && make && make install
```
### Nginx Configuration
```nginx
# /etc/nginx/nginx.conf
load_module modules/ngx_http_modsecurity_module.so ;
http {
modsecurity on ;
modsecurity_rules_file /etc/nginx/modsec/main.conf ;
server {
listen 443 ssl http2 ;
server_name example.com ;
# ModSecurity can also be enabled per-location
location /api/ {
modsecurity on ;
modsecurity_rules_file /etc/nginx/modsec/api-rules.conf ;
proxy_pass http://backend ;
}
}
}
```
### ModSecurity Main Configuration
```bash
# /etc/nginx/modsec/main.conf
Include /etc/nginx/modsec/modsecurity.conf
# Set to DetectionOnly first, switch to On after tuning
SecRuleEngine On
# Request body handling
SecRequestBodyAccess On
SecRequestBodyLimit 13107200
SecRequestBodyNoFilesLimit 131072
# Response body handling
SecResponseBodyAccess On
SecResponseBodyMimeType text/plain text/html text/xml application/json
# Logging
SecAuditEngine RelevantOnly
SecAuditLogRelevantStatus "^(?:5|4(?!04))"
SecAuditLogParts ABIJDEFHZ
SecAuditLogType Serial
SecAuditLog /var/log/modsec/modsec_audit.log
# Include OWASP Core Rule Set
Include /etc/nginx/modsec/crs/crs-setup.conf
Include /etc/nginx/modsec/crs/rules/*.conf
```
### OWASP Core Rule Set Setup
```bash
# Download and install OWASP CRS
cd /etc/nginx/modsec
git clone https://github.com/coreruleset/coreruleset crs
cp crs/crs-setup.conf.example crs/crs-setup.conf
# Customize CRS settings
cat >> crs/crs-setup.conf << 'EOF'
# Set paranoia level (1-4, higher = more strict)
SecAction "id:900000, phase:1, pass, t:none, nolog, setvar:tx.paranoia_level=2"
# Set anomaly score thresholds
SecAction "id:900110, phase:1, pass, t:none, nolog, \
setvar:tx.inbound_anomaly_score_threshold=5, \
setvar:tx.outbound_anomaly_score_threshold=4"
# Exclude known false positives
SecRule REQUEST_URI "@beginsWith /api/upload" \
"id:1001,phase:1,pass,nolog,ctl:ruleRemoveById=920420"
EOF
# Create rule exclusions file
cat > /etc/nginx/modsec/crs/RESPONSE-999-EXCLUSION-RULES-AFTER-CRS.conf << 'EOF'
# Exclude rules that cause false positives on specific paths
SecRule REQUEST_URI "@beginsWith /api/webhook" \
"id:1000001,phase:1,pass,nolog,ctl:ruleRemoveTargetById=942100;ARGS:payload"
# Exclude rules for specific parameters
SecRule ARGS_NAMES "^content$" \
"id:1000002,phase:1,pass,nolog,ctl:ruleRemoveTargetById=941100;ARGS:content"
EOF
```
### Custom ModSecurity Rules
```bash
# /etc/nginx/modsec/custom-rules.conf
# Block requests with known attack tool user agents
SecRule REQUEST_HEADERS:User-Agent "@pm sqlmap nikto nmap masscan dirbuster" \
"id:10001,phase:1,deny,status:403,log,msg:'Blocked attack tool'"
# Block requests to sensitive paths
SecRule REQUEST_URI "@rx /(\.git|\.env|\.svn|wp-admin|phpmyadmin|adminer)" \
"id:10002,phase:1,deny,status:404,log,msg:'Blocked sensitive path access'"
# Rate limit by IP (10 requests/second)
SecRule IP:REQUEST_RATE "@gt 10" \
"id:10003,phase:1,deny,status:429,log,msg:'Rate limit exceeded',\
setvar:IP.request_rate=+1,expirevar:IP.request_rate=1"
# Block oversized cookies (potential overflow attack)
SecRule REQUEST_HEADERS:Cookie "@gt 4096" \
"id:10004,phase:1,deny,status:400,log,msg:'Oversized cookie header'"
# Virtual patch: block specific CVE exploit pattern
SecRule ARGS:filename "@contains ../../" \
"id:10005,phase:2,deny,status:403,log,msg:'Path traversal blocked (virtual patch CVE-XXXX-XXXX)'"
# Require Content-Type on POST requests
SecRule REQUEST_METHOD "@streq POST" \
"id:10006,phase:1,chain,deny,status:400,log,msg:'POST without Content-Type'"
SecRule & REQUEST_HEADERS:Content-Type "@eq 0" ""
```
## WAF Tuning Workflow
```bash
#!/bin/bash
# waf-tune.sh - Analyze WAF logs for false positives
AUDIT_LOG = "/var/log/modsec/modsec_audit.log"
TIMEFRAME = "24h"
echo "=== WAF Tuning Report ==="
echo "Analyzing last ${ TIMEFRAME } of audit logs"
echo ""
# Top blocked rules
echo "--- Top 10 triggered rules ---"
grep -oP 'id "\K[0-9]+' " $AUDIT_LOG " | sort | uniq -c | sort -rn | head -10
echo ""
echo "--- Top blocked URIs ---"
grep -oP 'REQUEST_URI: \K[^\s]+' " $AUDIT_LOG " | sort | uniq -c | sort -rn | head -10
echo ""
echo "--- Top blocked IPs ---"
grep -oP 'client \K[0-9.]+' " $AUDIT_LOG " | sort | uniq -c | sort -rn | head -10
echo ""
echo "--- False positive candidates (high-frequency blocks on common paths) ---"
grep -oP 'id "\K[0-9]+' " $AUDIT_LOG " | sort | uniq -c | sort -rn | \
while read count rule_id; do
if [ " $count " -gt 100 ] ; then
echo " Rule $rule_id triggered $count times - review for false positive"
fi
done
```
## Troubleshooting
| Problem | Cause | Solution |
|---------|-------|----------|
| Legitimate requests blocked | False positives from CRS rules | Set `SecRuleEngine DetectionOnly` first; review audit log; add exclusions |
| WAF not blocking attacks | Rules in detection-only mode | Switch `SecRuleEngine On` after tuning period |
| High latency with WAF enabled | Response body inspection overhead | Disable `SecResponseBodyAccess` if not needed; reduce `paranoia_level` |
| AWS WAF rules not matching | Rule priority order wrong | Lower priority number = evaluated first; reorder rules |
| ModSecurity crashes nginx | Memory exhaustion on large requests | Increase `SecRequestBodyLimit` ; adjust `SecPcreMatchLimit` |
| Cloudflare WAF blocks API calls | Expression too broad | Narrow expression with path or method conditions |
| CRS update breaks application | New rules trigger on existing traffic | Pin CRS version; test updates in staging first |
2026-01-27 17:35:45 -05:00
## Best Practices
2026-03-24 18:02:50 -04:00
- Start in detection/log mode, switch to blocking after tuning
- Tune rules for at least 1-2 weeks before enforcement
- Monitor blocked requests daily during tuning phase
- Update managed rule sets and CRS regularly
- Create custom rules for application-specific attack patterns
- Use virtual patching to protect against known CVEs while code is being fixed
- Set appropriate rate limits per endpoint
- Maintain exclusion rules documentation with justifications
- Test WAF rules with known attack payloads before deploying
- Keep audit logs for at least 90 days for forensic analysis
2026-01-27 17:35:45 -05:00
## Related Skills
- [dast-scanning ](../../scanning/dast-scanning/ ) - Web security testing
- [ssl-tls-management ](../ssl-tls-management/ ) - HTTPS configuration
2026-03-24 18:02:50 -04:00
- [firewall-config ](../firewall-config/ ) - Network-level firewalling