waf-setup
Deploy and tune Web Application Firewalls. Configure rules for OWASP Top 10 protection. Use when protecting web applications from common attacks.WAF Setup
Protect web applications with Web Application Firewalls.
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
Before mutation, verify the owned account/zone/resource, existing rules and rollback plan. Save current configuration; creating duplicate phase entrypoints or applying Terraform can replace rules. Test the actual application in staging with representative allowed and disallowed requests, then promote a narrowly scoped blocking policy. The blocking examples below are desired enforcement states, not a recommendation to enable all rules immediately. Review sampled-request/log retention and redact tokens, cookies and bodies. WAFs do not replace application authorization, CSRF defenses, parameterized queries or dependency fixes.
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.json
Save the following reviewed JSON as waf-rules.json before creation. Inspect managed-rule versions and start with Count overrides and scoped test targets; tune and explicitly remove overrides only after evidence. AWS CLI byte-blob encoding depends on its binary-format setting: verify SearchString encoding for your CLI, rather than assuming the inline JSON is portable.
AWS 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"]
}
},
"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:123456789012:regional/webacl/production-waf/abc123 \
--resource-arn arn:aws:elasticloadbalancing:us-east-1:123456789012:loadbalancer/app/my-alb/abc123
# Associate with API Gateway
aws wafv2 associate-web-acl \
--web-acl-arn arn:aws:wafv2:us-east-1:123456789012:regional/webacl/production-waf/abc123 \
--resource-arn arn:aws:apigateway:us-east-1::/restapis/abc123/stages/prod
AWS 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
Use an approved authenticated connector when available. For curl examples, CF_CURL_CONFIG refers to a user-provisioned private untracked config file (mode 0600) containing the authorization header; never pass the token in argv or print the file. Verify zone and token scope. Fetch the existing phase entrypoint/ruleset ID before any create/update, and merge intended changes without dropping other rules. The illustrative Cloudflare Terraform fragment uses provider v4-style blocks; pin and validate the actual provider schema before use.
# List available WAF rulesets
curl --fail-with-body --max-time 30 -sS "https://api.cloudflare.com/client/v4/zones/${ZONE_ID}/rulesets" \
--config "$CF_CURL_CONFIG" | jq '.result[] | {id, name, phase}'
# Create a custom WAF rule
curl --fail-with-body --max-time 30 -X POST "https://api.cloudflare.com/client/v4/zones/${ZONE_ID}/rulesets" \
--config "$CF_CURL_CONFIG" \
-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": "managed_challenge",
"expression": "(http.request.uri.path eq \"/login\")",
"description": "Illustrative login challenge; enable only after application testing"
},
{
"action": "block",
"expression": "(lower(http.user_agent) contains \"sqlmap\" or lower(http.user_agent) contains \"nikto\")",
"description": "Block known attack tools"
}
]
}'
# Configure rate limiting
curl --fail-with-body --max-time 30 -X POST "https://api.cloudflare.com/client/v4/zones/${ZONE_ID}/rulesets" \
--config "$CF_CURL_CONFIG" \
-H "Content-Type: application/json" \
-d '{
"name": "Rate Limiting",
"kind": "zone",
"phase": "http_ratelimit",
"rules": [
{
"action": "block",
"ratelimit": {
"characteristics": ["cf.colo.id", "ip.src"],
"period": 60,
"requests_per_period": 100,
"mitigation_timeout": 600
},
"expression": "(starts_with(http.request.uri.path, \"/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 = "(http.request.uri.path eq \"/login\")"
description = "Illustrative login challenge; do not use on non-browser API clients"
}
}
Cloudflare cf.threat_score now always returns zero; do not use old score thresholds as protection. The login challenge above is an explicit path example, not a replacement threat score. Query substring and User-Agent rules are evadable and can block legitimate requests; use managed rules and application fixes. Rate-limit options and periods depend on the plan; confirm them before sending. Geo blocks require business authorization and are not proof of attacker origin.
Contents
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
Limitations
- Apply guidance only within authorized scope; test destructive steps in non-production first.
- Docs-only import: upstream scripts and templates not bundled.
Example
# Read-only first: inventory before any active step.
which <tool> && <tool> --help | head -n 20
Adapted from BagelHole/DevOps-Security-Agent-Skills (MIT); frontmatter, When to Use/Limitations, and safety boundaries added for upstream compliance. Docs-only import: helper scripts and templates not bundled.