Using Caddy Webserver with OWASP Core Ruleset in Coraza

I felt like there was not a good, in depth write up out there for using Caddy, Coraza, and the OWASP Core Ruleset. I was very interersted in this trio but I had trouble getting it to work initially using one of the documents I found online, so I decided to dive deep into the topic and document it well so others can benifit from it as well. Next I am hoping to look into the rate limiting add ons, but for now, lets look at using Caddy, Coraza, and the OWASP Core Ruleset.


Prerequisites:

I have a Ubuntu Linux 22.04 VM on an Intel architecture. You will need to have at least 1gb of ram. I attempted an install on an ec2 instance with 512mb of ram and it would error out due to not having enough ram when compiling the server. In order to build the image the way that I did, I needed the current version of go installed (1.21.5) and xcaddy installed (Installation from Cloudsmith Repo)

  • Note: I will not be covering these installations, it is left as an exercise to the reader.
  • Note 2: do not install go from your distribution's source repository, it most likely will not work as it is not the current version of go.

Compiling Caddy with Coraza WAF: Xcaddy

after the prerequisites are installed, it is now time to compile the coraza/caddy image. To build the image, run the following xcaddy command:

xcaddy build --with github.com/corazawaf/coraza-caddy/v2

this will build the current version of caddy, along with the coraza plugin. If it executes correctly, the resulting executable, named caddy will be added to your current working directory. If you run the file command on it, it should say that it is an ELF.

Making Caddy Run on Your Machine as a Service

Here is the official caddy documentation for completing this task, I recommend reading it before proceeding (as this is where all this information comes from). To get caddy to run as a systemctl service in Linux, you need to perform the following tasks:

  • Move the caddy ELF to the appropriate /bin directory:
    sudo mv caddy /usr/bin/
  • Add a caddy user:
    
            sudo useradd --system \
            --gid caddy \
            --create-home \
            --home-dir /var/lib/caddy \
            --shell /usr/sbin/nologin \
            --comment "Caddy web server" \
            caddy
                        
  • Create the main caddy config file, the caddy file. You do not need to fill it out at this time
    sudo nano /etc/caddy/Caddyfile
    You can put a single commented line in the file for the moment. Caddy needs to have this file in order for it to run.
  • Add the system service file:
    sudo nano /etc/systemd/system/caddy.service
    Inside this file, place these contents
  • Save the service file, and run:
    sudo systemctl status caddy
    It should say that caddy is disabled.
  • Run the following commands:
    sudo systemctl enable caddy
    sudo systemctl restart caddy
    sudo systemctl status caddy
    This enables the caddy service on startup, restarts caddy, and displays the status of caddy. Now, it should list the status of caddy as running!

Getting a Website Up and Running: The Caddyfile

