Sign inSign up

happensit/npx-waf

By happensit

•Updated 4 months ago

Drop-in nginx WAF that blocks SQLi/XSS/RCE inline at ~3 µs/request.

Image
Networking
Security
Web servers
1

641

happensit/npx-waf repository overview

⁠NPX WAF

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⁠).

GoTestWAF CVE Scan Multi-arch Distroless SBOM License

* resolved denominator per gotestwaf; see Honest GoTestWAF accounting⁠.


⁠At a glance

Imagehappensit/npx-waf:latest — amd64 + arm64
DocsGitHub README⁠ · copy from image: docker cp $(docker create happensit/npx-waf:latest):/README.md ./
Try30 seconds⁠
Productiondocker-compose⁠ · extract nginx.conf → proxy_pass
AccuracyGoTestWAF + honest accounting⁠
DiscussGitHub Discussions⁠

⁠Scope (what this image does and does not)

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.


⁠Not a fit if

  • You need a vendor-operated global edge only (no nginx you control).
  • You need a click-to-edit rule UI with hot reload — rules are compiled into a binary bundle; custom patterns use the offline compiler shipped in the image.
  • You cannot run nginx (or refuse a drop-in nginx image) at the enforcement point.

⁠License (one screen)

DeploymentBUSL-1.1
Protect your own apps / internal platforms on your infrastructureFree
Large enterprise or infrastructure providerCommercial terms⁠
SaaS, hosted WAF, or multi-tenant service for third partiesCommercial terms⁠
Each version: 4 years after its release, or Jan 1 2032, whichever is earlierConverts to Apache-2.0

⁠Quick reference


⁠Who it is for

Platform, 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.


⁠How it sits in your stack

   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.


⁠Try it in 30 seconds

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.


⁠Customize the bundled config (extract → edit → mount)

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-protected location. return fires in REWRITE_PHASE, before ACCESS_PHASE where the WAF runs — every request short-circuits unblocked. Use proxy_pass or try_files. return is safe only inside waf off; blocks.


⁠Why teams choose NPX WAF

NPX WAFCommon WAF deployment pattern
Runtime modelinside the nginx binarysidecar, 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 accuracyGoTestWAF v0.5: 674/674 resolved blocked. See accounting⁠.must be measured against the deployed ruleset
Attack signatures4,050, compiled into a SIMD scan enginerules often evaluated on the request path
Named-CVE rules110 rules (88 CVE-tagged + 22 IDS-derived; 16 KEV)varies by vendor, ruleset, and subscription tier
Base imagedistroless, no shell, non-root UID 65532frequently general-purpose Linux userland
Update flowimage tag swap — graceful drain, rollback in secondsrule reload, config rollout, or vendor-side change
Rule update cost0 (bundle is mmap-loaded)depends on cache, JIT, and reload strategy
Manifest signaturecosign-signed via Sigstore keyless OIDCproject-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.


⁠Production deployment

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⁠.


⁠Migration guide

  • From ModSecurity / Coraza: CRS-style anomaly scoring is fully supported. 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⁠.
  • From AWS / Cloudflare WAF: Same edge block, JSON logging, and Prometheus alerting. NPX WAF runs inside your nginx layer instead of upstream of it, avoiding per-request inspection fees or traffic-volume tiers.

⁠Configuration

Native directives configured directly in nginx.conf:

  • Inspection mode: waf_mode enforce (block on threshold) / waf_mode monitor (log only, no block).
  • Threshold: waf_threshold 8 per location, tunable.
  • Bypass: waf off; on health endpoints, internal APIs, etc.
  • IP allow/deny: waf_whitelist_ip <CIDR> / waf_blacklist_ip <CIDR>.
  • URI bypass: waf_whitelist_uri /api/internal/....
  • Honeypot: waf_honeypot_uri /trap/.... Offending IP is added to blocklist for ~24 hours.
  • Custom rules: patterns compile into a binary bundle; offline compiler at /usr/local/sbin/npx-waf-compile.

