How to build a Discourse Installation with an Open Source WAF

I am not allowed to post in How-Tos so I will post this here :slight_smile:

Overview

The goal is to add a WAF to existing resources while maintaining performance on a small installation. In this example, we will use a single EC2 instance. Since Discourse is run in docker, we are able to install NGINX on the host and use proxy pass to feed traffic into our docker container. We are also using RDS for PostgreSQL.

Discourse Configuration

Our first step is to configure our app.yml file for a custom discourse installation. In this example we will be running running web and reddis roles. We will not be using DB role or SSL roles.

Also, we want make sure we dont expose web ports on the docker container. The reason being is we will expose our web through NGINX proxy on host.

Example app.yml file

## this is the all-in-one, standalone Discourse Docker container template
##
## After making changes to this file, you MUST rebuild
## /var/discourse/launcher rebuild app
##
## BE *VERY* CAREFUL WHEN EDITING!
## YAML FILES ARE SUPER SUPER SENSITIVE TO MISTAKES IN WHITESPACE OR ALIGNMENT!
## visit http://www.yamllint.com/ to validate this file as needed
templates:
  - "templates/redis.template.yml"
  - "templates/web.template.yml"
  - "templates/web.ratelimited.template.yml"
  - "templates/web.socketed.template.yml"
## Uncomment these two lines if you wish to add Lets Encrypt (https)
  #- "templates/web.ssl.template.yml"
  #- "templates/web.letsencrypt.ssl.template.yml"


## which TCP/IP ports should this container expose?
## If you want Discourse to share a port with another webserver like Apache or nginx,
## see https://meta.discourse.org/t/17247 for details
expose:
#  - "80:80"   # http
#  - "443:443" # https


params:
  db_default_text_search_config: "pg_catalog.english"
  ## Set db_shared_buffers to a max of 25% of the total memory.
  ## will be set automatically by bootstrap based on detected RAM, or you can override
  db_shared_buffers: "768MB"
  ## can improve sorting performance, but adds memory usage per-connection
  #db_work_mem: "40MB"
  ## Which Git revision should this container use? (default: tests-passed)
  #version: tests-passed

env:
  LANG: en_US.UTF-8
  # DISCOURSE_DEFAULT_LOCALE: en
  ## How many concurrent web requests are supported? Depends on memory and CPU cores.
  ## will be set automatically by bootstrap based on detected CPUs, or you can override
  UNICORN_WORKERS: 2

  ## TODO: The domain name this Discourse instance will respond to
  ## Required. Discourse will not work with a bare IP number.
  DISCOURSE_HOSTNAME: cloudforums.net

  ## Uncomment if you want the container to be started with the same
  ## hostname (-h option) as specified above (default "$hostname-$config")
  #DOCKER_USE_HOSTNAME: true

  ## TODO: List of comma delimited emails that will be made admin and developer
  ## on initial signup example 'user1@example.com,user2@example.com'
  DISCOURSE_DEVELOPER_EMAILS: 'youremail@domain.com'

  ## TODO: The SMTP mail server used to validate new accounts and send notifications
  # SMTP ADDRESS, username, and password are required
  # WARNING the char '#' in SMTP password can cause problems!
  DISCOURSE_SMTP_ADDRESS: YOUR_SMTP
  DISCOURSE_SMTP_PORT: 587
  DISCOURSE_SMTP_USER_NAME: YOUR_SMTP_USERNAME
  DISCOURSE_SMTP_PASSWORD: YOUR_SMTP_PASSWORD
  #DISCOURSE_SMTP_ENABLE_START_TLS: true           # (optional, default true)

  ## If you added the Lets Encrypt template, uncomment below to get a free SSL certificate
  #LETSENCRYPT_ACCOUNT_EMAIL: me@example.com

  DISCOURSE_DB_SOCKET: ''
  DISCOURSE_DB_USERNAME: YOUR_USERNAME
  DISCOURSE_DB_PASSWORD: YOUR_PASSWORD
  DISCOURSE_DB_HOST: YOUR_HOST

  ## The http or https CDN address for this Discourse instance (configured to pull)
  ## see https://meta.discourse.org/t/14857 for details
  #DISCOURSE_CDN_URL: https://discourse-cdn.example.com
## The Docker container is stateless; all data is stored in /shared
volumes:
  - volume:
      host: /var/discourse/shared/standalone
      guest: /shared
  - volume:
      host: /var/discourse/shared/standalone/log/var-log
      guest: /var/log

