Skip to main content

Prerequisites

Before installing Coraza Proxy, ensure you have:
  • Go 1.24+ (for building from source)
  • Docker (for containerized deployment)
  • Git (for cloning dependencies)
  • Linux/macOS (Windows via WSL2)
Coraza Proxy requires Go 1.24.7 or higher due to dependencies in the Coraza v3 library.

Installation Methods

Configuration

Environment Variables

Coraza Proxy is configured entirely through environment variables:

Core Settings

VariableTypeDefaultDescription
PORTinteger8081Port to listen on
BACKENDSJSONSee belowBackend routing configuration
CORAZA_RULES_PATH_SITESpathsPL1 configColon-separated rule file paths for web sites
CORAZA_RULES_PATH_APISpathsPL2 configColon-separated rule file paths for APIs

Host Classification

# Hosts using web site rules (PL1)
PROXY_WEB_HOSTS=web.example.com,www.example.com

# Hosts using API rules (PL2)
PROXY_APIS_HOSTS=api.example.com,v1.example.com
Each host must be classified as either WEB or API. Unclassified hosts will return WAF not configured for this host errors.

Rate Limiting

# Requests per second per IP
PROXY_RATE_LIMIT=5

# Maximum burst size
PROXY_RATE_BURST=10
The rate limiter:
  • Tracks IPs separately with automatic cleanup after 3 minutes of inactivity
  • Uses token bucket algorithm from golang.org/x/time/rate
  • Returns 429 Too Many Requests when exceeded

Bot Protection

# Enable bot blocking
PROXY_BLOCK_BOTS=true

# Comma-separated User-Agent patterns to block (case-insensitive)
PROXY_BOTS=python,Googlebot,Bingbot,Slurp,DuckDuckBot,yandex,YandexBot,Sogou,baiduspider
Bot blocking happens before WAF inspection, reducing processing overhead for bot traffic.

GeoIP Filtering

# Enable GeoIP-based blocking
GEO_BLOCK_ENABLED=true

# ISO country codes to allow (empty = allow all)
GEO_ALLOW_COUNTRIES=US,CA,GB,DE,FR

# ISO country codes to block (checked first)
GEO_BLOCK_COUNTRIES=CN,RU,KP
GeoIP Database Setup:
  1. Download MaxMind GeoLite2 database
  2. Place at /app/GeoLite2-Country.mmdb in container
  3. Or mount via volume: -v /path/to/GeoLite2-Country.mmdb:/app/GeoLite2-Country.mmdb:ro
GeoIP lookups are performed from main.go:373 using the geoip2-golang library. Download the free database from MaxMind.

Backend Configuration

The BACKENDS variable supports two formats:

Simple Format (Legacy)

{
  "web.example.com": ["web:80"],
  "api.example.com": ["api:8000"],
  "default": ["fallback:80"]
}
With path-based routing:
{
  "example.com": {
    "default": ["web:80"],
    "paths": {
      "/api": ["api-server:8000"],
      "/static": ["cdn:80"],
      "/admin": ["admin-panel:3000"]
    }
  }
}
Path Matching Logic (main.go:317-341):
  1. Longest prefix match wins (/api/v2 matches /api not /)
  2. Falls back to default if no path matches
  3. Multiple backends in array enables round-robin load balancing
{
  "app.example.com": {
    "default": ["web1:80", "web2:80", "web3:80"]
  }
}

Rule Configuration

PL1 - Web Sites Profile