Snippets with verified behavior notes — GitHub README⁠.


⁠What it blocks & Scoring

4,050 patterns total (exact line-counts of .rules/.data at /var/lib/npx-waf/rules/). Top cohorts:

CategoryPatternsWeightCoverage
LFI / path traversal1,7168OS files, .env, .git/, /proc/, /var/log/
Command injection9488Unix shell builtins, PowerShell, RCE patterns
File upload restrictions4654Dangerous extensions, double-extension, polyglots
Code injection3018PHP functions, Java classes, deserialization gadgets
SSRF1357–8Cloud metadata; localhost; URL-parser bypasses
Named-CVE virtual-patches110888 CVE + 22 IDS. Ivanti, NetScaler, ActiveMQ, Fortinet
Web shells778PHP/ASP shells (c99, r57, WSO, China Chopper)
SQLi + XSS55 + 548Signature layers + structural tokenisers
Protocol abuse / scanner UA37 + 143CL/TE request smuggling; nuclei, sqlmap, nikto, dirb
Other (SSTI, pollution, etc.)137varJinja2, 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.


⁠Operations

  • Endpoints: /healthz (kubelet liveness), /readyz (readiness once WAF bundle is loaded).
  • Docker HEALTHCHECK: Built-in (interval 10s). Runs nginx -t and re-deserializes the WAF bundle.
  • Prometheus metrics: Bound to 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.
  • Structured access log: One JSON line per request with waf_action, waf_category, waf_severity, waf_reason, plus standard nginx fields. Ingest directly into Elastic, Loki, Splunk, or Datadog with zero Grok parsing.
  • Graceful shutdown: 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⁠.


⁠Performance & accuracy

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:

ScenarioWAF rps · p50 · p99noWAF rps · p50 · p99overhead p50
GET / clean46,323 · 20 µs · 24 µs49,717 · 19 µs · 22 µs+1 µs
GET /?q=(2 KiB benign)36,049 · 25 µs · 30 µs41,205 · 22 µs · 25 µs+3 µs
POST JSON 250 B body48,531 · 19 µs · 23 µs54,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):

MetricResultMeaning
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 Score100.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.

⁠Honest GoTestWAF accounting

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.


⁠Under the hood

Three inline layers in a single nginx ACCESS-phase handler:

  1. Per-field decoder: HTML entities → URL %XX and JSON \uXXXX (two-pass) → base64 applied to URI, args, headers, cookies, Referer, and body.
  2. Semantic gates: structural XSS/SQLi tokenisers that catch context-aware bypasses signature layers miss.
  3. SIMD regex scan: Compiled signature DB evaluated in one pass over NUL-delimited request streams using AVX-512 (64-byte lanes), AVX2 (32-byte), or NEON (16-byte).

Rules compile into a binary bundle at image build time. No per-request regex compilation, no JIT warmup, no cold cache.


⁠Security posture

  • Distroless base: gcr.io/distroless/cc-debian13:nonroot — no shell, no package manager.
  • Non-root runtime: UID 65532, caps dropped to NET_BIND_SERVICE, no-new-privileges enforced. Read-only root filesystem with tmpfs for /var/cache/nginx and /run.
  • Zero fixable HIGH/CRITICAL CVEs: Trivy gates every publish pipeline.
  • Hardened binaries: Stripped, no debug info, no build paths.
  • Signed manifest: cosign-signed via Sigstore keyless OIDC. Zero phone-home outbound network calls.

⁠FAQ

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.


⁠Support

Discussions & Q&A: GitHub Discussions⁠.

Responsible disclosure of security reports: contact the maintainer privately. Please do not post security bugs on public forums.

Tag summary

Content type

Image

Digest

sha256:8711885ed…

Size

11.7 MB

Last updated

4 months ago

docker pull happensit/npx-waf