Now that we have caddy configured, we must populate the caddyfile with the appropriate contents to get a website up and running. A blank caddyfile can be found here and the official caddy documentation I used can be found here inside your Caddyfile /etc/caddy/Caddyfile, add the following lines:

  • 
            :80 {
                # Set this path to your site's directory.
                root * /var/www/html
                
                # Enable the static file server.
                file_server
            }
                
  • :80 { This line specifies the port that your website will be served on If you have a firewall enabled, please ensure that you have a rule allowing this port. You can change this port to another unused port of your choice if you wish.
  • root * /var/www/html This line specifies the directory that will be served, this is where you will place the website files that you want rendered. You can choose a directory that you want, this one is commonly used so that is why I chose it.
  • file_server Enables fileserver for caddy (most web servers do this). More information on this topic here.
Now that we have the caddyfile configured, we need to add a website to serve. Since I am having the /var/www/html directory served by caddy, lets add a website file there:
sudo nano /var/www/html/index.html
For testing purposes, lets only put this in the file: CHICKENSTRIPS. Save it and lets see if it is working!
  • The first check I would recommend is to see if the site works on localhost. If you run curl localhost, it should return the contents of the index.html file.
  • After checking that, check it on a web browser that is on the same network that your webserver is connected to. When you are on the same network, you will want to navigate to the IP address of your webserver in your web browser. You can find your IP address of the server by using the ip a command.
    • enter the IP in your browser in this manner: http://###.###.###.###
  • note: I did have issues with chrome automatically redirecting to https and the site would not render. If that happens to you, there is a way to disable that automatic https redirect, but I will not cover that in this document. You should be able to curl the site from another machine on the same network, or use another browser like edge (I know, gross)
  • note2: If you have a firewall enabled, you will need to ensure that the port you specified in the caddyfile is open, in our case, port 80
You should now have a working webserver, Hooray! Now to add in the WAF.

Configuring Coraza

Coraza should already be installed with caddy if you followed the previous steps, so all that remains is configuring the Coraza WAF. It is fairly simple to do thru the caddyfile. The documentation for configuring Coraza inside of caddy can be found here. (Warning, it is a little vague and confusing but I still recommend taking a look at it)

Our configurations, for now, will take place in the caddyfile.

  • Add the following lines to the beginning of the caddy file:
  • 
            {
                order coraza_waf first
            }
                    
    These lines ensure that out of other add-ins, Coraza is ran first. You know, security first! This is required for Coraza to work and will enable it.
  • Inside the configuration for the website that we built, (all the stuff inside `:80 { }` in the caddyfile) we need to add in the coraza_waf plugin:
  • 
            :80 {
                coraza_waf {
                        #Directives, rules, and coraza
                        #configurations go here
                }
                # Set this path to your site's directory.
                root * /var/www/html
                    
                # Enable the static file server.
                file_server
            }
                    
  • 1. Let's configure a single rule inside of Coraza, to return a 403 code whenever anyone tries to access the `/admin` endpoint on the webserver. Documentation for Security Rule Language can be found here
  • 
            SecRule REQUEST_URI "@contains /admin" \
            "id:1001, \
            phase:1, \
            deny, \
            status:403, \
            log, \
            msg:'Access to /admin is forbidden'"
                
  • Here is the breakdown of this rule:
    • SecRule - This is the directive that initiates a rule in ModSecurity.
    • REQUEST_URI - This is a variable that represents the URI (Uniform Resource Identifier) component of the incoming HTTP request.
    • @contains /admin - Matches if the requested URI contains `/admin`.
    • id:1001 - Unique rule identification number
    • phase:1 - Specifies the phase in which the rule will be executed ModSecurity operates in multiple phases, and Phase 1 is the request phase with phase 2 being the response phase.
    • deny - Deny request if rule conditions are met.
    • status:403 - If the rule triggers a 403 Forbidden HTTP status code is given in the server response.
    • log - Logs the rule match to the audit log.
    • msg:'Access to /admin is forbidden' - Specifies a custom message for the rule to be added in the log file
  • Now that we have a rule created, lets prepare the rest of the WAF plugin. There are a couple of directives that we will need to add. Documentation for the directives can be found here and here. This is the current config that I have. I will explain each directive in the config. I recommend reading the documentation I listed above for more directive options.
  • 
            coraza_waf {
                directives `
                        # Audit logs
                        SecAuditEngine On
                        SecAuditLog "/var/log/coraza.log"
                        SecAuditLogFormat JSON
                        SecAuditLogParts ABIJDEFHKZ
        
                        # Test case rules for logging
                        SecAction "id:1,pass,log"
                        SecRule REQUEST_URI "@contains /admin" \
                        "id:1001, \
                        phase:1, \
                        deny, \
                        status:403, \
                        log, \
                        msg:'Access to /admin is forbidden'"
                        SecResponseBodyAccess On
                        SecRuleEngine On
            }  
                
    • SecAuditEngine On - Directive that enables ModSecurity audit engine, allowing ModSecurity to generate audit logs for requests and responses. This is where we will log the WAF rule matches.
    • SecAuditLog "/var/log/coraza.log" - Specifies the path to the audit log file. You can change this path if you'd like. You will need to ensure that the file exsists and has the appropriate ownership and access permissions.
    • SecAuditLogFormat JSON - Sets the format of the audit log entries to JSON.
    • SecAuditLogParts ABIJDEFHKZ - Specifies the parts of the audit log to include. Each letter represents a specific part of the audit log. There is a good chart here
    • SecAction "id:1,pass,log" - Defines a custom action that logs all traffic.
    • SecRule... - This is the rule that we created earlier
    • SecResponseBodyAccess On - Enables the ability to inspect and act upon the response body. This means ModSecurity will have access to and can analyze the content of the response body.
    • SecRuleEngine On - Turns on the ModSecurity rule engine, enabling the execution of ModSecurity rules.
  • If you add these directives and other configurations you would like to the caddyfile, you should be able to restart the caddy service and the rules should be enforced. If you go to the index page, it should be logged in the audit log file specified in the SecAuditLog directive. Then if you navigate to the `/admin` endpoint in the web browser, it should return the 403 unauthorized response. Then if you go into your audit log, you should see that rule match logged in there now.

With these steps, you can create your own custom configs and or rules to be used in coraza. You can also used the prebuilt OWASP Core Ruleset to cover attacks from the OWASP top 10.

Adding the OWASP Core Ruleset to Coraza:

Most of adding the core Ruleset to Coraza is documented here, so be sure to check out this documentation before proceeding.

  • Download the required OWASP Core Rule Set files. I downloaded mine to the /etc/caddy/rules directory. You will need to download these files (I used wget to get them):
    • https://raw.githubusercontent.com/corazawaf/coraza/v2/master/coraza.conf-recommended
      • This is the main conf for Coraza, the directives that we put in the caddyfile before can be added here. I renamed the file /etc/caddy/rules/coraza.conf
    • The actual rules file here https://coraza.io/docs/tutorials/coreruleset/
      • This contains the rules (in the /rules directory) and the core ruleset conf file crs-setup.conf.example in the main directory when extracted (coreruleset) I copied the conf file, as is, to a file named cre-setup.conf
    • Just to be sure we have the items in the same location, the coraza.conf file is now in /etc/caddy/rules/coraza/conf, crs-setup.conf is now in /etc/caddy/rules/coreruleset/crs-setup.conf, and the directory of rules is at /etc/caddy/rules/coreruleset/rules.
  • Modify the Caddyfile to include the conf files and the rules files:
    • You will need to include the configuration files inside of the directives for `coraza_waf`
    
            {
                order coraza_waf first
            }
                
            :80 {
                coraza_waf {
                        load_owasp_crs
                        directives `
                                SecAction "id:1,pass,log"
                                Include /etc/caddy/rules/coreruleset/crs-setup.conf
                                Include /etc/caddy/rules/coraza.conf
                                Include /etc/caddy/rules/coreruleset/rules/*.conf
                                SecResponseBodyAccess On
                                SecRuleEngine On
                        `
                }
                # Set this path to your site's directory.
                root * /var/www/html
        
                # Enable the static file server.
                file_server
            }
                
  • This can be the whole caddyfile for the webserver. The WAF is still going to run first. The core ruleset was loaded. The default pass rule was kept. Both the configs were included. The directory containing the rules files was included. Access to the responses was granted and the rule engine was turned on.
  • note: If you would like to include logging for your rules, you can add the directives we used before for logging to the coraza.conf file. towards the bottom of it, there should be a section that has some default logging settings.
  • note: If you are going to use this in production and are going to have logging enabled, I recommend looking into rolling the logs as these logs could get big fast. It is well documented for Coraza.

With all of this set up, you can restart caddy and it should all be running. You can test the WAF by adding something malicious. The first thing I tried to test this was going to http://localhost/<script></script> . This triggered the rule for XSS. and showed that the WAF is working as expected.

Links That I Found Useful (Some of them were mentioned in the article):