Securing web applications with Coraza WAF and Wazuh

| by | Wazuh 4.14.7
Post icon

Web application firewalls (WAFs) secure web applications by inspecting HTTP traffic and blocking malicious requests before they reach backend services. They help protect applications from attacks such as SQL injection, cross-site scripting (XSS), path traversal, command injection, and other threats covered by the OWASP Core Rule Set (CRS).

Coraza is an open source WAF engine written in Go that provides ModSecurity-compatible rule processing and supports the OWASP CRS. It integrates with reverse proxies, web servers, and application frameworks to inspect HTTP traffic and enforce security rules before it reaches an application. Wazuh complements Coraza by collecting and analyzing WAF audit events, correlating them with other security telemetry, and generating events for malicious requests and configuration changes.

This blog post shows how to monitor Coraza WAF activity with Wazuh. We use Caddy as the reverse proxy and the coraza-caddy module to add Coraza inspection. In this setup, Caddy runs on the WAF server, Coraza inspects incoming requests before Caddy forwards allowed traffic to the DVWA web server, and Wazuh collects Coraza audit events. We also create custom Wazuh rules to detect web attacks, blocked requests, repeated activity from the same source, and WAF configuration changes.

Infrastructure

We use the following infrastructure to demonstrate this capability:

  • A prebuilt, ready-to-use Wazuh OVA 4.14.7, which includes the Wazuh central components (Wazuh server, Wazuh indexer, and Wazuh dashboard). Follow this guide to download and set up the Wazuh virtual machine. 
  • An Ubuntu 24.04 endpoint that acts as the WAF server, installed with:
    • The Wazuh agent 4.14.7 installed and enrolled in the Wazuh server.
    • The Go toolchain installed.
    • Caddy built with the coraza-caddy module.
  • An Ubuntu 24.04 endpoint with DVWA installed. This endpoint is the web server.
  • Any Linux endpoint with curl installed, acting as the attacker endpoint.

How it works

The diagram below shows how Coraza and Wazuh work together in this integration.

Figure 1: Overview of the setup.
  1. A client sends an HTTPS request to access the web application through Caddy, which is running as the WAF server. Caddy terminates TLS, so Coraza inspects the decrypted requests.
  2. Coraza runs as middleware inside Caddy. It inspects the incoming request before Caddy forwards the request to the backend web server.
  3. Coraza evaluates the request against the OWASP CRS. Matching CRS rules contribute to the request anomaly score.
  4. If the request does not reach the configured blocking threshold, Caddy forwards it to the web server.
  5. If the request reaches the configured blocking threshold, Coraza interrupts the transaction, and Caddy returns an HTTP 403 response to the client. 
  6. Coraza records transaction details and matched-rule messages in the audit log.
  7. The Wazuh agent reads the audit logs and forwards the records to the Wazuh server for processing. It also monitors /etc/caddy for WAF configuration changes.

Configuration

We configure the WAF server to run Caddy with Coraza, collect the Coraza audit log, and monitor the WAF configuration. We then add custom detection rules on the Wazuh server.

WAF server

Follow these steps on the WAF server.

Install Caddy with Coraza

Coraza requires an HTTP server connector to inspect traffic. The coraza-caddy module integrates Coraza as Caddy HTTP middleware and embeds the OWASP CRS in the custom Caddy binary. We use xcaddy to build a custom Caddy binary with the coraza-caddy module, then install it at /usr/local/bin/caddy.

  1. Add the Go binary directory to the shell PATH:
# export PATH=$PATH:$(go env GOPATH)/bin
  1. Install xcaddy and build Caddy with the coraza-caddy module:
# go install github.com/caddyserver/xcaddy/cmd/xcaddy@v0.4.7
# xcaddy build --with github.com/corazawaf/coraza-caddy/v2 --output /tmp/caddy
# install -m 0755 /tmp/caddy /usr/local/bin/caddy
  1. Confirm that the binary includes the WAF handler:
# /usr/local/bin/caddy list-modules | grep -i waf
http.handlers.waf

