Discourse + Web Application Firewall (WAF) mod_security

What is the recommended WAF to run in-front of Discourse?

I recently succeeded in modifying the nginx config installed in the Discourse docker container to include ModSecurity and the OWASP (Open Web Application Security Project) CRS (Core Rule Set).

So far my tests are great, and ModSecurity appears to play very well with Discourse ootb.

What are other users’ experiences with WAFs & Discourse? Do you have any recommendations other than ModSecurity?

2 Likes

A note about why WAFs are important: they provide broad protection against 0days not only from the web application, but also its entire stack of dependencies of the web app.

It’s been pointed out to me that the Discourse team has a bug bounty program, which is great!

…but, of course, there is no such thing as 100% secure code. Mistakes happen, and it doesn’t even have to be your team’s mistake to include a critical vulnerability into your project.

For example, consider recent history with CVE-2019-11043: a bug in php-fpm that could be exploited as a remote code execution on boxes running vulnerable versions of php-fpm and nginx.

However, CVE-2019-11043 can be entirely mitigated by blocking requests that include carriage return or newlines:

This is just an example of how an installation of a web application can be exploited by a critical vulnerability, even if there’s no flaws in the code of that web application itself. In the case of Discourse, there’s a lot of external software pulled in during the docker install which could make Discourse vulnerable in the future.

In the above case, however, the ModSecurity CRS already blocks requests with newlines & carriage returns–effectively protecting otherwise vulnerable web servers to CVE-2019-11043 before the 0th day.

These sorts of vulnerabilities are discovered all the time. There are significant benefits to using a WAF like ModSecurity for web applications.

This is a bad idea and it is not recommended. The benefit of this type of thing for a JavaScript app is extremely limited and it adds significant complexity to your hosting setup.

6 Likes

related: I just found this guide from @joelradon on how to install Discourse with the NAXSI WAF in nginx before the Discourse docker container:

It’s no more supportable than what you’re asking above.

If it helps I can add an unsupported tag there too?

I think the desire for some magic device that auto mitigates issues is somewhat misguided in the Discourse setup. We have a bounty program, we patch issues in Discourse within hours of when they are reported. Sites run tests-passed by default which in today’s case contains commits from today.

Sure if you are running software that was exploited years ago and you have no freedom to upgrade cause … reasons… a WAF makes sense cause it could save you. But in the case of Discourse I think it is at best misguided.

2 Likes

I was successfully able to persist my nginx ModSecurity config changes across launcher rebuild app runs as follows:

First, we update the local copy of install-nginx that came from the discourse_docker repo and is cloned to /var/discourse/.

cd /var/discourse/image/base
cp install-nginx install-nginx.`date "+%Y%m%d_%H%M%S"`.orig

# add a block to checkout the the modsecurity nginx module just before downloading the nginx source
grep 'ModSecurity' install-nginx || sed -i 's%\(curl.*nginx\.org/download.*\)%# mod_security\napt-get install -y libmodsecurity-dev modsecurity-crs\ncd /tmp\ngit clone --depth 1 https://github.com/SpiderLabs/ModSecurity-nginx.git\n\n\1%' install-nginx

# update the configure line to include the ModSecurity module checked-out above
sed -i '/ModSecurity/! s%^[^#]*./configure \(.*nginx.*\)%#./configure \1\n./configure \1 --add-module=/tmp/ModSecurity-nginx%' install-nginx

# add a line to cleanup section
grep 'rm -fr /tmp/ModSecurity-nginx' install-nginx || sed -i 's%\(rm -fr.*/tmp/nginx.*\)%rm -fr /tmp/ModSecurity-nginx\n\1%' install-nginx

Note that the Dockerfile responsible for executing the install-nginx script is executed when the image is built. And the image is only built by the Discourse team before it’s uploaded to docker hub. When the Discourse ./launcher rebuild app command is run, it triggers (if updates are available) a docker pull, which grabs the latest Discourse docker image from docker hub. Again, this does not rebuild the image, execute the Dockerfile, or execute the install-nginx script that’s modified above.

The only way (that I know of) to trigger the updated install-nginx bash script to run (which is executed by the Dockerfile) is to have docker build a new image. For example, this triggers docker to build a new image named discourse_modsecurity – which will be built using the locally modified install-nginx script:

docker build --tag 'discourse_modsecurity' /var/discourse/image/base/

