Rendu depuis le dépôt source en conservant titres, exemples, code, tableaux, liens et images.
WAF Setup
Protect web applications with Web Application Firewalls.
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
AWS WAF
Create Web ACL with Managed Rules
# Create Web ACL with AWS managed rules
aws wafv2 create-web-acl \
--name production-waf \
--scope REGIONAL \
--default-action Allow={} \
--visibility-config SampledRequestsEnabled=true,CloudWatchMetricsEnabled=true,MetricName=production-waf \
--rules file://waf-rules.jsonAWS WAF Rules Configuration
[
{
"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"
}
}
]Associate WAF with ALB
# 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/prodAWS WAF Terraform
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
}Cloudflare WAF
API Configuration
# 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"
}
]
}'Cloudflare Terraform
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"
}
}ModSecurity with Nginx
Installation
# 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 installNginx Configuration
# /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
# /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/*.confOWASP Core Rule Set Setup
# 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"
EOFCustom ModSecurity Rules
# /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
#!/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
doneTroubleshooting
| 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 |
Best Practices
- 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
Related Skills
- dast-scanning - Web security testing
- ssl-tls-management - HTTPS configuration
- firewall-config - Network-level firewalling

