WAF
    OWASP CRS

    Deploy Coraza WAF on a VPS

    Deploy the Coraza web application firewall on a RamNode VPS via Caddy with the coraza-caddy plugin and the OWASP Core Rule Set.

    Target stack: Ubuntu 24.04 LTS, Coraza WAF via the Caddy web server (coraza-caddy plugin) with the OWASP Core Rule Set (CRS).

    Coraza is a Go library that implements a ModSecurity-compatible Web Application Firewall engine. It has no standalone daemon of its own — it's embedded into a proxy. The most straightforward production path is Caddy, since the coraza-caddy plugin is actively maintained and Caddy handles TLS automatically. (If you already run Nginx or HAProxy, see the "Alternative integrations" section at the end.)


    1. Prerequisites

    • A RamNode VPS running Ubuntu 24.04 LTS, with a sudo user configured
    • A domain name pointed at the VPS's public IP (A/AAAA record)
    • Ports 80 and 443 open
    shell
    sudo apt update && sudo apt upgrade -y
    sudo apt install -y curl git ufw

    Set up basic firewall rules:

    shell
    sudo ufw allow OpenSSH
    sudo ufw allow 80/tcp
    sudo ufw allow 443/tcp
    sudo ufw enable

    2. Install Go (build dependency)

    Caddy with custom plugins is built using xcaddy, which needs Go 1.22+.

    shell
    cd /tmp
    curl -LO https://go.dev/dl/go1.22.6.linux-amd64.tar.gz
    sudo rm -rf /usr/local/go
    sudo tar -C /usr/local -xzf go1.22.6.linux-amd64.tar.gz
    echo 'export PATH=$PATH:/usr/local/go/bin:$HOME/go/bin' >> ~/.bashrc
    source ~/.bashrc
    go version

    3. Install xcaddy and build Caddy with Coraza

    shell
    go install github.com/caddyserver/xcaddy/cmd/xcaddy@latest
    
    xcaddy build \
      --with github.com/corazawaf/coraza-caddy/v2
    
    sudo mv caddy /usr/local/bin/caddy
    sudo setcap cap_net_bind_service=+ep /usr/local/bin/caddy
    caddy version

    This produces a single Caddy binary with the Coraza module compiled in.

    4. Create a caddy system user and directories

    shell
    sudo groupadd --system caddy
    sudo useradd --system \
      --gid caddy \
      --create-home \
      --home-dir /var/lib/caddy \
      --shell /usr/sbin/nologin \
      caddy
    
    sudo mkdir -p /etc/caddy /etc/coraza
    sudo chown -R caddy:caddy /var/lib/caddy

    5. Fetch the OWASP Core Rule Set

    shell
    cd /etc/coraza
    sudo git clone --depth 1 https://github.com/corazawaf/coraza-coreruleset.git crs
    sudo cp crs/rules/*.conf-example crs/rules/ 2>/dev/null || true

    Coraza's own base config lives in the coraza-coreruleset repo's crs-setup.conf.example file — copy it and adjust as needed:

    shell
    sudo cp crs/crs-setup.conf.example /etc/coraza/crs-setup.conf

    6. Write the Caddyfile

    shell
    sudo tee /etc/caddy/Caddyfile > /dev/null <<'EOF'
    example.com {
        route {
            coraza_waf {
                load_owasp_crs
                directives `
                    Include @coraza.conf-recommended
                    Include @crs-setup.conf.example
                    Include @owasp_crs/*.conf
                    SecRuleEngine On
                `
            }
            reverse_proxy localhost:8080
        }
    }
    EOF

    Replace example.com with your domain and localhost:8080 with your actual backend application. Caddy will automatically obtain and renew a Let's Encrypt certificate for the domain.

    SecRuleEngine On blocks matching requests. Use DetectionOnly for a "log but don't block" trial period while you tune false positives.

    7. Create a systemd service

    shell
    sudo tee /etc/systemd/system/caddy.service > /dev/null <<'EOF'
    [Unit]
    Description=Caddy with Coraza WAF
    After=network.target
    
    [Service]
    User=caddy
    Group=caddy
    ExecStart=/usr/local/bin/caddy run --environ --config /etc/caddy/Caddyfile
    ExecReload=/usr/local/bin/caddy reload --config /etc/caddy/Caddyfile
    TimeoutStopSec=5s
    LimitNOFILE=1048576
    PrivateTmp=true
    ProtectSystem=full
    AmbientCapabilities=CAP_NET_BIND_SERVICE
    Restart=on-failure
    
    [Install]
    WantedBy=multi-user.target
    EOF
    
    sudo chown -R caddy:caddy /etc/caddy /etc/coraza
    sudo systemctl daemon-reload
    sudo systemctl enable --now caddy
    sudo systemctl status caddy

    8. Verify

    shell
    curl -I https://example.com
    # Trigger a rule (should be blocked/logged if CRS is active):
    curl "https://example.com/?id=1' OR '1'='1"

    Check the log for a blocked request:

    shell
    sudo journalctl -u caddy -f

    9. Tuning false positives

    1. Run with SecRuleEngine DetectionOnly for a few days under real traffic.
    2. Watch logs for id:XXXXXX rule hits against legitimate requests.
    3. Add exclusions in a rules file loaded after the CRS, e.g.:
    shell
    SecRuleUpdateTargetById 942100 "!ARGS:some_free_text_field"
    1. Only switch to On once the noise is manageable.

    10. Updating rules

    shell
    cd /etc/coraza/crs
    sudo git pull
    sudo systemctl reload caddy

    Alternative integrations

    • Nginx: Coraza has no official Nginx module; the community path is running Coraza behind Nginx as an upstream via the standalone coraza-caddy/Envoy setups above, or using modsecurity-nginx (actual ModSecurity, not Coraza) if you specifically need Nginx-native integration.
    • Envoy: use the coraza-proxy-wasm filter, compiled as a Wasm plugin and loaded via Envoy's http_filters.
    • HAProxy: use coraza-spoa, a standalone SPOE agent that HAProxy talks to over a Unix socket — install it as its own systemd service and add an SPOE filter block to haproxy.cfg.

    If your stack is Nginx or HAProxy rather than Caddy, let me know and I can write a guide tailored to that.