Default path: profiles/pl1-crs-setup.conf
CORAZA_RULES_PATH_SITES="
  profiles/coraza.conf:
  profiles/pl1-crs-setup.conf:
  coreruleset/rules/*.conf
"
Configuration highlights (profiles/pl1-crs-setup.conf):
# Paranoia Level 1 - Balanced security
SecAction "id:900000,phase:1,pass,nolog,
  setvar:tx.blocking_paranoia_level=1,
  setvar:tx.detection_paranoia_level=1,
  setvar:tx.inbound_anomaly_score_threshold=5,
  setvar:tx.outbound_anomaly_score_threshold=4"

# Allowed methods for web sites
SecAction "id:900200,phase:1,pass,nolog,
  setvar:tx.allowed_methods=GET HEAD POST OPTIONS"

# Content types for HTML sites
SecAction "id:900220,phase:1,pass,nolog,
  setvar:tx.allowed_request_content_type=
  |application/x-www-form-urlencoded|
  |multipart/form-data|
  |text/html|
  |application/json|
  |text/css|
  |image/|"

# Skip inspection for static files (performance optimization)
SecRule REQUEST_URI "@rx \.(ico|png|jpg|jpeg|gif|svg|css|js|woff2?)$" \
  "id:100001,phase:1,pass,nolog,ctl:ruleRemoveById=920100-920499"

PL2 - APIs Profile

Default path: profiles/pl2-crs-setup.conf
CORAZA_RULES_PATH_APIS="
  profiles/coraza.conf:
  profiles/pl2-crs-setup.conf:
  coreruleset/rules/REQUEST-901-INITIALIZATION.conf:
  coreruleset/rules/REQUEST-920-PROTOCOL-ENFORCEMENT.conf:
  coreruleset/rules/REQUEST-930-APPLICATION-ATTACK-LFI.conf:
  coreruleset/rules/REQUEST-934-APPLICATION-ATTACK-GENERIC.conf:
  coreruleset/rules/REQUEST-941-APPLICATION-ATTACK-XSS.conf:
  coreruleset/rules/REQUEST-942-APPLICATION-ATTACK-SQLI.conf
"
Why fewer rules for APIs?
  • APIs typically don’t serve HTML/CSS/JS
  • Focused on data injection attacks (SQLi, XSS in JSON)
  • Reduces false positives from API-specific patterns
  • Better performance with fewer rule evaluations

Base Coraza Configuration

From profiles/coraza.conf:
SecRuleEngine On

SecRequestBodyAccess On
SecRequestBodyLimit 13107200         # 12.5MB max request
SecRequestBodyInMemoryLimit 131072   # 128KB in-memory buffer
SecRequestBodyLimitAction Reject

SecResponseBodyAccess Off            # Prevent RDoS

SecDataDir /tmp/

SecAuditEngine RelevantOnly          # Only log blocked/flagged requests
SecAuditLogFormat JSON
SecAuditLogParts ABIJDEFHZ
SecAuditLog /tmp/log/coraza/audit.log

SecDebugLogLevel 0                   # Set to 3+ for debugging
SecResponseBodyAccess Off is critical for production. Response body inspection can cause Reverse Denial of Service if attackers send requests for large files.

Docker Compose Setup

Complete production-ready stack:
docker-compose.yml
version: '3.8'

services:
  coraza-proxy:
    build: .
    image: wafsec:local
    ports:
      - "8081:8081"
    environment:
      - PORT=8081
      - BACKENDS={"web.example.com":["web:80"], "api.example.com":["api:8000"]}
      - PROXY_WEB_HOSTS=web.example.com
      - PROXY_APIS_HOSTS=api.example.com
      - PROXY_RATE_LIMIT=10
      - PROXY_RATE_BURST=20
      - PROXY_BLOCK_BOTS=true
      - GEO_BLOCK_ENABLED=false
    volumes:
      - ./profiles:/app/profiles:ro
      - waf-logs:/tmp/log/coraza
    restart: unless-stopped
    networks:
      - frontend
      - backend
    depends_on:
      - web
      - api

  web:
    image: nginx:alpine
    networks:
      - backend

  api:
    image: your-api:latest
    networks:
      - backend

volumes:
  waf-logs:

networks:
  frontend:
  backend:
    internal: true

IP Address Detection

Coraza Proxy extracts the real client IP from multiple sources (main.go:249-262):
func realClientIP(r *http.Request) string {
    // Cloudflare proxy
    if cf := r.Header.Get("CF-Connecting-IP"); cf != "" {
        return cf
    }
    // Standard proxy header (first IP in chain)
    if xff := r.Header.Get("X-Forwarded-For"); xff != "" {
        parts := strings.Split(xff, ",")
        return strings.TrimSpace(parts[0])
    }
    // Direct connection
    host, _ := splitHostPort(r.RemoteAddr)
    return host
}
Priority:
  1. CF-Connecting-IP (Cloudflare)
  2. X-Forwarded-For (first IP only)
  3. RemoteAddr (direct connection)
Ensure your load balancer sets X-Forwarded-For correctly, or attackers can bypass rate limiting by spoofing IPs.

Logging & Monitoring

Audit Logs

JSON format logs to /tmp/log/coraza/audit.log:
{
  "transaction": {
    "timestamp": "2026-03-04T12:34:56Z",
    "client_ip": "203.0.113.42",
    "request": {
      "method": "GET",
      "uri": "/?id=1' OR '1'='1",
      "headers": {...}
    },
    "response": {
      "status": 403
    },
    "messages": [
      {
        "rule_id": "942100",
        "message": "SQL Injection Attack Detected"
      }
    ]
  }
}

Debug Logs

Enable with SecDebugLogLevel 3 in coraza.conf:
SecDebugLogLevel 3
SecDebugLog /tmp/log/coraza/debug.log
Levels:
  • 0 - None (production default)
  • 1 - Errors only
  • 3 - Warnings + errors
  • 9 - Full verbose (every rule evaluation)
Level 9 generates massive logs. Only use temporarily for troubleshooting specific issues.

Log Rotation

For production, use logrotate:
/etc/logrotate.d/coraza
/tmp/log/coraza/*.log {
    daily
    rotate 30
    compress
    delaycompress
    notifempty
    create 0644 coraza coraza
    postrotate
        docker exec coraza-proxy kill -USR1 1
    endscript
}

Performance Tuning

Request Body Limits

Adjust in coraza.conf:
SecRequestBodyLimit 13107200          # 12.5MB (default)
SecRequestBodyInMemoryLimit 131072    # 128KB (default)
Guidelines:
  • File upload endpoints: Increase SecRequestBodyLimit to 50MB+
  • API endpoints: Keep at 1-5MB
  • In-memory limit: Max 1MB to prevent memory exhaustion

Rate Limiting

Balance user experience with protection:
# Strict (API/backend)
PROXY_RATE_LIMIT=5
PROXY_RATE_BURST=10

# Moderate (web apps)
PROXY_RATE_LIMIT=10
PROXY_RATE_BURST=20

# Lenient (public content)
PROXY_RATE_LIMIT=20
PROXY_RATE_BURST=50

Paranoia Levels

Adjust in your CRS setup files:
# Level 1 - Production balanced (recommended)
setvar:tx.blocking_paranoia_level=1

# Level 2 - Increased security, some false positives
setvar:tx.blocking_paranoia_level=2

# Level 3-4 - Maximum security, high false positive rate
setvar:tx.blocking_paranoia_level=3
Start at PL1 and monitor for attacks. Only increase if you’re seeing successful exploits. Each level approximately doubles rule coverage and false positive rate.

Troubleshooting

Check WAF Status

# View startup logs
docker logs coraza-proxy | grep -E "(WAF started|Listening)"

# Expected output:
GeoIP database loaded
Coraza WAF started
Listening on :8081

Test Rule Loading

Verify rules are loaded:
# Send a known-bad request
curl -v "http://localhost:8081/?id=1'%20OR%201=1" \
  -H "Host: waf.test.local"

# Should return 403 with "Request blocked by WAF"

Backend Connection Issues

# Test backend connectivity from container
docker exec coraza-proxy wget -O- http://web:80

# Check DNS resolution
docker exec coraza-proxy nslookup web

Common Error Messages

Cause: No matching backend in BACKENDS for the requested hostSolution: Add host to BACKENDS JSON or ensure default backend is defined
{"default": ["fallback:80"]}
Cause: Host not in PROXY_WEB_HOSTS or PROXY_APIS_HOSTSSolution: Add to appropriate environment variable:
PROXY_WEB_HOSTS=waf.test.local,example.com
Cause: Rule files not found or syntax errorsSolution:
  • Verify rule paths exist: ls /app/coreruleset/rules/
  • Check for syntax errors in custom rules
  • Ensure OWASP CRS was cloned during build
Cause: GeoLite2 database missing but GEO_BLOCK_ENABLED=trueSolution:
  • Download database and mount it
  • Or disable: GEO_BLOCK_ENABLED=false

Security Considerations

Do not expose Coraza Proxy directly to the internet without:
  • TLS termination (use nginx/Caddy/Cloudflare in front)
  • Proper firewall rules
  • Regular rule updates from OWASP CRS
  • Log monitoring and alerting

Production Checklist

  • TLS termination configured upstream
  • Rate limiting tuned for expected traffic
  • GeoIP filtering enabled (if applicable)
  • Bot protection configured
  • Audit logs collected and monitored
  • Log rotation configured
  • Backend health checks implemented
  • Paranoia level tested with legitimate traffic
  • Custom exclusion rules documented
  • Incident response plan for false positives

Next Steps

WAF Rules

Configure WAF rules and protection levels

Monitoring

Set up observability and alerting

Build docs developers (and LLMs) love