## Plugins go here
## see https://meta.discourse.org/t/19157 for details
hooks:
  after_code:
    - exec:
        cd: $home/plugins
        cmd:
          - git clone https://github.com/discourse/docker_manager.git

## Any custom commands to run after building
run:
  - exec: echo "Beginning of custom commands"
  ## If you want to set the 'From' email address for your first registration, uncomment and change:
  ## After getting the first signup email, re-comment the line. It only needs to run once.
  - exec: rails c "SiteSetting.enable_badge_sql = true"
  - exec: echo "End of custom commands"

Begin Discourse Install

Clone Discourse Repository

sudo -u root git clone https://github.com/discourse/discourse_docker.git /var/discours

Copy Custom app.yml for Installation

You can run a command below to do this for you. Just replace the test “PASTE YOUR FULL APP.YML HERE” with your full app.yml

sudo -u root cat > /var/discourse/containers/app.yml <<\EOF
PASTE YOUR FULL APP.YML HERE
EOF

Run Discourse Setup

Start discourse setup with no prompts using the command below.

sudo -u yes "" | /var/discourse/./discourse-setup

Discourse should take 10-15 minutes to install.

NGINX , WAF, and GEOIP installation

Run updates and install pre-reqs

apt update -y
apt upgrade -y
apt -y install libpcre3-dev libssl-dev unzip build-essential daemon libxml2-dev libxslt1-dev libgd-dev libgeoip-dev zlib1g-dev libpcre3

Install MindMax DB so you can run GEO IP rules based on location. You can use this database to route or block traffic based on location. Our NGINX logs will now show country of user even if you dont turn on Geo blocking. I

sudo add-apt-repository -y ppa:maxmind/ppa
apt update -y
apt install -y libmaxminddb0 libmaxminddb-dev mmdb-bin

Download and extract NGINX and NAXSI WAF

Note: Replace NGINX with latest version

mkdir ~/nginx-waf
wget https://nginx.org/download/nginx-1.16.1.tar.gz -O ~/nginx-waf/nginx.tar.gz
tar xzf ~/nginx-waf/nginx.tar.gz -C ~/nginx-waf
wget https://github.com/nbs-system/naxsi/archive/master.zip -O ~/nginx-waf/waf.zip
unzip ~/nginx-waf/waf.zip -d ~/nginx-waf/

Git Clone GEO IP2 Module for NGINX

apt install -y git
git clone https://github.com/leev/ngx_http_geoip2_module.git /etc/ngx_http_geoip2_module

Compile NGINX

We will build a script to do this for us and run it below


cat > ~/nginx-waf/nginx-1.16.1/install.sh <<\EOF
cd ~/nginx-waf/nginx-1.16.1/
./configure --conf-path=/etc/nginx/nginx.conf --add-module=../naxsi-master/naxsi_src/ --error-log-path=/var/log/nginx/error.log --http-client-body-temp-path=/var/lib/nginx/body --http-fastcgi-temp-path=/var/lib/nginx/fastcgi --http-log-path=/var/log/nginx/access.log --http-proxy-temp-path=/var/lib/nginx/proxy --lock-path=/var/lock/nginx.lock --pid-path=/var/run/nginx.pid --user=www-data --group=www-data --with-http_ssl_module --without-mail_pop3_module --without-mail_smtp_module --without-mail_imap_module --without-http_uwsgi_module --add-dynamic-module=/etc/ngx_http_geoip2_module --without-http_scgi_module --prefix=/usr
make
make install
EOF

sh ~/nginx-waf/nginx-1.16.1/install.sh

Create Firewall Rules

The default below will turn on WAF and block malicious requests. If you would like to run the WAF in learning mode, you can do that by adding Learning Mode to the rule file below.

cp ~/nginx-waf/naxsi-master/naxsi_config/naxsi_core.rules /etc/nginx/



cat > /etc/nginx/naxsi.rules <<\EOF
SecRulesEnabled;
DeniedUrl "/RequestDenied";
## Check Naxsi rules
CheckRule "$SQL >= 8" BLOCK;
CheckRule "$RFI >= 8" BLOCK;
CheckRule "$TRAVERSAL >= 4" BLOCK;
CheckRule "$EVADE >= 4" BLOCK;
CheckRule "$XSS >= 8" BLOCK;
EOF

Create NGINX configuration file

By default in this configuration file, GEO blocking is commented out, but you can uncomment and turn on if you would like.

Note: We must run NGINX WITHOUT SSL at first. Because we need certbot to see a live domain. We will be getting SSL in a later step and actually copying down a new NGINX conf file to replace this one

