A production-ready Docker-Compose stack that deploys an NGINX Web Application Firewall (WAF) powered by ModSecurity and the OWASP Core Rule Set (CRS), complete with observability via Prometheus and Grafana.
- Overview
- Features
- Project Structure
- Prerequisites
- Quick Start
- Testing the WAF
- Observability
- Configuration
- Maintenance
- Contributing
- License
- Contact
NGX-Shield provides a secure, configurable, and isolated environment for testing and deploying NGINX as a WAF. The stack includes:
- NGINX as a reverse proxy and WAF gateway
- ModSecurity with the OWASP CRS for real-time threat protection (CRS ships bundled inside the
owasp/modsecurity-crsimage, no separate download needed) - Prometheus for metrics collection and monitoring
- Grafana for visualization and alerting
- NGINX Exporter to expose NGINX metrics to Prometheus
- A simple Flask application for testing WAF functionality
This stack is ideal for learning WAF concepts, testing security rules, or as a starting point for production deployments.
- 🛡️ Real-time WAF Protection: Blocks common web exploits (SQLi, XSS, RCE, etc.) using OWASP CRS
- 📊 Full Observability: Metrics via Prometheus, dashboards via Grafana
- 🐳 Dockerized: Easy deployment with Docker Compose
- 🔧 Configurable: Simple to adjust rules, ports, and settings
- 📝 Detailed Logging: ModSecurity audit log, NGINX access/error logs
- 🧪 Testing Ready: Includes test application and verification steps
- 📚 Well Documented: Comprehensive guides for setup, testing, and extension
ngx-shield/
├── docker-compose.yml # Main compose file
├── nginx/ # NGINX configuration
│ ├── nginx.conf # Main NGINX config
│ ├── conf.d/
│ │ ├── waf.conf # WAF-specific site config (app proxy + stub_status)
│ │ ├── logging.conf # Log formats
│ │ └── modsecurity.conf # Enables the ModSecurity module
│ └── modsec/ # ModSecurity configuration (mounted as individual files,
│ ├── modsecurity.conf # not as a directory, so the image's bundled OWASP CRS
│ ├── modsecurity-override.conf # rules at /etc/modsecurity.d/owasp-crs/ stay intact)
│ └── setup.conf # Includes the conf files above + the bundled CRS rules
├── prometheus/ # Prometheus configuration
│ └── prometheus.yml
├── grafana/ # Grafana provisioning
│ └── provisioning/
│ ├── datasources/ds.yml
│ └── dashboards/
│ ├── dashboards.yml # Provider config that tells Grafana to load the JSON below
│ └── ngx-shield.json
├── logs/ # Bind-mounted logs directory
├── app/ # Test Flask application
│ ├── Dockerfile
│ └── app.py
└── README.md # This file- Docker Engine (v20.10+)
- Docker Compose (v2.0+)
- Approximately 2GB of free disk space
- Basic familiarity with terminal/command line
The OWASP CRS ships bundled inside the owasp/modsecurity-crs:nginx-alpine image at /etc/modsecurity.d/owasp-crs/ — there's nothing to clone or initialize separately.
docker compose up -dCheck that all containers are healthy:
docker compose psThe WAF is published on host port 8080 (see docker-compose.yml):
-
Normal request (should return 200):
curl -i http://localhost:8080/
-
SQL injection attempt (should be blocked with 403):
curl -i "http://localhost:8080/?id=1'+OR+1=1--"
- URL: http://localhost:3000
- Username:
admin - Password:
ngxshield - The NGX-Shield dashboard is automatically provisioned and displays key metrics (requests/sec, active connections, connection states, accepted vs. handled connections).
docker compose up -d app nginx-waf
# Test normal request and SQLi as above
docker compose stop app nginx-wafdocker compose up -d app nginx-waf nginx-exporter
curl -s http://localhost:9113/metrics | head -n 20
docker compose stop app nginx-waf nginx-exporterdocker compose up -d app nginx-waf nginx-exporter prometheus
# Visit http://localhost:9090, check target status, query nginx_http_requests_total
docker compose stop app nginx-waf nginx-exporter prometheusdocker compose up -d
# Visit http://localhost:3000, explore pre-built dashboardThe bundled dashboard only covers connection/request metrics from nginx-prometheus-exporter's stub_status scrape — per-status-code rates, blocked IPs, and triggered CRS rule IDs aren't available from stub_status. To get those, add Loki and Promtail to ingest modsec_audit.log:
-
Add to
docker-compose.yml:loki: image: grafana/loki:2.9.4 ports: ["3100:3100"] networks: [shield] promtail: image: grafana/promtail:2.9.4 volumes: - ./logs:/var/log/nginx:ro - ./promtail-config.yml:/etc/promtail/config.yml:ro networks: [shield]
-
Create
promtail-config.yml:server: http_listen_port: 9080 grpc_listen_port: 0 positions: filename: /tmp/positions.yaml clients: - url: http://loki:3100/loki/api/v1/push scrape_configs: - job_name: nginx static_configs: - targets: [localhost] labels: job: nginx __path__: /var/log/nginx/modsec_audit.log
-
Add a Loki data source in Grafana and extend
ngx-shield.jsonwith Loki-backed panels.
| Component | Host Port | Purpose |
|---|---|---|
| NGINX (WAF) | 8080 | WAF entry point (proxies to the app) |
| NGINX stub_status | 8081 | Internal metrics endpoint scraped by NGINX Exporter |
| Prometheus | 9090 | Metrics collection and querying |
| Grafana | 3000 | Visualization and alerting |
| NGINX Exporter | 9113 | Exposes NGINX metrics to Prometheus |
| Test app | 3001 | Direct access to the Flask test app (bypassing the WAF) |
- The OWASP CRS itself lives inside the image at
/etc/modsecurity.d/owasp-crs/— upgrade it by pulling a newerowasp/modsecurity-crsimage tag. - Core directives:
nginx/modsec/modsecurity.conf - Local overrides/custom rules: add directives to
nginx/modsec/modsecurity-override.conf - Include order:
nginx/modsec/setup.conf - To toggle between DetectionOnly and Prevention modes, edit
SecRuleEngineinnginx/modsec/modsecurity.conf
- Main config:
nginx/nginx.conf - WAF site:
nginx/conf.d/waf.conf(adjustproxy_passfor your application) - Log formats:
nginx/conf.d/logging.conf
- Prometheus scrape config:
prometheus/prometheus.yml - Grafana dashboards:
grafana/provisioning/dashboards/ - Data sources:
grafana/provisioning/datasources/ds.yml
# ModSecurity audit log (attack details)
tail -f logs/modsec_audit.log
# NGINX access log
tail -f logs/access.log
# NGINX error log
tail -f logs/error.log# View logs for all services
docker compose logs -f
# Restart specific service
docker compose restart nginx-waf
# Update the WAF image (also picks up a newer bundled OWASP CRS release)
docker compose pull nginx-waf
docker compose up -d nginx-waf- Always test rules in DetectionOnly mode first
- Regularly update the
owasp/modsecurity-crsimage to pick up newer CRS releases - Review logs for false positives and tune rules accordingly
- Consider rate limiting and IP reputation for production
- Use HTTPS in production (terminate at upstream or add certs to NGINX — TLS isn't configured in this stack by default)
We welcome contributions to improve NGX-Shield! Please follow these guidelines:
- Fork the repository
- Create a feature branch (
git checkout -b feature/AmazingFeature) - Commit your changes (
git commit -m 'Add some AmazingFeature') - Push to the branch (
git push origin feature/AmazingFeature) - Open a Pull Request
- Documentation improvements
- New feature requests (WAF rules, exporters, dashboards)
- Bug fixes
- Configuration examples
- Testing scenarios
Please ensure your contributions align with the project's goal of providing a production-ready, educational WAF stack.
This project is licensed under the MIT License - see the LICENSE file for details.
Note: The OWASP CRS bundled inside the owasp/modsecurity-crs image is licensed separately under the Apache License 2.0.
Project Link: https://github.com/sudoNaji/ngx-shield
For questions, suggestions, or security concerns, please open an issue on the GitHub repository.
Built with ❤️ for secure web applications