Deploy and tune web application firewalls for production protection. Covers WAF rule sets, false positive management, rate limiting, bot detection, custom rules, and the patterns that protect web applications without blocking legitimate traffic.
A Web Application Firewall (WAF) is a security layer that inspects and filters HTTP traffic between a web application and the internet. It protects against common web attacks—SQL injection, cross-site scripting, file inclusion, and more—by applying rules to incoming requests and outgoing responses. WAF engineering matters when the application is exposed to untrusted clients, especially at scale: serving tens of thousands of concurrent requests, handling API endpoints, or processing sensitive user data. It is essential during deployment, incident response, and when enforcing security policies across microservices.
Rules define the behavior of the WAF. They are evaluated in sequence, each rule matching a subset of traffic and applying actions such as blocking, logging, or transforming requests. Rules can be authored in a declarative format, often using JSON or YAML, and deployed via version-controlled configuration files.
A typical rule specifies:
block, log, redirect, set-header, add-cookie, rewrite-path.Example rule configuration in rules.yaml:
- id: 100
name: "Block SQL Injection"
priority: 10
conditions:
- type: "header"
key: "Content-Type"
value: "application/json"
- type: "body"
pattern: "(?i)\\b(SELECT|INSERT|UPDATE|DELETE|FROM|WHERE|JOIN|UNION)\\b"
- type: "path"
pattern: "^/api/v[1-9]/"
actions:
- type: "block"
code: 403
message: "SQL Injection detected"
- type: "log"
level: "warning"
message: "SQLi: {{request.id}}"
- type: "add-header"
key: "X-WAF-Blocked"
value: "SQLi"
The WAF evaluates rules in order. A request with Content-Type: application/json and a body containing SELECT * FROM users matches this rule, triggers the block, logs a warning, and adds a header. If a later rule applies the same action, the effects accumulate.
body and path are case-sensitive unless explicitly specified. A rule using pattern: "user" fails on User or USER.key: "Content-Type" matches exactly Content-Type, but not content-type or CONTENT-TYPE. Use match-case: false or normalize headers before evaluation.conditions must match (AND logic). To implement OR, use multiple rules with the same priority or nest conditions.Rules are versioned and deployed via CI/CD pipelines. A change to rules.yaml triggers a build that validates syntax and generates a WAF configuration artifact.
$ git add rules.yaml
$ git commit -m "Add rate-limiting rule for /login"
$ git push origin main
The pipeline runs:
- name: Validate WAF rules
run: |
python -c "
import yaml
with open('rules.yaml') as f:
config = yaml.safe_load(f)
for rule in config:
assert 'id' in rule, 'Every rule must have an ID'
assert 'priority' in rule, 'Priority must be defined'
assert 'conditions' in rule and len(rule['conditions']) > 0, 'At least one condition required'
print('All rules valid')
"
- name: Deploy WAF config
run: |
scp rules.yaml user@waf-prod:/etc/waf/rules.yaml
systemctl reload waf
Failure modes:
id in rules.yaml causes the WAF to reject the entire configuration.priority: 10 but no priority in another rule leads to unpredictable evaluation order.type: "body" as type: "body-content" results in a rule that never matches.The WAF inspects every HTTP request and response. It parses headers, body, and query parameters, and can apply transformations before forwarding the request to the backend.
The WAF processes requests in this order:
Example request:
GET /api/v1/users?role=admin&limit=10 HTTP/1.1
Host: api.example.com
Content-Type: application/json
Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...
User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/114.0.0.0 Safari/537.36
{
"name": "Alice",
"email": "[email protected]"
}
The WAF can:
role=admin, limit=10) and match them against rules.Authorization header and validate JWT tokens.After the backend responds, the WAF inspects the response and applies post-processing rules.
Example response:
HTTP/1.1 200 OK
Content-Type: application/json
X-Response-Time: 124ms
{
"data": [
{ "id": 1, "name": "Alice", "role": "admin" }
],
"pagination": { "next": "/api/v1/users?cursor=1" }
}
The WAF can:
Content-Type and enforce application/json with specific schema.X-Content-Type-Options: nosniff, X-Frame-Options: DENY.gzip or br if not already sent.Cache-Control headers based on endpoint or response content.Content-Type: application/json but invalid JSON fails to parse, leading to rule misses.Accept-Encoding and accept-encoding as different headers unless normalized.Location headers with rewrite-path fails if the path is not absolute or contains variables not in the request.A rule detects SQL injection by scanning the request body or URL parameters for keywords.
- id: 200
name: "Detect SQL Injection"
priority: 5
conditions:
- type: "body"
pattern: "(?i)\\b(SELECT|INSERT|UPDATE|DELETE|FROM|WHERE|JOIN|UNION|CREATE|ALTER|DROP|INDEX|WITH|AS|ORDER|BY|GROUP|HAVING|LIMIT|OFFSET|EXISTS|IN|LIKE|IS|NULL|NOT)\\b"
- type: "query"
key: "q"
pattern: "(?i)\\b(SELECT|INSERT|UPDATE|DELETE|FROM|WHERE|JOIN|UNION)\\b"
actions:
- type: "block"
code: 403
message: "SQL Injection detected in request"
- type: "log"
level: "info"
message: "SQLi: {{request.query.q}}"
This rule blocks requests with q=SELECT%20*%20FROM%20users and logs the query string.
The WAF inspects input for JavaScript, HTML, or event handlers.
- id: 300
name: "Block XSS Payloads"
priority: 15
conditions:
- type: "body"
pattern: "<(script|iframe|object|embed|input|img|link|style|div|a|button|form|select|option|textarea|nav|header|footer|section|article|aside|h[1-6])[^>]*>(.*?)(<\\1[^>]*>)"
- type: "query"
key: "xss"
value: "1"
actions:
- type: "block"
code: 403
message: "Cross-Site Scripting detected"
- type: "add-header"
key: "X-XSS-Protection"
value: "1; mode=block"
The WAF tracks request frequency per source (IP, API key, user ID).
- id: 400
name: "Rate Limit: API v1"
priority: 20
conditions:
- type: "path"
pattern: "^/api/v1/"
actions:
- type: "rate-limit"
window: "1m"
limit: 100
action: "block"
code: 429
message: "Rate limit exceeded"
header: "X-RateLimit-Limit"
header-value: "100"
header: "X-RateLimit-Remaining"
header-value: "{{remaining}}"
The WAF stores counters in memory or a database. On hit, it returns 429 Too Many Requests with headers:
HTTP/1.1 429 Too Many Requests
X-RateLimit-Limit: 100
X-RateLimit-Remaining: 97
Retry-After: 60
Content-Type: application/json
{
"error": "Rate limit exceeded",
"details": "Request limit of 100 per minute exceeded"
}
The WAF validates incoming headers and body structure.
- id: 500
name: "Validate JWT in Authorization Header"
priority: 30
conditions:
- type: "header"
key: "Authorization"
pattern: "^Bearer\\s+([a-zA-Z0-9\\-_]+\\.[a-zA-Z0-9\\-_]+\\.[a-zA-Z0-9\\-_]+)$"
- type: "header"
key: "Content-Type"
value: "application/json"
actions:
- type: "validate-jwt"
key: "Authorization"
algorithm: "HS256"
issuer: "https://auth.example.com"
audience: "api.example.com"
expected-claims:
- name: "exp"
type: "number"
- name: "sub"
type: "string"
error-action:
- type: "block"
code: 401
message: "Invalid or expired JWT"
- type: "log"
level: "error"
message: "JWT validation failed: {{jwt.error}}"
The WAF extracts the JWT from the Authorization header, validates signature, expiration, issuer, and audience. If validation fails, it blocks the request and logs the error.
priority: 10 but a later rule with priority: 10 applies the same action, but the WAF does not merge them. The first rule’s actions are executed, but the second rule’s actions are not applied until a later pass.block rule without code and message returns a plain 403 with no body, but the API expects JSON. The frontend fails to parse the response.type: "body" but missing pattern or value causes the WAF to skip the condition entirely, leading to subtle bugs.X-Response-Time from a backend header fails when the backend sends X-Response-Time: 124ms but the WAF expects X-Response-Time: 124. Case sensitivity or whitespace in header keys breaks the mapping.Enable detailed logging and request tracing:
$ systemctl set-environment WAF_LOG_LEVEL=debug
$ systemctl reload waf
# Trace a request
$ curl -v -H "X-Debug: trace" https://api.example.com/api/v1/users?limit=10
The WAF logs:
The WAF emits structured logs:
{
"timestamp": "2023-10-05T12:34:56Z",
"request_id": "a1b2c3d4",
"method": "GET",
"path": "/api/v1/users",
"ip": "192.168.1.100",
"rules": [
{
"id": 100,
"name": "Block SQL Injection",
"matched": true,
"actions": [
{ "type": "block", "code": 403, "message": "SQL Injection detected" },
{ "type": "log", "level": "warning", "message": "SQLi: a1b2c3d4" },
{ "type": "add-header", "key": "X-WAF-Blocked", "value": "SQLi" }
]
}
],
"metrics": {
"rule_evaluation_time_ms": 12,
"request_size_bytes": 234,
"response_size_bytes": 1245
}
}
Alerting rules:
rule_evaluation_time_ms > 50 → slow rule processing.request_id in [blocked_requests] → investigate blocked requests.response.status_code = 403 and log.level = warning → investigate WAF block events.WAF upgrades are atomic. A new configuration is applied, and the WAF switches to it with zero downtime.
# Prepare new configuration
$ cp rules.yaml rules-new.yaml
$ systemctl reload waf
# Verify
$ curl -H "X-Test: verify" https://api.example.com/health
# Success → switch to new config
$ systemctl set-environment WAF_ACTIVE_CONFIG=rules-new.yaml
$ systemctl reload waf
Rollback:
WAF_ACTIVE_CONFIG=rules-old.yaml.Web Application Firewall engineering is the practice of defining, deploying, and operating rules that inspect and secure HTTP traffic. It is not a static configuration but a dynamic system that evolves with the application. Engineers must understand how rules are evaluated, how request and response data flows through the WAF, and how each action affects the final response. The most common failures—misleading rule priorities, header mismatches, and unvalidated transformations—arise not from complexity, but from subtle expectations. Mastery comes from anticipating how each rule behaves under real load, and from observing the WAF’s logs and metrics during incidents.
This page was rewritten on 17 October 2026. It replaced a templated version that was not specific to its own title. How these pages are written and checked. If anything here is wrong, tell us at [email protected] and we will correct it.
We use cookies for analytics (Google Analytics) and advertising (Google AdSense) to improve your experience and support free content. Privacy Policy