Phase 8: WAF & Advanced Security
Goal
Request-level security filtering built into bext — IP filtering, geo-blocking, request inspection, bot detection, and DDoS mitigation — so production deployments don't need Cloudflare or a separate WAF.
Current State
- Rate limiting: per-IP token bucket (600 req/min default,
middleware/rate_limit.rs) - Security headers: CSP, HSTS, X-Frame-Options (builtin plugin)
- SSRF prevention: private IP blocking in plugin host functions
- CORS: origin allowlisting
- No IP allow/deny lists
- No geo-blocking
- No request body inspection
- No bot detection
- No DDoS mitigation beyond basic rate limits
Design
Security Middleware Stack
Insert before all other middleware (after TLS termination):
Request → IP Filter → Geo Block → Rate Limit → Request Inspect → Bot Detect → ... → App
1. IP Filtering
Allow/deny lists with CIDR support:
[security.ip_filter]
mode = "deny" # allow | deny | off
# Deny list (blocked IPs/ranges)
deny = [
"192.168.1.0/24",
"10.0.0.5",
"2001:db8::/32",
]
# Allow list (always permitted, bypasses deny)
allow = [
"10.0.0.1", # Monitoring
"192.168.1.100", # Admin
]
# Action when blocked
deny_status = 403
deny_body = "Forbidden"
# Optional: load from file (hot-reloaded)
deny_file = "/etc/bext/blocklist.txt"
Implementation: Patricia trie for O(prefix_length) CIDR lookup.
2. Geo-Blocking
Block or allow by country code using MaxMind GeoLite2 database:
[security.geo]
enabled = true
database = "/etc/bext/GeoLite2-Country.mmdb" # MaxMind DB path
# Block mode: deny listed countries
mode = "deny"
countries = ["CN", "RU", "KP"]
# Or allow mode: only allow listed countries
# mode = "allow"
# countries = ["FR", "DE", "US", "GB"]
# Bypass for specific paths (e.g., health checks)
bypass_paths = ["/health", "/.well-known/*"]
# Header to check for real IP (behind proxy)
real_ip_header = "X-Forwarded-For"
Implementation: maxminddb crate for mmdb parsing, cached in memory.
3. Request Inspection (Mini-WAF)
Rule-based request filtering inspired by OWASP ModSecurity Core Rule Set, but lightweight:
[security.waf]
enabled = true
mode = "block" # block | log-only | off
# Built-in rule sets
[security.waf.rules]
sql_injection = true # Detect SQLi patterns in query/body
xss = true # Detect XSS patterns in query/body
path_traversal = true # Block ../ in URLs
shell_injection = true # Detect shell metacharacters
protocol_violation = true # Malformed HTTP requests
scanner_detection = true # Block known scanner UAs
# Custom rules
[[security.waf.custom_rules]]
name = "block-wp-scans"
match = { path = "/wp-admin/*" }
action = "block"
status = 404
[[security.waf.custom_rules]]
name = "rate-limit-login"
match = { path = "/api/auth/login", method = "POST" }
action = "rate-limit"
rate = "5/minute"
[[security.waf.custom_rules]]
name = "require-api-key"
match = { path = "/api/v1/**" }
condition = { header_missing = "X-API-Key" }
action = "block"
status = 401
Rule patterns (regex-based, compiled at startup):
lazy_static! {
static ref SQL_INJECTION_PATTERNS: Vec = vec![
Regex::new(r"(?i)(union\s+select|or\s+1\s*=\s*1|drop\s+table|;\s*delete)").unwrap(),
Regex::new(r"(?i)('|\"|;|--)\s*(or|and|union|select|drop|delete|insert|update)").unwrap(),
];
static ref XSS_PATTERNS: Vec = vec![
Regex::new(r"(?i)<script[^>]*>").unwrap(),
Regex::new(r"(?i)(javascript|vbscript|data):").unwrap(),
Regex::new(r"(?i)on(load|error|click|mouse|focus)\s*=").unwrap(),
];
static ref PATH_TRAVERSAL: Regex = Regex::new(r"(\.\./|\.\.\\|%2e%2e)").unwrap();
}
4. Bot Detection
Distinguish humans from bots using behavioral signals:
[security.bot]
enabled = true
mode = "challenge" # block | challenge | log-only
# Known good bots (skip detection)
allow = [
"Googlebot",
"Bingbot",
"Slurp",
"DuckDuckBot",
"facebookexternalhit",
]
# Detection signals
[security.bot.signals]
missing_accept_header = true # Bots often omit Accept
tls_fingerprint = true # JA3/JA4 fingerprinting
request_rate_anomaly = true # Burst detection
known_scanner_ua = true # nikto, sqlmap, etc.
Challenge mode serves a lightweight JS challenge (< 1KB) that browsers execute automatically but headless bots often fail.
5. DDoS Mitigation
Beyond per-IP rate limiting — connection-level and pattern-based protection:
[security.ddos]
enabled = true
# Connection limiting
max_connections_per_ip = 50 # Concurrent connections per IP
max_new_connections_per_second = 20 # Connection rate per IP
# Request limiting (layered on top of rate_limit)
max_request_body_size = "10mb" # Reject oversized bodies
slowloris_timeout_ms = 10000 # Close slow-reading connections
header_count_limit = 100 # Max headers per request
header_size_limit = "8kb" # Max total header size
# Automatic throttling under load
[security.ddos.auto_throttle]
enabled = true
cpu_threshold = 80 # Start throttling at 80% CPU
response_time_threshold_ms = 500 # Start throttling when avg response > 500ms
throttle_ratio = 0.5 # Reject 50% of new connections when throttling
6. Rate Limit Enhancements
Extend existing rate limiter with features from nginx and Cloudflare:
[security.rate_limit]
enabled = true
default_rpm = 600 # Global default
# Per-route rate limits
[[security.rate_limit.rules]]
pattern = "/api/auth/**"
rpm = 30
burst = 10 # Allow burst above limit
delay = "nodelay" # nodelay | delay (queue excess)
[[security.rate_limit.rules]]
pattern = "/api/v1/**"
rpm = 100
key = "header:X-API-Key" # Rate limit by API key, not IP
burst = 20
# Distributed rate limiting (Redis-backed)
[security.rate_limit.distributed]
enabled = true # Use Redis for cross-instance limits
Response headers:
RateLimit-Limit: 100
RateLimit-Remaining: 42
RateLimit-Reset: 1711843200
Retry-After: 30 # Only on 429 responses
Implementation
New Crate: bext-waf
bext-waf/
src/
lib.rs # Public API, middleware factory
ip_filter.rs # CIDR allow/deny with Patricia trie
geo.rs # GeoIP lookup (maxminddb)
rules/
mod.rs # Rule engine
sqli.rs # SQL injection patterns
xss.rs # XSS patterns
traversal.rs # Path traversal patterns
scanner.rs # Scanner UA detection
custom.rs # User-defined rules
bot.rs # Bot detection signals
ddos.rs # Connection limiting, slowloris, auto-throttle
rate_limit.rs # Enhanced rate limiter (burst, per-key, distributed)
challenge.rs # JS challenge for bot detection
Dependencies
| Crate | Purpose |
|---|---|
maxminddb |
GeoIP database reader |
ip_network_table |
Patricia trie for CIDR lookup |
regex |
Rule pattern matching (already in deps) |
actix-web Middleware Integration
pub struct WafMiddleware {
ip_filter: Arc,
geo_blocker: Option<Arc>,
rule_engine: Arc,
bot_detector: Option<Arc>,
ddos_guard: Arc,
rate_limiter: Arc,
}
impl WafMiddleware {
fn check_request(&self, req: &HttpRequest) -> WafDecision {
// 1. IP filter (fastest check)
if let Some(block) = self.ip_filter.check(req.peer_addr()) {
return WafDecision::Block(block);
}
// 2. Geo check
if let Some(geo) = &self.geo_blocker {
if let Some(block) = geo.check(req.peer_addr()) {
return WafDecision::Block(block);
}
}
// 3. DDoS checks (connection limits)
if let Some(block) = self.ddos_guard.check(req) {
return WafDecision::Block(block);
}
// 4. Rate limit
if let Some(limited) = self.rate_limiter.check(req) {
return WafDecision::RateLimit(limited);
}
// 5. Rule engine (SQLi, XSS, etc.)
if let Some(violation) = self.rule_engine.inspect(req) {
return WafDecision::Block(violation);
}
// 6. Bot detection
if let Some(bot) = &self.bot_detector {
if let Some(challenge) = bot.check(req) {
return WafDecision::Challenge(challenge);
}
}
WafDecision::Allow
}
}
Audit Logging
All WAF decisions are logged for analysis:
struct WafEvent {
timestamp: DateTime,
client_ip: IpAddr,
path: String,
rule: String, // "sqli", "xss", "ip-deny", "geo-block", "rate-limit"
action: String, // "block", "challenge", "log"
details: String, // Matched pattern or reason
}
Accessible via:
GET /__bext/waf/events— recent WAF events (bounded buffer)GET /metrics— Prometheus counters per rule typebext waf stats— CLI summary
Testing Plan
| Test | Type | What it validates |
|---|---|---|
| CIDR allow/deny | Unit | IP ranges matched correctly |
| Patricia trie lookup | Unit | O(prefix_length) performance |
| GeoIP country lookup | Unit | IP → country code mapping |
| Geo mode allow/deny | Unit | Correct countries blocked/allowed |
| SQLi detection | Unit | Known injection patterns caught |
| XSS detection | Unit | Script tags, event handlers caught |
| Path traversal | Unit | ../ variants blocked |
| Custom rules | Unit | User-defined rules match and act |
| Bot UA detection | Unit | Known scanners flagged |
| Good bot bypass | Unit | Googlebot etc. allowed |
| DDoS connection limit | Unit | Excess connections rejected |
| Slowloris detection | Unit | Slow clients disconnected |
| Rate limit burst | Unit | Burst above limit allowed, excess blocked |
| Rate limit by key | Unit | Per-API-key limits independent |
| Rate limit headers | Unit | RateLimit-* headers in response |
| Distributed rate limit | Integration | Redis-backed limits work across instances |
| JS challenge | Integration | Browser passes, simple bot fails |
| Auto-throttle | Unit | Throttling activates at CPU threshold |
| Audit logging | Unit | WAF events recorded with details |
| Log-only mode | Unit | Violations logged but not blocked |
Done Criteria
- IP allow/deny with CIDR support
- GeoIP blocking with MaxMind database
- SQLi, XSS, path traversal, scanner detection rules
- Custom WAF rules in config
- Bot detection with JS challenge
- DDoS mitigation (connection limits, slowloris, auto-throttle)
- Enhanced rate limiting (burst, per-key, distributed)
- Rate limit response headers (RateLimit-*, Retry-After)
- WAF audit log endpoint
-
log-onlymode for testing rules without blocking - Hot-reload for IP blocklists
- All tests passing