Note: Replace cloudforums.net with your domain name

cat > /etc/nginx/nginx.conf <<\EOF
#user  nobody;
worker_processes  1;
load_module modules/ngx_http_geoip2_module.so;
events {
    worker_connections  1024;
}
http {
    include       mime.types;
    include       /etc/nginx/naxsi_core.rules;
        include     /etc/nginx/conf.d/*.conf;
        include     /etc/nginx/sites-enabled/*;
    
    geoip2 /etc/geo_ip/GeoLite2-Country.mmdb {
        $geoip2_data_country_code source=$remote_addr country iso_code;
        $geoip2_data_country_name source=$remote_addr country names en;
    }  
    log_format  main_geo  '$remote_addr - $remote_user [$time_local] "$request" '
                          '$status $body_bytes_sent "$http_referer" '
                          '"$http_user_agent" "$http_x_forwarded_for" '
                          '$geoip2_data_country_code $geoip2_data_country_name';
    
    access_log /var/log/nginx/access.log main_geo;
   
    default_type  application/octet-stream;
    error_log /var/log/nginx/error.log;
    #access_log  logs/access.log  main;
    sendfile        on;
    #tcp_nopush     on;
    #keepalive_timeout  0;
    keepalive_timeout  65;
    #gzip  on;
    server {
        listen       80;
        server_name cloudforums.net;
        root /;
 
        location /.well-known/acme-challenge/ {
                root /var/www;
        }
        location / {
            return 301 https://$host$request_uri;
            include /etc/nginx/naxsi.rules;
                root   html;
                index  index.html index.htm;
        }
        error_page   500 502 503 504  /50x.html;
        location = /50x.html {
            root   html;
        }
    }
}
EOF

Create upstart script for NGINX

We need to create a script for the NGINX service to start

cat > /etc/init.d/nginx <<\EOF
#! /bin/sh PATH=/usr/local/sbin:/usr/local/bin:/sbin:/bin:/usr/sbin:/usr/bin 
 DAEMON=/usr/sbin/nginx 
 NAME=nginx 
 DESC=nginx
 
 test -x $DAEMON || exit 0 
 # Include nginx defaults if available 
 if [ -f /etc/nginx ] ; then 
         . /etc/nginx 
 fi
 
 set -e
 
 case "$1" in 
     start)
         echo -n "Starting $DESC: " 
         start-stop-daemon --start --quiet --pidfile /var/run/nginx.pid \ 
             --exec $DAEMON -- $DAEMON_OPTS 
         echo "$NAME." 
         ;; 
     stop) 
         echo -n "Stopping $DESC: " 
         start-stop-daemon --stop --quiet --pidfile /var/run/nginx.pid \ 
             --exec $DAEMON 
         echo "$NAME." 
         ;; 
     restart|force-reload) 
         echo -n "Restarting $DESC: " 
         start-stop-daemon --stop --quiet --pidfile \ 
             /var/run/nginx.pid --exec $DAEMON 
         sleep 1 start-stop-daemon --start --quiet --pidfile \ 
             /var/run/nginx.pid --exec $DAEMON -- $DAEMON_OPTS 
         echo "$NAME." 
         ;; 
     reload) 
         echo -n "Reloading $DESC configuration: " 
         start-stop-daemon --stop --signal HUP --quiet --pidfile /var/run/nginx.pid \ 
             --exec $DAEMON 
         echo "$NAME." 
         ;; 
     *) 
         N=/etc/init.d/$NAME 
         echo "Usage: $N {start|stop|restart|force-reload}" >&2 
         exit 1 
         ;; 
 esac
 
 exit 0
EOF

systemctl daemon-reload

Create NGINX service file

cat > /lib/systemd/system/nginx.service <<\EOF
[Unit]
Description=A high performance web server and a reverse proxy server
Documentation=man:nginx(8)
After=syslog.target network.target remote-fs.target nss-lookup.target
[Service]
Type=forking
PIDFile=/run/nginx.pid
ExecStartPre=/usr/sbin/nginx -t
ExecStart=/usr/sbin/nginx
ExecReload=/usr/sbin/nginx -s reload
ExecStop=/bin/kill -s QUIT $MAINPID
PrivateTmp=true
[Install]
WantedBy=multi-user.target
EOF

Download Geo IP Country Database

mkdir /etc/geo_ip
wget http://geolite.maxmind.com/download/geoip/database/GeoLite2-Country.mmdb.gz
gzip -d GeoLite2-Country.mmdb.gz
mv GeoLite2-Country.mmdb /etc/geo_ip/

Enable and Start NGINX

systemctl stop apache2
systemctl daemon-reload
systemctl enable nginx
systemctl start nginx

Add SSL cert for you domain

Dont forget to replace cloudforums.net with your domain.com

mkdir /var/www
apt-get -y update
apt-get -y install letsencrypt
yes "n" | letsencrypt certonly --webroot --agree-tos -w /var/www -d cloudforums.net -m youremail@domain.com

Create new NGINX file with SSL rules

Create a new NGINX file with your SSL cert you just downloaded.

Note: change cloudforums.net to your domain


cat > /etc/nginx/nginx.conf <<\EOF
#user  nobody;
worker_processes  1;
load_module modules/ngx_http_geoip2_module.so;
events {
    worker_connections  1024;
}
http {
    include       mime.types;
    include       /etc/nginx/naxsi_core.rules;
        include     /etc/nginx/conf.d/*.conf;
        include     /etc/nginx/sites-enabled/*;
    
    geoip2 /etc/geo_ip/GeoLite2-Country.mmdb {
        $geoip2_data_country_code source=$remote_addr country iso_code;
        $geoip2_data_country_name source=$remote_addr country names en;
    }  
    log_format  main_geo  '$remote_addr - $remote_user [$time_local] "$request" '
                          '$status $body_bytes_sent "$http_referer" '
                          '"$http_user_agent" "$http_x_forwarded_for" '
                          '$geoip2_data_country_code $geoip2_data_country_name';
    
    #***********************************************************
    #Uncomment to do GeoBlocking. Default is US only in settings below
    #***********************************************************
    #map $geoip2_data_country_code $allowed_country {
    #    default no;
    #    US yes;
    # }
    access_log /var/log/nginx/access.log main_geo;
   
    default_type  application/octet-stream;
    error_log /var/log/nginx/error.log;
    #access_log  logs/access.log  main;
    sendfile        on;
    #tcp_nopush     on;
    #keepalive_timeout  0;
    keepalive_timeout  65;
    #gzip  on;
    server {
        listen       80;
        server_name  cloudforums.net;
        root /;
 
        location /.well-known/acme-challenge/ {
                root /var/www;
        }
        location / {
            return 301 https://$server_name$request_uri;
            include /etc/nginx/naxsi.rules;
                root   html;
                index  index.html index.htm;
        }
        error_page   500 502 503 504  /50x.html;
        location = /50x.html {
            root   html;
        }
    }
    server {
      listen 443 ssl;  listen [::]:443 ssl;
      server_name cloudforums.net;  
      ssl on;
      ssl_certificate      /etc/letsencrypt/live/cloudforums.net/fullchain.pem;
      ssl_certificate_key  /etc/letsencrypt/live/cloudforums.net/privkey.pem;
      ssl_ciphers 'ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-SHA384:ECDHE-RSA-AES256-SHA384:ECDHE-ECDSA-AES128-SHA256:ECDHE-RSA-AES128-SHA256';
      ssl_protocols TLSv1.2;
      ssl_prefer_server_ciphers on;
      ssl_session_cache shared:SSL:10m;
      add_header Strict-Transport-Security "max-age=63072000;";
      ssl_stapling on;
      ssl_stapling_verify on;
      client_max_body_size 0;
      location / {
        proxy_pass http://unix:/var/discourse/shared/standalone/nginx.http.sock:;
        proxy_set_header Host $http_host;
        proxy_http_version 1.1;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto https;
        proxy_set_header X-Real-IP $remote_addr;
       }
    }
    # HTTPS server
    #
    #server {
    #    listen       443 ssl;
    #    server_name  localhost;
    #    ssl_certificate      cert.pem;
    #    ssl_certificate_key  cert.key;
    #    ssl_session_cache    shared:SSL:1m;
    #    ssl_session_timeout  5m;
    #    ssl_ciphers  HIGH:!aNULL:!MD5;
    #    ssl_prefer_server_ciphers  on;
    #    location / {
    #        root   html;
    #        index  index.html index.htm;
    #    }
    #}
}
EOF

Restart NGINX

systemctl restart nginx

You now have an awsome open source WAF!! :slight_smile:

Shameless Plug

Make sure to join us at cloudforums.net if you enjoyed this guide. We would really love to have you over here. We don’t sell anything or have ads. Just looking for good IT posting :slight_smile:

3 Likes

One click automated NGINX / WAF install for after you install discourse. Dont forget to change the domain name from cloudforums.net to your domain.

just run

sh discourse_install.sh
1 Like