Installation and configuration HAProxy v2.4.22 with WAF Coraza SPOA on Ubuntu Server 22.04 LTS

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.

3 thoughts on “Installation and configuration HAProxy v2.4.22 with WAF Coraza SPOA on Ubuntu Server 22.04 LTS”

  1. 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.

    Reply
    • 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.

      Reply

Leave a Comment