Discourse + Web Application Firewall (WAF) mod_security

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