Unfortunately, I couldn’t find a way to tell launcher to use a custom image (specifying a run-image uses the specified image directly, without executing the templates against it – as needed to actually configure [as opposed to just install] nginx). So we replace the image variable defined in the launcher script to use our new local docker image named discourse_modsecurity

# replace the line "image="discourse/base:<version>" with 'image="discourse_modsecurity"'
grep 'discourse_modsecurity' launcher || sed --in-place=.`date "+%Y%m%d_%H%M%S"` '/base_image/! s%^\(\s*\)image=\(.*\)$%#\1image=\2\n\1image="discourse_modsecurity"%' /var/discourse/launcher

Now we add a new template file to setup our nginx configs to include the necessary modsecurity files/blocks

cat << EOF > /var/discourse/templates/web.modsecurity.template.yml
run:
  - exec:
     cmd:
       - sudo apt-get install -y modsecurity-crs
       - cp /etc/modsecurity/modsecurity.conf-recommended /etc/modsecurity/modsecurity.conf
       - sed -i 's/SecRuleEngine DetectionOnly/SecRuleEngine On/' /etc/modsecurity/modsecurity.conf
       - sed -i 's^\(\s*\)[^#]*SecRequestBodyInMemoryLimit\(.*\)^\1#SecRequestBodyInMemoryLimit\2^' /etc/modsecurity/modsecurity.conf
       - sed -i '/nginx/! s%^\(\s*\)[^#]*SecAuditLog \(.*\)%#\1SecAuditLog \2\n\1SecAuditLog /var/log/nginx/modsec_audit.log%' /etc/modsecurity/modsecurity.conf

  - file:
     path: /etc/nginx/conf.d/modsecurity.include
     contents: |
        ################################################################################
        # File:    modsecurity.include
        # Version: 0.1
        # Purpose: Defines mod_security rules for the discourse vhost
        #          This should be included in the server{} blocks nginx vhosts.
        # Author:  Michael Altfield <michael@opensourceecology.org>
        # Created: 2019-11-12
        # Updated: 2019-11-12
        ################################################################################
        Include "/etc/modsecurity/modsecurity.conf"
        
        # OWASP Core Rule Set, installed from the 'modsecurity-crs' package in debian
        Include /etc/modsecurity/crs/crs-setup.conf
        Include /usr/share/modsecurity-crs/rules/*.conf

  - replace:
     filename: "/etc/nginx/conf.d/discourse.conf"
     from: /server.+{/
     to: |
       server {
         modsecurity on;
         modsecurity_rules_file /etc/nginx/conf.d/modsecurity.include;

EOF

And add this template (templates/web.modsecurity.template.yml) to our app’s yaml config file’s templates block so it looks something like this:

[root@osestaging1 discourse]# vim /var/discourse/containers/app.yml
...
[root@osestaging1 discourse]# grep -A 6 'templates:' /var/discourse/containers/app.yml
templates:
  - "templates/postgres.template.yml"
  - "templates/redis.template.yml"
  - "templates/web.template.yml"
  - "templates/web.ratelimited.template.yml"
  - "templates/web.socketed.template.yml"
  - "templates/web.modsecurity.template.yml"
[root@osestaging1 discourse]# 

Now when you rebuild the Discourse docker app, it will use your new discourse_modsecurity docker image using nginx with modsecurity and it will configure nginx to use the OWASP CRS.

/var/discourse/launcher rebuild app
1 Like

I do agree with you that ModSecurity or similar WAFs are not a golden bullet.

But I know (at least?) one vulnerability that was found in RoR which affected Discourse that would have been mitigated by a ModSecurity-like mechanism.

When we were patching this we saw in our logs that it had been used in the wild on at least one of our forums before the CVE was publicly known and patched. This did not result in any information disclosure but that was only because we did some things differently from a standard install (i.e. luck).

I’m not sure if the added complexity outweighs the added security though.

4 Likes

I liken WAFs to signature-based virus scanners which IMO are not very useful in environments with a limited attack surface (e.g. your non-Windows servers)

They’re not going to catch everything but they will (in theory) catch common patterns of attacks (e.g. SQLI) and known exploits (e.g. the RoR vuln above) across a range of software.

I can see the value of running them especially in an environment (enterprise) where you have a ton of applications running god-knows-what and you need to know you’re protecting each application from these exploits without needing to worry about every individual one - that takes an NxM size problem and reduces it to N+M

Whether or not it’s worth running on your site is a different matter and one of those risk (of breakage)-benefit (of protection) analyses you’ll need to do for yourself.

9 Likes