Drop-in nginx WAF that blocks SQLi/XSS/RCE inline at ~3 µs/request.
641
Next-gen Protection X — nginx WAF module by 203x
nginx with inline WAF — block SQLi, XSS, RCE, LFI, SSRF, scanner probes, and named-CVE patterns in the same ACCESS phase as your proxy_pass and rewrites. No sidecar, no extra hop, no control plane.
For teams that already run nginx at the edge: swap the image, keep your config model, point port 8080 (or mount your own nginx.conf). Attacks are decided before traffic hits your upstream. Structured JSON logs and Prometheus metrics ship in the default config.
Proof (reproduce before prod): GoTestWAF v0.5 — 674 / 674 resolved attacks blocked, 141 / 141 benign passed (default flags, real HTTP-200 backend). Gotestwaf overall score 100.00 % (resolved-only denominator); sent coverage 674 / 675 = 99.85 % — one path payload with %00 is rejected by nginx as HTTP 400 before the WAF runs (details). Performance: +1–3 µs p50 vs waf off on a lab Ryzen 9 7950X (single connection); at c=100 clean GET is ~13 % below waf off on the same host (850k+ rps). Run your own A/B on hardware that matches production (benchmark).
Distroless multi-arch image (~12 MB compressed), cosign-signed manifest, Trivy HIGH/CRITICAL gate at publish. License: BUSL-1.1 (free for your own apps; commercial terms for multi-tenant / hosted WAF — matrix).
* resolved denominator per gotestwaf; see Honest GoTestWAF accounting.
| Image | happensit/npx-waf:latest — amd64 + arm64 |
| Docs | GitHub README · copy from image: docker cp $(docker create happensit/npx-waf:latest):/README.md ./ |
| Try | 30 seconds |
| Production | docker-compose · extract nginx.conf → proxy_pass |
| Accuracy | GoTestWAF + honest accounting |
| Discuss | GitHub Discussions |
Does: HTTP/1.x (and nginx HTTP/2 front) request inspection in ACCESS phase — URI, args, headers, cookies, Referer, body (JSON, form, multipart) per bundled parsers; CRS-style anomaly scoring; IP/URI allowlists; honeypot blocklist; Prometheus + JSON access log.
Does not replace: a global CDN/WAF (bot management, DDoS edge, managed rule UI). Use your CDN in front; NPX WAF at the origin is a common pattern.
Limits to plan for: TLS termination is your nginx.conf (demo listens on 8080 plain HTTP). Very large single field values may skip full decode above the engine buffer cap and fall back to raw-byte signature scanning — tune with metrics and rules. WebSocket/gRPC are only covered if they flow through nginx HTTP locations you protect.
| Deployment | BUSL-1.1 |
|---|---|
| Protect your own apps / internal platforms on your infrastructure | Free |
| Large enterprise or infrastructure provider | Commercial terms |
| SaaS, hosted WAF, or multi-tenant service for third parties | Commercial terms |
| Each version: 4 years after its release, or Jan 1 2032, whichever is earlier | Converts to Apache-2.0 |
linux/amd64 + linux/arm64latest, semver MAJOR.MINOR.PATCH/README.md inside the imagePlatform, DevOps, AppSec, and infrastructure teams that already run nginx at the edge and want inline WAF enforcement inside that same layer — self-hosted SaaS, internal platforms, API gateways, staging edges, and regulated environments.
If you need a vendor-operated global edge, keep your CDN/WAF and place NPX WAF behind it as a second layer at the origin. If you operate nginx yourself, NPX WAF is a drop-in replacement that blocks attacks before they reach your upstreams.
Internet NPX WAF inspection Result
──────────────────── ────────────────────────────── ────────
GET /?q=hello → [scan 4,050 patterns] → 200 → backend
GET /?q=[XSS] → [hit: XSS, score=8, 3µs] → 403 ✕ backend
GET /?id=[SQLi] → [hit: SQLi, score=8, 3µs] → 403 ✕ backend
POST [NoSQLi body] → [hit: NoSQLi, score=8, 5µs] → 403 ✕ backend
↓ ↓
Prometheus /metrics JSON access log
(counters + histograms) (waf_action, waf_category,
waf_severity, waf_reason)
↓ ↓
Alertmanager / Grafana SIEM (Elastic/Loki/Splunk)
Inspection happens inside nginx, not beside it. Your existing config keeps working; the WAF adds an inline decision step. The compiled signature DB is mmap-loaded once at startup and shared across workers via fork-COW.
docker run -d --name npx-waf -p 8080:8080 happensit/npx-waf:latest
# Benign request passes
curl -si 'http://127.0.0.1:8080/?q=hello' | grep -E '^(HTTP/|X-WAF-)'
# HTTP/1.1 200 OK
# X-WAF-Action: PASS
# Any classic injection probe (XSS / SQLi / RCE / LFI payload) is
# rejected at the edge — substitute one of your own:
curl -si 'http://127.0.0.1:8080/?q=...test-payload-here...' | grep -E '^(HTTP/|X-WAF-)'
# HTTP/1.1 403 Forbidden
# X-WAF-Action: BLOCK
More probes (SQLi, LFI, RCE) — GitHub README or /README.md in the image.
The image ships a working /etc/nginx/nginx.conf — waf on;, waf_mode enforce;, waf_threshold 8;, JSON access log, /healthz+/readyz, and /metrics on 127.0.0.1:9113. Start from that file, change only location / to a proxy_pass;.
ID=$(docker create happensit/npx-waf:latest)
docker cp $ID:/etc/nginx/nginx.conf ./nginx.conf && docker rm $ID
# Edit `location / { ... }` → proxy_pass http://your-backend:3000;
docker run -d --name npx-waf -p 8080:8080 \
-v "$PWD/nginx.conf:/etc/nginx/nginx.conf:ro" happensit/npx-waf:latest
docker kill --signal=SIGHUP npx-waf # reload without dropped conns
Recipes for TLS, per-route waf off;, CDN real-IP, etc. — GitHub README.
Important — don't use
return 200 "..."in a WAF-protectedlocation.returnfires in REWRITE_PHASE, before ACCESS_PHASE where the WAF runs — every request short-circuits unblocked. Useproxy_passortry_files.returnis safe only insidewaf off;blocks.
| NPX WAF | Common WAF deployment pattern | |
|---|---|---|
| Runtime model | inside the nginx binary | sidecar, external proxy, or managed edge |
| Per-request overhead | +1–3 µs p50 (lab, single connection; see bench) | depends heavily on engine, rules, and tuning |
| Detection accuracy | GoTestWAF v0.5: 674/674 resolved blocked. See accounting. | must be measured against the deployed ruleset |
| Attack signatures | 4,050, compiled into a SIMD scan engine | rules often evaluated on the request path |
| Named-CVE rules | 110 rules (88 CVE-tagged + 22 IDS-derived; 16 KEV) | varies by vendor, ruleset, and subscription tier |
| Base image | distroless, no shell, non-root UID 65532 | frequently general-purpose Linux userland |
| Update flow | image tag swap — graceful drain, rollback in seconds | rule reload, config rollout, or vendor-side change |
| Rule update cost | 0 (bundle is mmap-loaded) | depends on cache, JIT, and reload strategy |
| Manifest signature | cosign-signed via Sigstore keyless OIDC | project-dependent |
GoTestWAF gives you a concrete attack-coverage and FP-rate baseline before prod traffic touches the image. Start in waf_mode monitor; for sensitive legacy routes (log only, no block); move to enforce once the traffic profile is known. Every decision lands in your SIEM as a top-level JSON field (waf_action, waf_category, waf_severity) — no Grok, no parser pipelines.
Minimal docker-compose.yml:
services:
waf:
image: happensit/npx-waf:latest
restart: unless-stopped
ports: [ "80:8080" ]
volumes:
- ./nginx.conf:/etc/nginx/nginx.conf:ro
read_only: true
tmpfs:
- /var/cache/nginx:size=64m,uid=65532,gid=65532
- /run:size=8m,uid=65532,gid=65532
cap_drop: [ALL]
cap_add: [NET_BIND_SERVICE]
security_opt: [ "no-new-privileges:true" ]
stop_signal: SIGQUIT
stop_grace_period: 30s
Defense-in-depth: read-only root filesystem, capabilities dropped to one (NET_BIND_SERVICE), no-new-privileges, and a graceful SIGQUIT shutdown. No shell and no writable root, removing the most common post-exploitation primitives.
Resource sizing: memory scales linearly with worker_processes. Steady-state is ~30 MB per worker, peaking around 60 MB during heavy POST body inspection, plus ~18 MB fixed overhead. Sizing tables and deployment-tier matrix — GitHub README.
waf_severity (CRITICAL/HIGH/MEDIUM/LOW) maps from the anomaly score (CRITICAL ≥ 8, HIGH ≥ 5, MEDIUM ≥ 3, LOW ≥ 1). Tune waf_threshold per location instead of long lists of rule exceptions. Mapping details — GitHub README.Native directives configured directly in nginx.conf:
waf_mode enforce (block on threshold) / waf_mode monitor (log only, no block).waf_threshold 8 per location, tunable.waf off; on health endpoints, internal APIs, etc.waf_whitelist_ip <CIDR> / waf_blacklist_ip <CIDR>.waf_whitelist_uri /api/internal/....waf_honeypot_uri /trap/.... Offending IP is added to blocklist for ~24 hours./usr/local/sbin/npx-waf-compile.Snippets with verified behavior notes — GitHub README.
4,050 patterns total (exact line-counts of .rules/.data at /var/lib/npx-waf/rules/). Top cohorts:
| Category | Patterns | Weight | Coverage |
|---|---|---|---|
| LFI / path traversal | 1,716 | 8 | OS files, .env, .git/, /proc/, /var/log/ |
| Command injection | 948 | 8 | Unix shell builtins, PowerShell, RCE patterns |
| File upload restrictions | 465 | 4 | Dangerous extensions, double-extension, polyglots |
| Code injection | 301 | 8 | PHP functions, Java classes, deserialization gadgets |
| SSRF | 135 | 7–8 | Cloud metadata; localhost; URL-parser bypasses |
| Named-CVE virtual-patches | 110 | 8 | 88 CVE + 22 IDS. Ivanti, NetScaler, ActiveMQ, Fortinet |
| Web shells | 77 | 8 | PHP/ASP shells (c99, r57, WSO, China Chopper) |
| SQLi + XSS | 55 + 54 | 8 | Signature layers + structural tokenisers |
| Protocol abuse / scanner UA | 37 + 14 | 3 | CL/TE request smuggling; nuclei, sqlmap, nikto, dirb |
| Other (SSTI, pollution, etc.) | 137 | var | Jinja2, Twig, __proto__, constructor pollution |
Default waf_threshold = 8. A single SQLi / XSS / RCE / LFI / Webshell / CVE match reaches the threshold alone and blocks. Lower-weight signals (Scanner UA / Disclosure) require at least one additional signal to block. Tunable per-location.
/healthz (kubelet liveness), /readyz (readiness once WAF bundle is loaded).nginx -t and re-deserializes the WAF bundle.127.0.0.1:9113/metrics. Exports waf_requests_total, waf_action_total, waf_score (histogram), waf_category_hits_total, waf_inspect_duration_us (histogram), waf_blocklist_hits_total.waf_action, waf_category, waf_severity, waf_reason, plus standard nginx fields. Ingest directly into Elastic, Loki, Splunk, or Datadog with zero Grok parsing.STOPSIGNAL SIGQUIT drains in-flight requests during rolling deploys.SIEM shipper configs (Vector, Filebeat, Promtail, Datadog Agent, Splunk) and a sample dashboard query library — GitHub README.
Lab setup: Ryzen 9 7950X, nginx 1.31.1 (32 workers, reuseport), 4,050 patterns, 3-byte static backend, oha 1.14.0. Re-run on your hardware before production.
Single connection WAF cost:
| Scenario | WAF rps · p50 · p99 | noWAF rps · p50 · p99 | overhead p50 |
|---|---|---|---|
GET / clean | 46,323 · 20 µs · 24 µs | 49,717 · 19 µs · 22 µs | +1 µs |
GET /?q=(2 KiB benign) | 36,049 · 25 µs · 30 µs | 41,205 · 22 µs · 25 µs | +3 µs |
| POST JSON 250 B body | 48,531 · 19 µs · 23 µs | 54,810 · 17 µs · 20 µs | +2 µs |
| Attack (XSS → 403, early-exit) | 50,348 · 18 µs · 23 µs | (noWAF→200) 49,364 · 19 µs · 22 µs | −1 µs ¹ |
Ceiling throughput (WAF enabled): Peaks at ~1.0 M rps at c=500 (p50 ~314 µs). noWAF peak is ~1.04 M rps (WAF overhead at saturation is ~3%). Full sweep tables — GitHub README.
¹ Blocked attacks use fewer CPU cycles than passed requests. 403 blocks in ACCESS_PHASE before proxying or serving content, shielding your origin under heavy attack floods.
Detection accuracy (Wallarm GoTestWAF v0.5):
| Metric | Result | Meaning |
|---|---|---|
| True-Positive (App Security) | 674 / 674 resolved — 100.00 % | Every attack payload reaching WAF is blocked |
| True-Positive (API Security) | 14 / 14 — 100.00 % | Every API-shaped attack blocked |
| True-Negative (FP-freedom) | 141 / 141 — 100.00 % | No false positives in this corpus |
| Overall Score | 100.00 % |
Reproducible in 2 minutes:
docker run --rm --network=host wallarm/gotestwaf --url=http://127.0.0.1:8080 --noEmailReport
Re-run the benchmark yourself before any production rollout. The important number is not the badge — it is the delta between waf on; and waf off; on hardware that looks like yours.
GoTestWAF score is blocked_tests / resolved_tests * 100 (unresolved tests are excluded from the denominator). Out of 675 attack payloads, 674 resolved and were blocked (99.85% total sent coverage). One payload is unresolved:
owasp/xss-scripting containing %00 in the URL path. Nginx's URI parser rejects NUL bytes after URL-decoding per RFC 3986, returning HTTP 400 before the phase handler runs. Gotestwaf excludes HTTP 400 from the score denominator. Without %00, the payload is blocked by the WAF with HTTP 403.Multipart bare-LF bypass (community-lfi-multipart) was closed in commit 17e66b7 via raw-body signature-scan fallback.
Three inline layers in a single nginx ACCESS-phase handler:
%XX and JSON \uXXXX (two-pass) → base64 applied to URI, args, headers, cookies, Referer, and body.Rules compile into a binary bundle at image build time. No per-request regex compilation, no JIT warmup, no cold cache.
gcr.io/distroless/cc-debian13:nonroot — no shell, no package manager.NET_BIND_SERVICE, no-new-privileges enforced. Read-only root filesystem with tmpfs for /var/cache/nginx and /run.Is “100% GoTestWAF” equal to “blocks everything”? No. It measures a specific public corpus. We publish the exact 674/675 sent coverage. Always verify with your own test suites before a production rollout.
Will I get 1 million requests per second? Only in our optimal lab setup (32 workers, Zen 4, minimal backend). Size your deployment based on the memory tables in the GitHub README.
Will this slow down my application? In our benchmarks: +1 µs p50 for clean GET, +2 µs for 250 B JSON POST. At c=100, WAF overhead at saturation is ~3%.
Can I run in log-only mode?
Yes, set waf_mode monitor; per location. Decisions will log with waf_* metadata and set the X-WAF-Action header without blocking.
How do I update rules or compile custom rules?
Pull new tags. Rollback/updates take seconds via SIGQUIT graceful drain. To compile custom rules, use /usr/local/sbin/npx-waf-compile inside the image (rules sources at /var/lib/npx-waf/rules/).
What is BUSL-1.1? Free for internal/self-hosted use protecting your own apps. Large enterprise deployments, infrastructure providers, and multi-tenant/SaaS offerings require commercial terms. Each version auto-converts to Apache-2.0 four years after its first public distribution, or January 1, 2032, whichever is earlier.
Discussions & Q&A: GitHub Discussions.
Responsible disclosure of security reports: contact the maintainer privately. Please do not post security bugs on public forums.
Content type
Image
Digest
sha256:8711885ed…
Size
11.7 MB
Last updated
4 months ago
docker pull happensit/npx-waf