Configure Coraza

  1. Create the audit log and the Caddy configuration directories:
# mkdir -p /var/log/coraza /etc/caddy
  1. Create the /etc/caddy/Caddyfile configuration file and add the following content. Replace <WEB_SERVER_IP> with the IP address of the web server:
{
    order coraza_waf first
}

https://dvwa {
    tls internal

    coraza_waf {
        load_owasp_crs
        directives `
        Include @coraza.conf-recommended
        Include @crs-setup.conf.example
        Include @owasp_crs/*.conf

        SecRuleEngine On
        SecAuditEngine RelevantOnly
        SecAuditLog /var/log/coraza/audit.json
        SecAuditLogType Serial
        SecAuditLogFormat JSON
        SecAuditLogParts ABCFKZ
        `
    }

    reverse_proxy <WEB_SERVER_IP>:80
}

Where:

  • https://dvwa is the hostname Caddy listens on.
  • order coraza_waf first makes Coraza inspect requests before Caddy proxies them.
  • tls internal lets Caddy serve HTTPS using its internal CA. 
  • load_owasp_crs loads the OWASP Core Rule Set. 
  • SecRuleEngine On enables Coraza blocking mode.
  • reverse_proxy <WEB_SERVER_IP>:80 tells Caddy where to send requests after Coraza inspects them.

Note

In this blog post, https://dvwa is the URL used to access DVWA through the WAF server. It is not a public domain name. Configure the attacker endpoint, and any other client used for testing, to resolve dvwa to the WAF server’s IP address.

  1. Create the caddy user and group:
# groupadd --system caddy
# useradd --system --gid caddy --create-home --home-dir /var/lib/caddy \
    --shell /usr/sbin/nologin --comment "Caddy web server" caddy
  1. Set the ownership and permissions for the Caddy configuration file and Coraza audit log directory:
# chown -R caddy:caddy /var/log/coraza
# chown root:caddy /etc/caddy/Caddyfile
# chmod 640 /etc/caddy/Caddyfile
  1. Create /etc/systemd/system/caddy.service and insert the following. This service file allows Linux to manage the Caddy binary, run it as the dedicated Caddy user, and start it automatically at boot:
[Unit]
Description=Caddy with OWASP Coraza WAF
Documentation=https://caddyserver.com/docs/
After=network.target network-online.target
Requires=network-online.target

[Service]
Type=notify
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 --force
TimeoutStopSec=5s
LimitNOFILE=1048576
PrivateTmp=true
ProtectSystem=full
AmbientCapabilities=CAP_NET_ADMIN CAP_NET_BIND_SERVICE

[Install]
WantedBy=multi-user.target
  1. Enable and start the Caddy service:
# systemctl daemon-reload
# systemctl enable --now caddy
  1. Create the /etc/logrotate.d/coraza file and add the following configuration. This sets up daily rotation for the Coraza audit log and retains a rolling 14-day history:
/var/log/coraza/audit.json {
    daily
    rotate 14
    missingok
    notifempty
    compress
    delaycompress
    copytruncate
    su caddy caddy
}

Configure the Wazuh agent log collection and FIM

  1. Add the following block within the <ossec_config> block of /var/ossec/etc/ossec.conf:
<localfile>
  <log_format>json</log_format>
  <location>/var/log/coraza/audit.json</location>
</localfile>
  1. Add the following block within the <syscheck> block of /var/ossec/etc/ossec.conf:
<directories check_all="yes" realtime="yes" report_changes="yes">/etc/caddy</directories>
  1. Restart the Wazuh agent to apply the changes:
# systemctl restart wazuh-agent

Web server

Perform the following steps to complete the DVWA setup.

  1. Browse to https://dvwa/DVWA/setup.php and select Create / Reset Database. This builds the required database tables for the DVWA application.
  2. Allow HTTP access only from the WAF server, using the Uncomplicated Firewall (UFW). Replace <WAF_SERVER_IP> with the IP address of the WAF server:
# ufw allow from <WAF_SERVER_IP> to any port 80 proto tcp
# ufw deny 80/tcp
# ufw enable

