Firewall
Firewall Plugin
The firewall plugin evaluates conditions against every request and can allow, block, redirect, or modify headers. Rules are evaluated in order — first match wins.
Configuration
{
"plugins": {
"firewall": {
"enabled": true,
"rules": [
{
"id": "rule-id",
"name": "Human readable name",
"enabled": true,
"conditions": [
{
"left": "req.path",
"operator": "starts_with",
"value": "/admin/"
}
],
"action": {
"type": "block",
"status": 403
}
}
]
}
}
}
Rule structure
| Field | Type | Required | Description |
|---|---|---|---|
id | string | ✅ | Unique rule identifier |
name | string | ✅ | Human-readable name |
enabled | bool | — | Enable/disable rule (default: true) |
conditions | array | ✅ | Matching conditions (see Conditions) |
action | object | ✅ | Action to take when conditions match |
conditions are rejected at startup (unlike rate limit rules, where empty conditions mean "always"). A firewall rule must always state when it applies.Actions
Block
Returns an error response and stops the pipeline.
{
"action": {
"type": "block",
"status": 403
}
}
Response body:
{"error": "blocked", "status": 403, "rule_id": "rule-id"}
Allow
Continues the pipeline (skips remaining firewall rules). Useful for whitelisting.
{
"action": {
"type": "allow"
}
}
Redirect
Returns a redirect response.
{
"action": {
"type": "redirect",
"status": 302,
"location": "https://example.com/blocked"
}
}
| Field | Type | Default | Description |
|---|---|---|---|
status | int | 302 | HTTP redirect status code |
location | string | "/" | Redirect destination URL |
Set request header
Injects headers into the request context for later plugins. The request continues to the upstream unchanged by this action.
{
"action": {
"type": "set_request_header",
"headers": {
"X-Custom-Header": "value",
"X-Client-Country": "${ip.country}"
}
}
}
Injected headers are visible to everything downstream in the pipeline — condition
resolution, rate-limit keys, and cache
keys — but are not written to the upstream request. (The gateway adds its own
X-Forwarded-Proto, X-Forwarded-Host, and X-Forwarded-For headers upstream.)
set_request_header a request-context tagging tool: annotate a request with
x-risk-tier: high in the firewall, then key a rate limit or cache rule on it. The
Test suite verifies this visibility with a
dedicated probe.Set response header
Reserved for future use. The rule matches and is logged, but no response header is currently set on the wire.
Rule evaluation
Rules are evaluated in order. The first matching rule wins:
Request
│
├── Rule 1: conditions match?
│ ├── YES → execute action → DONE
│ └── NO → continue
│
├── Rule 2: conditions match?
│ ├── YES → execute action → DONE
│ └── NO → continue
│
└── No rules matched → Continue to next plugin
Examples
Block Tor exit nodes
{
"id": "block-tor",
"name": "Block Tor Exit Nodes",
"enabled": true,
"conditions": [
{ "left": "ip.is_tor", "operator": "equals", "value": true }
],
"action": { "type": "block", "status": 403 }
}
Block VPN on login endpoints
{
"id": "block-vpn-login",
"name": "Block VPN on Login",
"enabled": true,
"conditions": [
{ "left": "req.path", "operator": "starts_with", "value": "/auth/" },
{ "left": "ip.is_vpn", "operator": "equals", "value": true }
],
"action": { "type": "block", "status": 403 }
}
Block high-risk IPs
{
"id": "block-high-risk",
"name": "Block High Risk IPs",
"enabled": true,
"conditions": [
{ "left": "ip.risk_score", "operator": "greater_than", "value": 40 }
],
"action": { "type": "block", "status": 403 }
}
Geo-block countries
{
"id": "geo-block",
"name": "Block specific countries",
"enabled": true,
"conditions": [
{ "left": "ip.country", "operator": "in", "value": ["CN", "RU", "KP"] }
],
"action": { "type": "block", "status": 403 }
}
Allow admin access from specific IPs
{
"id": "allow-admin-whitelist",
"name": "Allow Admin Whitelist",
"enabled": true,
"conditions": [
{ "left": "req.path", "operator": "starts_with", "value": "/admin/" },
{ "left": "req.ip", "operator": "not_in", "value": ["10.0.0.1", "10.0.0.2"] }
],
"action": { "type": "block", "status": 403 }
}
Block by User-Agent
{
"id": "block-bots",
"name": "Block known bots",
"enabled": true,
"conditions": [
{ "left": "req.user_agent", "operator": "regex", "value": "(?i)(bot|crawler|spider|scraper)" }
],
"action": { "type": "block", "status": 403 }
}
Redirect old URLs
{
"id": "redirect-old-api",
"name": "Redirect old API",
"enabled": true,
"conditions": [
{ "left": "req.path", "operator": "starts_with", "value": "/v1/" }
],
"action": {
"type": "redirect",
"status": 301,
"location": "https://api.example.com/v2/"
}
}
Set custom headers for upstream
conditions are rejected at startup. Use a catch-all condition like "req.ip" exists when a rule should apply to every request.{
"id": "inject-geo-headers",
"name": "Inject Geo Headers",
"enabled": true,
"conditions": [
{ "left": "req.ip", "operator": "exists" }
],
"action": {
"type": "set_request_header",
"headers": {
"X-Client-Country": "${ip.country}",
"X-Client-ASN": "${ip.asn}",
"X-Client-Risk": "${ip.risk_score}"
}
}
}
Allow specific ASN (whitelist)
{
"id": "allow-whitelist-asn",
"name": "Allow Whitelist ASN",
"enabled": true,
"conditions": [
{ "left": "ip.asn", "operator": "in", "value": [13335, 16509, 15169] }
],
"action": { "type": "allow" }
},
{
"id": "block-everyone-else",
"name": "Block Non-Whitelisted",
"enabled": true,
"conditions": [
{ "left": "req.ip", "operator": "exists" }
],
"action": { "type": "block", "status": 403 }
}
Log output
{
"firewall": {
"matched": true,
"rule_id": "block-tor",
"action": "Block"
}
}
When no rule matches:
{
"firewall": {
"matched": false,
"rule_id": null,
"action": null
}
}