Security & WAF
bext includes an enterprise-grade Web Application Firewall (WAF) that inspects every request through six layered defense stages. The WAF runs in-process with zero per-request regex compilation cost -- all patterns are compiled once at startup. This is a Pro feature; Community edition includes the default security headers and basic rate limiting.
Defense Layers
Requests pass through these stages in order. The first non-Allow decision terminates the chain:
1. IP Filter -- CIDR-based allow/deny lists 2. GeoIP Blocking -- country-level access control 3. DDoS Guard -- connection limits, body/header size caps 4. Rate Limiting -- token-bucket per-path rules 5. Bot Detection -- multi-signal behavioral analysis 6. Rule Engine -- SQL injection, XSS, path traversal, shell injection, scanner detection, custom rules
WAF Configuration
[waf]
enabled = true
# IP filtering (mode: "allow", "deny", or "off")
[waf.ip_filter]
mode = "deny"
deny_list = ["10.0.0.0/8", "203.0.113.50"]
allow_list = ["10.0.0.5"] # Allow list overrides deny list
# GeoIP blocking (requires MaxMind GeoLite2 database)
[waf.geo]
enabled = true
mode = "deny" # or "allow" for whitelist mode
countries = ["CN", "RU"] # ISO 3166-1 alpha-2 codes
bypass_paths = ["/health", "/api/status"]
real_ip_header = "X-Forwarded-For" # Extract real IP from proxy header
# Request inspection rules
[waf.rules]
sql_injection = true
xss = true
path_traversal = true
shell_injection = true
scanner_detection = true
protocol_violations = true
# Bot detection
[waf.bot]
enabled = true
mode = "block" # "block", "challenge", or "log_only"
signal_threshold = 2 # Number of signals to trigger detection
good_bots = [ # Always allowed (case-insensitive match)
"Googlebot", "Bingbot", "Slurp", "DuckDuckBot",
"facebookexternalhit", "Twitterbot", "Applebot"
]
# DDoS mitigation
[waf.ddos]
max_connections_per_ip = 100
max_new_connections_per_second = 500
max_request_body_size = 10485760 # 10 MB
header_count_limit = 100
header_size_limit = 16384 # 16 KB
slowloris_timeout_ms = 10000
# Path-specific rate limiting
[[waf.rate_limit_rules]]
name = "api_global"
pattern = "/api/**"
rpm = 600 # Requests per minute
burst = 50 # Extra burst capacity
key_source = "ip" # "ip", {"header": "X-Api-Key"}, {"cookie": "session"}
[[waf.rate_limit_rules]]
name = "auth_strict"
pattern = "/api/auth/**"
rpm = 20
burst = 5
key_source = "ip"
# Custom blocking rules
[[waf.custom_rules]]
name = "block-admin-external"
action = "block"
status = 403
reason = "Admin access denied from external network"
[waf.custom_rules.match_config]
path = "/admin/**"
IP Filtering
The IP filter supports both individual addresses and CIDR ranges for IPv4 and IPv6. In deny mode, the allow list takes priority over the deny list, letting you carve out exceptions:
[waf.ip_filter]
mode = "deny"
deny_list = ["192.168.0.0/16"]
allow_list = ["192.168.1.100"] # This specific IP is allowed despite the range deny
In allow mode, only listed IPs can access the server -- all others receive a 403.
The IP filter supports hot-reload: update the configuration and send SIGHUP to apply new rules without restarting the server.
GeoIP Blocking
Country-level blocking uses MaxMind's GeoLite2-Country database. Download the free .mmdb file and point bext to it:
geoip_db_path = "/etc/bext/GeoLite2-Country.mmdb"
In deny mode, unknown countries (lookup failures) are allowed through. In allow mode, unknown countries are blocked -- this is the more restrictive default for compliance scenarios.
The real_ip_header setting handles proxy chains. For X-Forwarded-For: 1.2.3.4, 5.6.7.8, bext extracts the first IP (1.2.3.4) as the client address.
Bot Detection
The bot detector scores requests against five behavioral signals:
1. Missing User-Agent header
2. Missing Accept header
3. Missing Accept-Language header
4. Missing Accept-Encoding header
5. Known scanner user-agent (sqlmap, Nikto, etc.)
When the signal count meets the threshold, the configured action triggers:
- block -- returns 403 immediately
- challenge -- serves a JavaScript challenge page; headless bots without a JS engine fail
- log_only -- records the detection without blocking (useful for tuning thresholds)
Known good bots (Googlebot, Bingbot, etc.) are exempted by default using case-insensitive user-agent substring matching.
DDoS Protection
The DDoS guard enforces multiple limits simultaneously:
- Per-IP connection limit -- prevents a single IP from monopolizing server resources (default: 100 concurrent connections)
- Global connection rate -- caps new connections per second to prevent SYN flood saturation (default: 500/s)
- Body size limit -- rejects oversized payloads with 413 (default: 10 MB)
- Header abuse -- limits header count (100) and total header size (16 KB) to prevent Slowloris-style attacks
Connection tracking is bounded at 100,000 entries to prevent the guard itself from becoming a memory exhaustion vector.
Rate Limiting
Rate limit rules use a token-bucket algorithm with configurable burst capacity. Keys can be extracted from:
| Key source | Use case |
|---|---|
ip |
General API protection (normalizes IPv6 to /64) |
header("X-Api-Key") |
Per-API-key limits |
cookie("session") |
Per-session limits |
When a request is rate-limited, bext returns 429 with standard headers:
HTTP/1.1 429 Too Many Requests
Retry-After: 12
RateLimit-Limit: 600
RateLimit-Remaining: 0
Default Security Headers
Even without the WAF enabled, bext injects baseline security headers on every response:
| Header | Value |
|---|---|
X-Content-Type-Options |
nosniff |
X-Frame-Options |
SAMEORIGIN |
Referrer-Policy |
strict-origin-when-cross-origin |
X-XSS-Protection |
0 (disables legacy auditor per modern best practice) |
Content-Security-Policy |
default-src 'self'; script-src 'self' 'unsafe-inline'; style-src 'self' 'unsafe-inline'; img-src 'self' data:; frame-ancestors 'none' |
Strict-Transport-Security |
max-age=63072000; includeSubDomains (TLS only) |
CORS Configuration
[cors]
allowed_origins = ["https://app.example.com", "https://admin.example.com"]
max_age_secs = 3600
When no origins are configured, bext defaults to restrictive CORS (no cross-origin requests allowed). Allowed methods include GET, POST, PUT, DELETE, PATCH, and OPTIONS. Custom headers x-tenant-id and x-site-id are included by default.
Monitoring
WAF statistics are available at /_bext/admin/waf/stats and in Prometheus format at /_bext/metrics:
{
"total_requests": 184920,
"allowed": 184501,
"blocked": 389,
"rate_limited": 27,
"challenged": 3
}
The audit log at /_bext/admin/waf/audit records each block decision with the client IP, path, rule name, and timestamp for forensic analysis.