Wazuh dashboard

Perform the following steps on the Wazuh dashboard to configure the Wazuh server.

  1. Navigate to Server management > Rules, then click + Add new rules file.
  2. Copy and paste the rules below and name the file coraza_rules.xml. Click Save, then Reload to apply the changes:
<group name="coraza,waf,">

  <rule id="101800" level="0">
    <decoded_as>json</decoded_as>
    <location>coraza/audit.json$</location>
    <field name="transaction.is_interrupted">\.+</field>
    <description>Coraza WAF audit event</description>
    <options>no_full_log</options>
  </rule>

  <rule id="101801" level="6">
    <if_sid>101800</if_sid>
    <field name="messages" type="pcre2">attack-sqli</field>
    <description>Coraza WAF: SQL injection attempt against $(transaction.request.uri) from $(transaction.client_ip) (blocked: $(transaction.is_interrupted))</description>
    <mitre><id>T1190</id></mitre>
    <group>web_attack,sql_injection,</group>
  </rule>

  <rule id="101802" level="6">
    <if_sid>101800</if_sid>
    <field name="messages" type="pcre2">attack-xss</field>
    <description>Coraza WAF: Cross-site scripting attempt against $(transaction.request.uri) from $(transaction.client_ip) (blocked: $(transaction.is_interrupted))</description>
    <mitre><id>T1189</id></mitre>
    <group>web_attack,xss,</group>
  </rule>

  <rule id="101803" level="6">
    <if_sid>101800</if_sid>
    <field name="messages" type="pcre2">attack-lfi</field>
    <description>Coraza WAF: Path traversal or local file inclusion attempt against $(transaction.request.uri) from $(transaction.client_ip) (blocked: $(transaction.is_interrupted))</description>
    <mitre><id>T1190</id></mitre>
    <group>web_attack,path_traversal,</group>
  </rule>

  <rule id="101804" level="7">
    <if_sid>101800</if_sid>
    <field name="messages" type="pcre2">attack-rce</field>
    <description>Coraza WAF: Remote command execution attempt against $(transaction.request.uri) from $(transaction.client_ip) (blocked: $(transaction.is_interrupted))</description>
    <mitre><id>T1190</id></mitre>
    <group>web_attack,rce,</group>
  </rule>

  <rule id="101805" level="10" frequency="6" timeframe="60">
    <if_matched_group>web_attack</if_matched_group>
    <same_field>transaction.client_ip</same_field>
    <description>Coraza WAF: Multiple CRS detections from $(transaction.client_ip) in a short window, consistent with automated attack tooling</description>
    <mitre><id>T1595</id></mitre>
    <group>web_scan,recon,</group>
    <options>no_full_log</options>
  </rule>

  <rule id="101806" level="7">
    <if_sid>550</if_sid>
    <field name="file">^/etc/caddy</field>
    <description>Coraza WAF configuration file modified: $(file)</description>
    <mitre><id>T1562.001</id></mitre>
    <group>waf_config_change,</group>
  </rule>

  <rule id="101807" level="7">
    <if_sid>554</if_sid>
    <field name="file">^/etc/caddy</field>
    <description>Coraza WAF configuration file added: $(file)</description>
    <mitre><id>T1562.001</id></mitre>
    <group>waf_config_change,</group>
  </rule>

  <rule id="101808" level="10">
    <if_sid>553</if_sid>
    <field name="file">^/etc/caddy</field>
    <description>Coraza WAF configuration file deleted: $(file)</description>
    <mitre><id>T1562.001</id></mitre>
    <group>waf_config_change,</group>
  </rule>

</group>

