A production-ready Web Application Firewall (WAF) implementation using ModSecurity 3.x with NGINX, containerized with Docker for easy deployment and management.
- ModSecurity 3.x with OWASP Core Rule Set (CRS)
- NGINX as reverse proxy with security hardening
- TLS/SSL encryption with auto-generated certificates
- Custom rule support with hot-reload capability
- Rate limiting and DDoS protection
- Security headers automatically configured
- Comprehensive logging (access, error, and audit logs)
- Custom error pages with modern UI
- Docker containerization for portability
βββββββββββββββ ββββββββββββββββ βββββββββββββββ
β Client ββββββββββΆβ NGINX WAF ββββββββββΆβ Backend β
β β HTTPS β + ModSecurityβ HTTP β Applicationβ
βββββββββββββββ ββββββββββββββββ βββββββββββββββ
β
βΌ
ββββββββββββ
β Logs β
β & Audit β
ββββββββββββ
modsecurity-nginx-docker/
βββ cert-gen.sh # SSL certificate generation script
βββ docker-compose.yml # Docker orchestration file
βββ Dockerfile # NGINX + ModSecurity container
βββ watcher.sh # Auto-reload script for rule changes
βββ update-crs.sh # OWASP CRS update utility
βββ README.md # This file
β
βββ modsec-data/ # ModSecurity configuration directory
β βββ nginx.conf # Main NGINX configuration
β βββ modsecurity.conf # ModSecurity core configuration
β βββ custom-rules.conf # Your custom ModSecurity rules
β β
β βββ conf.d/ # NGINX server blocks
β β βββ node.conf # Backend proxy configuration
β β
β βββ crs/ # OWASP Core Rule Set
β β βββ crs-setup.conf # CRS configuration
β β βββ rules/ # CRS rule files
β β βββ REQUEST-901-*.conf # Initialization rules
β β βββ REQUEST-9XX-*.conf # Attack detection rules
β β βββ RESPONSE-9XX-*.conf# Response inspection rules
β β βββ *.data # Supporting data files
β β
β βββ html/ # Custom error pages
β β βββ custom_403.html # Blocked request page
β β βββ tailwind.css # Styling
β β
β βββ logs/ # Log files (generated at runtime)
β β βββ access.log # HTTP access logs
β β βββ error.log # NGINX error logs
β β βββ audit.log # ModSecurity audit logs
β β
β βββ ssl/ # TLS certificates
β βββ cert.pem # Certificate
β βββ privkey.pem # Private key
β
βββ nodeapp/ # Example backend application
βββ Dockerfile
βββ app.js
git clone https://github.com/susuomlu/modsecurity-nginx-docker.git
cd modsecurity-nginx-docker/chmod +x cert-gen.sh
./cert-gen.shdocker compose -f docker-compose.yml build --no-cachedocker compose -f docker-compose.yml up -d# Check container status
docker ps -a
# View logs
docker compose logs -f
# Test HTTPS access
curl -k https://localhost:8443Edit modsec-data/modsecurity.conf:
# Enable ModSecurity
SecRuleEngine On
# Set paranoia level (1-4, higher = more strict)
SecAction "id:900000,phase:1,nolog,pass,t:none,setvar:tx.paranoia_level=2"
# Logging
SecAuditEngine RelevantOnly
SecAuditLog /var/log/modsec/audit.logEdit modsec-data/nginx.conf for global settings:
# Worker processes
worker_processes auto;
# Rate limiting zone
limit_req_zone $binary_remote_addr zone=waf_limit:10m rate=10r/s;Edit modsec-data/conf.d/node.conf for backend configuration:
server {
listen 8443 ssl http2;
server_name _;
# Backend proxy
location / {
proxy_pass http://nodeapp:3000;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
}
}Add custom ModSecurity rules in modsec-data/custom-rules.conf:
# Block specific parameter
SecRule ARGS:testparam "@streq test" \
"id:10001,phase:2,deny,log,status:403,msg:'Test parameter blocked'"
# Block curl User-Agent
SecRule REQUEST_HEADERS:User-Agent "@contains curl" \
"id:10002,phase:1,deny,log,status:403,msg:'Curl blocked'"
# Block SQL injection patterns
SecRule ARGS "@rx (?i)(union(.*?)select|select.+from)" \
"id:10003,phase:2,deny,log,status:403,msg:'SQLi attempt blocked'"
# Block XSS attempts
SecRule ARGS "@rx (?i)(<script|javascript:|onerror=)" \
"id:10004,phase:2,deny,log,status:403,msg:'XSS attempt blocked'"
# Block path traversal
SecRule REQUEST_URI "@rx (\.\./|\.\.\\)" \
"id:10005,phase:1,deny,log,status:403,msg:'Path traversal blocked'"Enable automatic rule reloading:
docker exec -it modsec /usr/local/bin/watcher.shOr manually reload:
docker compose restart- SQL Injection (SQLi)
- Cross-Site Scripting (XSS)
- Remote File Inclusion (RFI)
- Local File Inclusion (LFI)
- Remote Code Execution (RCE)
- Session Fixation
- Scanner Detection
- Protocol Violations
- Data Leakage Prevention
Automatically configured:
X-Frame-Options: DENY
X-Content-Type-Options: nosniff
X-XSS-Protection: 1; mode=block
Referrer-Policy: strict-origin-when-cross-origin
Permissions-Policy: geolocation=(), microphone=(), camera=()
- Default: 10 requests/second per IP
- Burst allowance: 20 requests
- Configurable per location
- TLS 1.2 and 1.3 only
- Strong cipher suites
- HTTP to HTTPS redirect
- HSTS enabled
# All logs
docker compose logs -f
# Access logs only
docker exec modsec tail -f /var/log/nginx/access.log
# ModSecurity audit logs
docker exec modsec tail -f /var/log/modsec/audit.log
# Error logs
docker exec modsec tail -f /var/log/nginx/error.logInside container:
- Access:
/var/log/nginx/access.log - Error:
/var/log/nginx/error.log - Audit:
/var/log/modsec/audit.log
On host (mounted):
modsec-data/logs/access.logmodsec-data/logs/error.logmodsec-data/logs/audit.log
curl -k https://localhost:8443curl -k "https://localhost:8443/?id=1' OR '1'='1"
# Expected: 403 Forbiddencurl -k "https://localhost:8443/?name=<script>alert('xss')</script>"
# Expected: 403 Forbiddencurl -k "https://localhost:8443/?testparam=test"
# Expected: 403 Forbiddencurl -k https://localhost:8443
# Expected: 403 Forbidden (curl is blocked by custom rule)docker compose up -ddocker compose downdocker compose restartdocker compose down
docker compose build --no-cache
docker compose up -dchmod +x update-crs.sh
./update-crs.sh
docker compose restartdocker exec -it modsec /bin/shHigher paranoia = more false positives but better security:
# In modsec-data/crs/crs-setup.conf
SecAction "id:900000,phase:1,nolog,pass,t:none,setvar:tx.paranoia_level=2"Levels:
- 1: Basic security (recommended for start)
- 2: Elevated security
- 3: High security (more false positives)
- 4: Maximum security (many false positives)
# In nginx.conf or server block
geo $whitelist {
default 0;
10.0.0.0/8 1;
192.168.1.100 1;
}
# In custom-rules.conf
SecRule REMOTE_ADDR "@ipMatch 192.168.1.100" \
"id:10100,phase:1,pass,nolog,ctl:ruleEngine=Off"# In custom-rules.conf
SecRuleRemoveById 942100
SecRuleRemoveByMsg "SQL Injection Attack"# Check what's using the port
sudo lsof -i :8443
# Change ports in docker-compose.yml
ports:
- "9443:8443" # For example, use 9443 instead# Fix log directory permissions
chmod -R 755 modsec-data/logs/- Review audit logs to identify problematic rules
- Temporarily disable the rule
- Whitelist legitimate patterns
- Lower paranoia level
- Use Valid SSL Certificates: Replace self-signed certs with Let's Encrypt or commercial certificates
- Configure Monitoring: Integrate with ELK, Splunk, or similar
- Regular Updates: Keep CRS and ModSecurity updated
- Tune Rules: Start with paranoia level 1, gradually increase
- Backup Configuration: Version control your custom rules
- Resource Limits: Set appropriate Docker resource constraints
- Network Segmentation: Place WAF in DMZ
- Regular Audits: Review logs and rules monthly
This project configuration is provided as-is for educational and production use.
Feel free to submit issues, fork the repository, and create pull requests for any improvements.
Note: Always test thoroughly in a staging environment before deploying to production.