With the announcement of the end of development of ModSecurity in 2024, it is time to explore other alternatives.
The solution recommended by OWASP is Coraza, compatible with CRS v3 and v4 rules.
This guide aims to set up a waf on HAProxy v2.4.22 with a ready-to-use configuration, to get started for those who are not familiar with HAProxy and Waf. We will perform a basic installation of HAProxy and configure the Coraza SPOA based on Coraza WAF v3.0.1 module to filter web traffic using the OWASP ModSecurity Core Rule Set (CRS) v4.0.
We will set up a default site and enable the basic rules.
The multi-domain version of this guide is available here https://www.alldiscoveries.com/multidomain-installation-and-configuration-of-haproxy-with-waf-coraza-spoa-and-owasp-modsecurity-core-rule-set-4-0-wordpress-rule-exclusions-on-ubuntu-server-22-04-lts/
All pre-configured configuration files are available for download on my github at: https://github.com/thelogh/haproxy-coraza
I have also created an automatic installation script “install-coraza_basic.sh” which already contains all the commands and configurations present in the guide, download it, give it the execution permissions and run it.
First of all we always update our system.
Assuming you are root otherwise prepend the command sudo run the command:
apt-get update
apt-get upgrade -y
Let’s install the components to compile coraza-spoa and Git.
apt-get install pkg-config make gcc -y
apt-get install git -y
Install an updated version of Go programming language.
snap install go --classic
Let’s clone the coraza-spoa repository.
git clone https://github.com/corazawaf/coraza-spoa.git
We enter the cloned repository folder and launch the make compilation command.
cd ./coraza-spoa
make
Create a group and a coraza-spoe user
addgroup --quiet --system coraza-spoa
adduser --quiet --system --ingroup coraza-spoa --no-create-home --home /nonexistent --disabled-password coraza-spoa
Let’s create the root directory for the configuration
mkdir -p /etc/coraza-spoa
Create the directory for the logs.
mkdir -p /var/log/coraza-spoa /var/log/coraza-spoa/audit
Create the log files
touch /var/log/coraza-spoa/server.log /var/log/coraza-spoa/error.log \
/var/log/coraza-spoa/audit.log /var/log/coraza-spoa/debug.log
Copy the executable as soon as they create it into the bin directory.
cp -a ./coraza-spoa_amd64 /usr/bin/coraza-spoa
Set the permissions.
chmod 755 /usr/bin/coraza-spoa
I copy the default configuration file of coraza-spoa
cp -a ./config.yaml.default /etc/coraza-spoa/config.yaml
This is our main configuration file at the heart of Coraza WAF, where we will configure our default configuration (application) in case an already configured domain is not found, and our specific configurations for our domains (multiple application).
We edit the configuration file /etc/coraza-spoa/config.yaml to change the listening port instead of all the addresses, for security reasons we only set the localhost interface.
vim /etc/coraza-spoa/config.yaml
Change:
---bind: 0.0.0.0:9000
+++bind: 127.0.0.1:9000
Change the how of our default application from:
---default_application: sample_app
+++default_application: haproxy_waf
Create our default application
applications:
haproxy_waf:
# Get the coraza.conf from https://github.com/corazawaf/coraza
#
# Download the OWASP CRS from https://github.com/coreruleset/coreruleset/releases
# and copy crs-setup.conf & the rules, plugins directories to /etc/coraza-spoa
directives: |
Include /etc/coraza-spoa/coraza.conf
Include /etc/coraza-spoa/crs-setup.conf
Include /etc/coraza-spoa/plugins/*-config.conf
Include /etc/coraza-spoa/plugins/*-before.conf
Include /etc/coraza-spoa/rules/*.conf
Include /etc/coraza-spoa/plugins/*-after.conf
# HAProxy configured to send requests only, that means no cache required
# NOTE: there are still some memory & caching issues, so use this with care
no_response_check: true
# The transaction cache lifetime in milliseconds (60000ms = 60s)
transaction_ttl_ms: 60000
# The maximum number of transactions which can be cached
transaction_active_limit: 100000
# The log level configuration, one of: debug/info/warn/error/panic/fatal
log_level: info
# The log file path
log_file: /var/log/coraza-spoa/coraza-agent.log
If the coraza-spoa service does not start, and no errors appear in the logs, change the log path:
---log_file: /dev/stdout
+++log_file: /var/log/coraza-spoa/coraza-agent.log
Download the Coraza configuration file for SecRuleEngine
wget https://raw.githubusercontent.com/corazawaf/coraza/main/coraza.conf-recommended -O /etc/coraza-spoa/coraza.conf
Edit the file /etc/coraza-spoa/coraza.conf and enable the rules
---SecRuleEngine DetectionOnly
+++SecRuleEngine On
Create the directory containing our rules
mkdir -p ./coraza-crs
cd ./coraza-crs
Download the OWASP ModSecurity Core Rule Set version 4.0
git clone https://github.com/coreruleset/coreruleset
Copy the rules files
cp ./coreruleset/crs-setup.conf.example /etc/coraza-spoa/crs-setup.conf
cp -R ./coreruleset/rules /etc/coraza-spoa
cp -R ./coreruleset/plugins /etc/coraza-spoa
Configure the permissions
chown -R coraza-spoa:coraza-spoa /etc/coraza-spoa/
chmod 700 /etc/coraza-spoa
chmod -R 600 /etc/coraza-spoa/*
chmod 700 /etc/coraza-spoa/rules
chmod 700 /etc/coraza-spoa/plugins
Copy the service configuration file
cp -a ./contrib/coraza-spoa.service /lib/systemd/system/coraza-spoa.service
Restart the services daemon and enable coraza-spoa for automatic startup
systemctl daemon-reload
systemctl enable coraza-spoa.service
Now let’s move on to configuring the Haproxy service to integrate the configuration for coraza-spoa.
If you don’t already have haproxy, install it with the command:
apt-get install haproxy -y
Copy the spoa configuration file to the haproxy configuration folder
cp -a ./doc/config/coraza.cfg /etc/haproxy/coraza.cfg
We change the name of the configuration to use, both in spoe-message coraza-req and in spoe-message coraza-res, from args app=str(sample_app) to args app=str(haproxy_waf)
# https://github.com/haproxy/haproxy/blob/master/doc/SPOE.txt
# /etc/haproxy/coraza.cfg
[coraza]
spoe-agent coraza-agent
# Process HTTP requests only (the responses are not evaluated)
messages coraza-req
# Comment the previous line and add coraza-res, to process responses also.
# NOTE: there are still some memory & caching issues, so use this with care
#messages coraza-req coraza-res
option var-prefix coraza
option set-on-error error
timeout hello 2s
timeout idle 2m
timeout processing 500ms
use-backend coraza-spoa
log global
spoe-message coraza-req
args app=str(haproxy_waf) id=unique-id src-ip=src src-port=src_port dst-ip=dst dst-port=dst_port method=method path=path query=query version=req.ver headers=req.hdrs body=req.body
event on-frontend-http-request
spoe-message coraza-res
args app=str(haproxy_waf) id=unique-id version=res.ver status=status headers=res.hdrs body=res.body
event on-http-response
Important! Add a blank line at the end of the configuration file (/etc/haproxy/coraza.cfg) otherwise haproxy will fail with: /etc/haproxy/coraza.cfg:24]: Missing LF on last line, file might have been truncated at position 27.
If we don’t already have Haproxy configured we can use an example configuration file by copying it with:
cp -a ./doc/config/haproxy.cfg /etc/haproxy/haproxy.cfg
Important! Add a blank line at the end of the configuration file (/etc/haproxy/haproxy.cfg) otherwise haproxy will fail with: [/etc/haproxy/haproxy.cfg:48]: Missing LF on last line, file might have been truncated at position 29
# https://www.haproxy.com/documentation/hapee/latest/onepage/#home
global
log stdout format raw local0
defaults
log global
option httplog
timeout client 1m
timeout server 1m
timeout connect 10s
timeout http-keep-alive 2m
timeout queue 15s
timeout tunnel 4h # for websocket
frontend test
mode http
bind *:80
unique-id-format %[uuid()]
unique-id-header X-Unique-ID
filter spoe engine coraza config /etc/haproxy/coraza.cfg
# Currently haproxy cannot use variables to set the code or deny_status, so this needs to be manually configured here
http-request redirect code 302 location %[var(txn.coraza.data)] if { var(txn.coraza.action) -m str redirect }
http-response redirect code 302 location %[var(txn.coraza.data)] if { var(txn.coraza.action) -m str redirect }
http-request deny deny_status 403 hdr waf-block "request" if { var(txn.coraza.action) -m str deny }
http-response deny deny_status 403 hdr waf-block "response" if { var(txn.coraza.action) -m str deny }
http-request silent-drop if { var(txn.coraza.action) -m str drop }
http-response silent-drop if { var(txn.coraza.action) -m str drop }
# Deny in case of an error, when processing with the Coraza SPOA
http-request deny deny_status 504 if { var(txn.coraza.error) -m int gt 0 }
http-response deny deny_status 504 if { var(txn.coraza.error) -m int gt 0 }
use_backend test_backend
backend test_backend
mode http
http-request return status 200 content-type "text/plain" string "Welcome!\n"
backend coraza-spoa
mode tcp
balance roundrobin
timeout connect 5s # greater than hello timeout
timeout server 3m # greater than idle timeout
server s1 127.0.0.1:9000
Test the configuration with the command:
haproxy -c -f /etc/haproxy/haproxy.cfg
Start the coraza-spoa and haproxy service
systemctl start coraza-spoa
systemctl start haproxy
We verify that HAProxy is listening on port 80 and the coraza-spoa service on 9000 with the command: ss -ltpn
ss -ltpn
State Recv-Q Send-Q Local Address:Port Peer Address:Port Process
LISTEN 0 4096 127.0.0.1:9000 0.0.0.0:* users:(("coraza-spoa",pid=1182,fd=7))
LISTEN 0 4096 0.0.0.0:80 0.0.0.0:* users:(("haproxy",pid=1035,fd=6))
Connect via http to the machine’s IP with our browser http://My-Ip/, and we should see the “Welcome!” message.
With curl: curl -I http://My-Ip/
curl -I http://192.168.0.10/
HTTP/1.1 200 OK
content-length: 9
content-type: text/plain
In case the coraza-spoa service is not active, the directive in the configuration file (/etc/haproxy/haproxy.cfg)
http-request deny deny_status 504 if { var(txn.coraza.error) -m int gt 0 }
http-response deny deny_status 504 if { var(txn.coraza.error) -m int gt 0 }
will cause us to get a 504 error message. I recommend commenting it out for production.
Now we can finally test our WAF with haproxy
On our browser we add http://My-Ip/index.php?f=/etc/passwd after our IP address
With curl: curl -I http://My-Ip/index.php?f=/etc/passwd
curl -I http://192.168.0.10/index.php?f=/etc/passwd
HTTP/1.1 403 Forbidden
waf-block: request
content-length: 0
You will see that the Welcome! It will not appear, and we will get a 403 Forbidden response. Let’s check the logs in /var/log/coraza-spoa/coraza-agent.log
{"level":"warn","ts":1697397396.133613,"msg":"[client \"192.168.0.10\"] Coraza: Access denied (phase 1). Host header is a numeric IP address [file \"/etc/coraza-spoa/rules/REQUEST-920-PROTOCOL-ENFORCEMENT.conf\"] [line \"2441\"] [id \"920350\"] [rev \"\"] [msg \"Host header is a numeric IP address\"] [data \"192.168.0.10\"] [severity \"warning\"] [ver \"OWASP_CRS/4.0.0-rc1\"] [maturity \"0\"] [accuracy \"0\"] [tag \"application-multi\"] [tag \"language-multi\"] [tag \"platform-multi\"] [tag \"attack-protocol\"] [tag \"paranoia-level/1\"] [tag \"OWASP_CRS\"] [tag \"capec/1000/210/272\"] [tag \"PCI/6.5.10\"] [hostname \"192.168.0.10\"] [uri \"/index.php?f=/etc/passwd\"] [unique_id \"b16a843e-b8e7-43f9-b07d-f60c9e3a98c9\"]\n"}
Congratulations your Waf Coraza on HAProxy is up and running. Now all you have to do is fine-tune the rules. In the next article we will use a multi-domain configuration with different OWASP ModSecurity Core Rule Set rules. https://www.alldiscoveries.com/multidomain-installation-and-configuration-of-haproxy-with-waf-coraza-spoa-and-owasp-modsecurity-core-rule-set-4-0-wordpress-rule-exclusions-on-ubuntu-server-22-04-lts/
If you need help or want to give suggestions, feel free to contact me on My Linkedin profile https://www.linkedin.com/in/valerio-puglia-332873125/
Awesome Documentation, Helped a lot, Thanks
This tutorial is a godsend! Though I must add that the setup only worked when I changed the permissions to 750. The permissions you stated earlier (600 & 700) were throwing errors when I started the coraza-spoa service.
Thank you, I have found it helpful to share my study with others. Since it is something new, it can be complex at first. Strange that error on the permissions, I wanted to set it that way precisely to strengthen security more (perhaps an error in the group?). Currently I have them set as per the script. But the important thing is that it works. I’m waiting for the release of Ubuntu 24.04 to update the guide.
in My Case permission 750 also didn’t work, 755 worked!
Outstanding document. So great when someone takes the time to document something like this. I am new to HAProxy and UBuntu and could not have figured this all out. Having a detailed guide like this was just amazing. Many thanks. Incidentally, I ran through this a couple of times and ran into the permissions issues others have stated. Skipped that step on my final run and everything worked perfectly from there. Curious here, is the ruleset updated all the time? Should I set up something to download it daily and how would I go about doing that? Also, whats needed to update the SPOA? Run the make steps again and copy the executable again? Thanks again for this. My hero.
Hi I’m glad it was useful, not having found anything I decided to write a guide to help others who like me were looking for an alternative to commercial WAFs.
With the script you can install the latest version and compile it, there are no alternatives to automatically install the latest version of Spoa Coraza at the moment unfortunately.
Yes, modsec updates with various releases when I created the script they were at v4.0.0, now at v4.7.0. Here too the only thing is to create a script that updates them