Where:

  • Rule ID 101800 serves as the parent rule that matches Coraza audit records.
  • Rule ID 101801 is triggered when Coraza detects an SQL injection attempt.
  • Rule ID 101802 is triggered when Coraza detects a cross-site scripting attempt.
  • Rule ID 101803 is triggered when Coraza detects a path traversal or local file inclusion attempt.
  • Rule ID 101804 is triggered when Coraza detects an attempt to execute a remote command.
  • Rule ID 101805 is triggered when Wazuh correlates repeated attack-category alerts from the same source.
  • Rule ID 101806 is triggered when Wazuh detects a modified file in the /etc/caddy directory.
  • Rule ID 101807 is triggered when Wazuh detects a new file in the /etc/caddy directory.
  • Rule ID 101808 is triggered when Wazuh detects a deleted file in the /etc/caddy directory.

Attack simulation

Perform the following steps on the attacker endpoint.

Note

Configure the attacker endpoint to resolve dvwa to the WAF server’s IP address by adding the WAF IP and dvwa to /etc/hosts or using a DNS server.

Simulate SQL injection

Run the following command to generate an alert for rule 101801:

# B="https://dvwa/DVWA"
# curl -ik -G --data-urlencode "id=1' OR '1'='1" --data-urlencode "Submit=Submit" \
    "$B/vulnerabilities/sqli/"

Simulate cross-site scripting

Run the following command to generate an alert for rule 101802:

# curl -ik -G --data-urlencode "name=<script>alert(1)</script>" \
    "$B/vulnerabilities/xss_r/"

Simulate path traversal

Run the following command to generate an alert for rule 101803:

# curl -ik -G --data-urlencode "page=../../../../../../etc/hosts" \
    "$B/vulnerabilities/fi/"

Simulate command injection

Run the following command to generate an alert for rule 101804:

# curl -ik -X POST --data-urlencode "ip=;cat /etc/passwd" \
    --data-urlencode "Submit=Submit" "$B/vulnerabilities/exec/"

Simulate repeated attacks from one source

Run the following command to generate an alert for rule 101805:

# for i in 1 2; do
    curl -sk -G --data-urlencode "id=1' OR '1'='1" --data-urlencode "Submit=Submit" \
      -o /dev/null "$B/vulnerabilities/sqli/"
    curl -sk -G --data-urlencode "name=<script>alert(1)</script>" \
      -o /dev/null "$B/vulnerabilities/xss_r/"
    curl -sk -G --data-urlencode "page=../../../../../../etc/hosts" \
      -o /dev/null "$B/vulnerabilities/fi/"
    curl -sk -X POST --data-urlencode "ip=;cat /etc/passwd" \
      --data-urlencode "Submit=Submit" -o /dev/null "$B/vulnerabilities/exec/"
    sleep 2
  done

Simulate WAF configuration changes

An attacker who reaches the WAF server can weaken the ruleset or disable enforcement, which leaves the application exposed while the service still appears healthy. 

Run the following commands on the WAF endpoint to simulate WAF configuration changes. This generates an alert for rules 101806, 101807, and 101808.

# touch /etc/caddy/test.conf
# sleep 5
# echo "# comment" >> /etc/caddy/Caddyfile
# sleep 5
# rm /etc/caddy/test.conf

Detection results

Perform the following steps on the Wazuh dashboard to view the alerts generated during the tests:

  1. Navigate to Threat intelligence > Threat Hunting > Events.
  2. In the search bar, enter rule.groups:coraza, and click Update.
Figure 2: Coraza WAF alerts on the Wazuh dashboard.

Click Inspect document details on an alert to view the decoded Coraza event fields.

Figure 3: Detailed event.

Conclusion

In this blog post, we demonstrate how to monitor Coraza WAF activity with Wazuh. We configured Caddy with the coraza-caddy module to inspect requests to DVWA, enabled Coraza audit logging, and created custom Wazuh rules to detect common web attacks and WAF configuration changes.

By collecting and analyzing Coraza audit logs, Wazuh provides centralized visibility into suspicious web requests, blocked traffic, repeated attack activity, and WAF configuration changes. This helps security teams monitor web application protection activity and investigate potential attacks from the Wazuh dashboard.

Wazuh is a free and open source security platform with capabilities for threat detection, incident response, and compliance. If you have questions about this integration or Wazuh, join the Wazuh community, where the team and community members can assist you.

References