Install Wekan+nginx (HTTPS) in a FreeNAS jail in 2020

Once again, when you try to combine an unpopular app on an unpopular platform, and you want the latest version of them, the journey is long. Today, we want a FreeNAS 11.3 jail hosting Wekan 4.01, the Trello-like kanban-style board app, behind nginx 1.18.0 with OpenSSL 1.1.1g using TLS 1.3. You may want to do that if you don’t want to share your private boards with yet another cloud company and its likely ambiguous privacy policy.

1. Create a new jail

Assuming you already have created jails in the past, your FreeNAS is ready to make new ones quickly.

Log in to your FreeNAS admin panel, go to Jails, click ADD.

Give it a name (here “wekan-test”), and select the latest release version available, then Next.

Create jail step 1

Check VNET and select either DHCP (if your router can be configured to give static DHCP lease for instance), or give it a static IP. Next. Submit.

Create jail step 2 (static IPv4 only here)

Start the jail by clicking the START button.

Jail is down, start it

Then, SSH to your FreeNAS instance, locate your Jail ID using jls, then jexec <JID> csh.

Locate jail and enter it

2. Install dependencies

Install MongoDB 4.0:

pkg install mongodb40 mongodb40-tools
sysrc mongod_enable=YES
service mongod start

Don’t worry about exposing your DB to the world: MongoDB no longer listens on 0.0.0.0 by default, it only creates a local socket as you can see with sockstat -L:

# sockstat -L
USER COMMAND PID FD PROTO LOCAL ADDRESS FOREIGN ADDRESS
mongodb mongod 99827 10 stream /tmp/mongodb-27017.sock

Install node.js 12:

pkg install node12 npm-node12 bcrypt

Make sure python2 can be found by npm:

cd /usr/local/bin
ln -s python2.7 python2

Install some other tools

pkg install nano

3. Install Wekan

Create new user

adduser

Use csh for the shell, and use an empty password. We will disable login after anyway.

Plenty of questions to answer to add a new user

Fetch sources

Go to https://releases.wekan.team/ and locate the ZIP or TAR package for the latest wekan release. This is a Meteor-wrapped bundle, easier to deploy, which is different than cloning the Github repo.

Latest release as of April 29, 2020

Right-click, copy link address.

Back in the jail, switch to the new user wekan, fetch and decompress the archive in the user’s home directory.

su wekan
cd /home/wekan
fetch https://releases.wekan.team/wekan-4.01.zip
tar xzpf wekan-4.01.zip

Remove phantomJS

The version needed is not available on FreeBSD but Wekan works without it.

cd ~/bundle/programs/server/npm/node_modules/meteor/lucasantoniassi_accounts-lockout/node_modules
rm -rf phantomjs-prebuilt

Run npm install a first time

cd ~/bundle/programs/server
npm install

This will fail with a bcrypt error and a node-pre-gyp error.

npm cannot install bcrypt

Fix the node-pre-gyp error

rm -rf /usr/home/wekan/bundle/programs/server/node_modules/.bin/node-pre-gyp
npm install node-pre-gyp

Fix the bcrypt error

npm install bcrypt
cd npm/node_modules
mv bcrypt ~/
cd ../..
npm install
mv ~/bcrypt npm/node_modules

npm install should have completed without error this time.

Install fibers

npm install fibers

4. Configure Wekan

Make a config file

Next, we need to prepare a config file that will apply all the environment variables needed by Wekan.

Grab https://raw.githubusercontent.com/wekan/wekan/master/start-wekan.sh as /home/wekan/start-wekan.sh.

cd ~
fetch https://raw.githubusercontent.com/wekan/wekan/master/start-wekan.sh

Open the file

nano start-wekan.sh

and comment the line cd .build/bundle at the beginning, as well as the lines node main.js and cd ../.. lines towards the end of the file:

#while true; do
      #cd .build/bundle

      [...]

      #node main.js
      # & >> ../../wekan.log
      #cd ../..
#done

Next, adjust ROOT_URL to correspond to the URL you will be using Wekan with. For instance, you could configure an entry in your hosts file to map the FreeNAS jail’s IP with the name wekan (LAN use only). Through a DNS server on your network, you could make sure to resolve, let’s say wekan.lan to the jail’s IP. If you’re exposing Wekan to the internet, you probably will get a domain name for it.

This will give you something like this:

      export ROOT_URL='https://my-super-wekan-setup.com'

For my example, I’ll do wekan-test.lan.

Note: this is not the IP/domain and port that Wekan will be listening on. This is the final form of the URL once served by nginx, which we will configure shortly.

Customize the local port that Wekan will be listening on, and make it bind to localhost only. This is achieved by setting the undocumented BIND_IP environment variable. You don’t want Wekan to be open to the world and directly reachable, it should go through nginx.

      export PORT=3001
      export BIND_IP=127.0.0.1

Make sure to also configure MAIL_URL, MAIL_FROM (not specified in the .sh file), WITH_API, and check other options as well.

Make it a service

Next, we want to start Wekan as a service and use the config we just made. Exit from su wekan, then edit /usr/local/etc/rc.d/wekan.

% exit
# nano /usr/local/etc/rc.d/wekan

Paste the content below into it:

#!/bin/sh
# PROVIDE: wekan
# REQUIRE: mongod nginx
# BEFORE:
# KEYWORD: shutdown

. /etc/rc.subr

name="wekan"
rcvar="wekan_enable"
pidfile="/var/run/${name}.pid"

. /home/wekan/start-wekan.sh
cd /home/wekan/bundle
command="/usr/sbin/daemon"
command_args="-P ${pidfile} -u wekan -r /usr/local/bin/node main.js"

load_rc_config $name
: ${wekan_enable:="NO"}

run_rc_command "$1"

Save and exit. Set the proper permissions:

chmod 555 /usr/local/etc/rc.d/wekan

Enable and start the service.

sysrc wekan_enable=yes
service wekan start

At this point, Wekan should be running, but is only accessible on localhost. One way to test if things are running well is to netcat to localhost on port 3001 (as configured in your start-wekan.sh) and send a simple HTTP request.

# nc localhost 3001
GET / HTTP/1.1
Host: wekan-test.lan
Accept: */*


Check if Wekan is alive

Let’s now disable wekan login:

chsh -s /usr/sbin/nologin wekan

5. Install Nginx

Let’s assume you want the latest nginx version available, with support for TLS 1.3, and you don’t care about legacy clients. You can’t just pkg install nginx. You will get an older version compiled against a version of OpenSSL that doesn’t even support TLS 1.3. You’d not be happy.

Fetch the latest OpenSSL source

Go to https://www.openssl.org/source/ and get the link to the .tar.gz file corresponding to the latest v1.1 release.

OpenSSL download page

Today, this is https://www.openssl.org/source/openssl-1.1.1g.tar.gz.

Fetch the source:

cd /tmp
fetch https://www.openssl.org/source/openssl-1.1.1g.tar.gz
tar zxvf openssl-1.1.1g.tar.gz

Fetch the latest nginx source

Similarly, go to https://nginx.org/en/download.html and get the link to the .tar.gz file corresponding to the latest stable release.

Get the latest nginx stable source

Today, this is https://nginx.org/download/nginx-1.18.0.tar.gz.

Fetch the source.

cd /tmp
fetch https://nginx.org/download/nginx-1.18.0.tar.gz
tar zxvf nginx-1.18.0.tar.gz

Compile nginx with OpenSSL

Note: adjust the path to OpenSSL in the --with-openssl= accordingly. Also, the list of modules for nginx is a small list but should be enough to run Wekan (probably even an overkill).

pkg install perl5
cd nginx-1.18.0
./configure --prefix=/usr/local/etc/nginx --with-cc-opt='-I /usr/local/include' --with-ld-opt='-L /usr/local/lib' --conf-path=/usr/local/etc/nginx/nginx.conf --sbin-path=/usr/local/sbin/nginx --pid-path=/var/run/nginx.pid --error-log-path=/var/log/nginx/error.log --user=www --group=www --modules-path=/usr/local/libexec/nginx --with-file-aio --http-client-body-temp-path=/var/tmp/nginx/client_body_temp --http-proxy-temp-path=/var/tmp/nginx/proxy_temp --http-log-path=/var/log/nginx/access.log --with-http_v2_module --with-http_addition_module --with-http_auth_request_module --with-http_gunzip_module --with-http_gzip_static_module --with-http_realip_module --with-pcre --with-http_slice_module --with-http_ssl_module --with-openssl=../openssl-1.1.1g --with-http_stub_status_module --with-http_sub_module --with-threads
make
make install

Then test it:

# nginx -V
nginx version: nginx/1.18.0
built by clang 8.0.0 (tags/RELEASE_800/final 356365) (based on LLVM 8.0.0)
built with OpenSSL 1.1.1g  21 Apr 2020
TLS SNI support enabled
configure arguments: [...]

Configure nginx

Since we will overwrite the existing config, I find it easier to just delete nginx config file and recreate it:

rm /usr/local/etc/nginx/nginx.conf
touch /usr/local/etc/nginx/nginx.conf
chown root:wheel /usr/local/etc/nginx/nginx.conf
chmod 644 /usr/local/etc/nginx/nginx.conf

Edit this config file:

nano /usr/local/etc/nginx/nginx.conf

Paste the following content in the file:

user  www;

events {
	worker_connections 10;
	# multi_accept on;
}

http {
    # this section is needed to proxy web-socket connections
    map $http_upgrade $connection_upgrade {
        default upgrade;
        ''      close;
    }

    ##
    # Basic Settings
    ##
    include       mime.types;
    default_type  application/octet-stream;
    client_max_body_size 100M;
    server_tokens off;
    charset utf-8;
    sendfile on;
    keepalive_timeout 60;
    gzip on;

    ##
    # Logging Settings
    ##
    log_format  main  '$remote_addr - $remote_user [$time_local] "$request" '
                      '$status $body_bytes_sent "$http_referer" '
                      '"$http_user_agent" "$http_x_forwarded_for"';
    #if logging requests is needed
    #access_log  /var/log/access.log  main;
    access_log off;
    error_log /var/log/nginx/error.log;

    server {
        listen       443 ssl http2;
        server_name  wekan-test.lan;

        ##
        # SSL Settings
        ##
        ssl_certificate     /usr/local/etc/ssl/wekan.crt;
        ssl_certificate_key /usr/local/etc/ssl/wekan.key;
        #ssl_password_file   /usr/local/etc/ssl/pass.txt;

        ssl_protocols       TLSv1.3;
        #if clients can't connect (because they don't support TLSv1.3), use:
        #ssl_protocols       TLSv1.3 TLSv1.2;

        #TLS 1.3 and FS TLS 1.2 ciphersuites with EC certificates only
        ssl_ciphers         "TLS_CHACHA20_POLY1305:TLS_AES_128_GCM_SHA256:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-ECDSA-AES128-GCM-SHA256";
        ssl_ecdh_curve      X25519:secp521r1:secp384r1;

        # if using a browser-trusted certificate
        #ssl_stapling on;
        #ssl_stapling_verify on;

        ssl_session_timeout 1h;
        ssl_session_cache shared:SSL:30m;
        ssl_session_tickets off;
        add_header Strict-Transport-Security "max-age=31536000;";
        add_header X-Frame-Options SAMEORIGIN;
        add_header X-Content-Type-Options nosniff;
        add_header X-XSS-Protection "1; mode=block";

        # Pass requests to Wekan.
        # If you have Wekan at https://example.com/wekan , change location to:
        # location /wekan {
        location / {
            # proxy_pass http://127.0.0.1:3001/wekan;
            proxy_pass http://127.0.0.1:3001; # local Wekan instance
            proxy_http_version 1.1;
            proxy_set_header Upgrade $http_upgrade; # allow websockets
            proxy_redirect off;
            proxy_set_header Host $host;
            proxy_set_header X-Real-IP $remote_addr;
            proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;

            # this setting allows the browser to cache the application in a way compatible with Meteor
            # on every application update the name of CSS and JS file is different, so they can be cache infinitely (here: 30 days)
            # the root path (/) MUST NOT be cached
            #if ($uri != '/wekan') {
            #    expires 30d;
            #}
        }
    }
}

At this point, you should customize server_name:

server_name my-super-wekan-setup.com

and your TLS certificate/private key (use EC certificates preferably, otherwise adapt ssl_ciphers):

ssl_certificate /usr/local/etc/ssl/wekan.crt;
ssl_certificate_key /usr/local/etc/ssl/wekan.key;

You can either use your own self-signed certificate or PKI, or get a browser-trusted certificate from Let’s Encrypt and automate the renewal using certbot. This is a separate exercise.

Make sure that the proxy_pass also reflects the port Wekan is listening on:

proxy_pass http://127.0.0.1:3001

Create a client_body_temp folder:

mkdir /var/tmp/nginx
chown www:www /var/tmp/nginx

Check the config:

nginx -t

Make nginx a service

This is needed since we did not install nginx with pkg install. You can skip this step if you first pkg install nginx, then overwrite the installation with make install.

nano /usr/local/etc/rc.d/nginx

Copy the following:

#!/bin/sh
# $FreeBSD: branches/2020Q2/www/nginx/files/nginx.in 518572 2019-11-28 10:17:37Z joneum $

# PROVIDE: nginx
# REQUIRE: LOGIN cleanvar
# KEYWORD: shutdown

#
# Add the following lines to /etc/rc.conf to enable nginx:
# nginx_enable (bool):		Set to "NO" by default.
#				Set it to "YES" to enable nginx
# nginx_profiles (str):		Set to "" by default.
#				Define your profiles here.
# nginx_pid_prefix (str):	Set to "" by default.
#				When using profiles manually assign value to "nginx_"
#				for prevent collision with other PIDs names.
# nginxlimits_enable (bool):	Set to "NO" by default.
#				Set it to yes to run `limits $limits_args`
#				just before nginx starts.
# nginx_flags (str):		Set to "" by default.
#				Extra flags passed to start command.
# nginxlimits_args (str):	Default to "-e -U www"
#				Arguments of pre-start limits run.
# nginx_http_accept_enable (bool): Set to "NO" by default.
#				Set to yes to check for accf_http kernel module
#				on start-up and load if not loaded.

. /etc/rc.subr

name="nginx"
rcvar=nginx_enable

start_precmd="nginx_precmd"
restart_precmd="nginx_checkconfig"
reload_precmd="nginx_checkconfig"
configtest_cmd="nginx_checkconfig"
gracefulstop_cmd="nginx_gracefulstop"
upgrade_precmd="nginx_checkconfig"
upgrade_cmd="nginx_upgrade"
command="/usr/local/sbin/nginx"
_pidprefix="/var/run"
pidfile="${_pidprefix}/${name}.pid"
_tmpprefix="/var/tmp/nginx"
required_files=/usr/local/etc/nginx/nginx.conf
extra_commands="reload configtest upgrade gracefulstop"

[ -z "$nginx_enable" ]		&& nginx_enable="NO"
[ -z "$nginxlimits_enable" ]	&& nginxlimits_enable="NO"
[ -z "$nginxlimits_args" ]	&& nginxlimits_args="-e -U www"
[ -z "$nginx_http_accept_enable" ] && nginx_http_accept_enable="NO"

load_rc_config $name

if [ -n "$2" ]; then
	profile="$2"
	if [ "x${nginx_profiles}" != "x" ]; then
		pidfile="${_pidprefix}/${nginx_pid_prefix}${profile}.pid"
		eval nginx_configfile="\${nginx_${profile}_configfile:-}"
		if [ "x${nginx_configfile}" = "x" ]; then
			echo "You must define a configuration file (nginx_${profile}_configfile)"
			exit 1
		fi
		required_files="${nginx_configfile}"
		eval nginx_enable="\${nginx_${profile}_enable:-${nginx_enable}}"
		eval nginx_flags="\${nginx_${profile}_flags:-${nginx_flags}}"
		eval nginxlimits_enable="\${nginxlimits_${profile}_enable:-${nginxlimits_enable}}"
		eval nginxlimits_args="\${nginxlimits_${profile}_args:-${nginxlimits_args}}"
		nginx_flags="-c ${nginx_configfile} -g \"pid ${pidfile};\" ${nginx_flags}"
	else
		echo "$0: extra argument ignored"
	fi
else
	if [ "x${nginx_profiles}" != "x" -a "x$1" != "x" ]; then
		for profile in ${nginx_profiles}; do
			echo "===> nginx profile: ${profile}"
			/usr/local/etc/rc.d/nginx $1 ${profile}
			retcode="$?"
			if [ "0${retcode}" -ne 0 ]; then
				failed="${profile} (${retcode}) ${failed:-}"
			else
				success="${profile} ${success:-}"
			fi
		done
		exit 0
	fi
fi

# tmpfs(5)
nginx_checktmpdir()
{
	if [ ! -d ${_tmpprefix} ] ; then
		install -d -o www -g www -m 755 ${_tmpprefix}
	fi
}

nginx_checkconfig()
{
	nginx_checktmpdir

	echo "Performing sanity check on nginx configuration:"
	eval ${command} ${nginx_flags} -t
}

nginx_gracefulstop()
{
	echo "Performing a graceful stop:"
	sig_stop="QUIT"
	run_rc_command ${rc_prefix}stop $rc_extra_args || return 1
}

nginx_upgrade()
{
	echo "Upgrading nginx binary:"

	reload_precmd=""
	sig_reload="USR2"
	run_rc_command ${rc_prefix}reload $rc_extra_args || return 1

	sleep 1

	echo "Stopping old binary:"

	sig_reload="QUIT"
	pidfile="$pidfile.oldbin"
	run_rc_command ${rc_prefix}reload $rc_extra_args || return 1
}

nginx_precmd() 
{
	if checkyesno nginx_http_accept_enable
	then
		required_modules="$required_modules accf_http accf_data"
	fi

	nginx_checkconfig

	if checkyesno nginxlimits_enable
	then
		eval `/usr/bin/limits ${nginxlimits_args}` 2>/dev/null
	else
		return 0
	fi
}

run_rc_command "$1"

Give it the right permissions:

chmod 555 /usr/local/etc/rc.d/nginx

Enable the service and start it!

sysrc nginx_enable=yes
service nginx start

Access your Wekan

Now visit your Wekan’s URL.

Wekan is running over HTTPS
Wekan is running with TLS 1.3
Everything runs as the latest version \o/

Ultimate test: restart your jail to see if Wekan come back alive automatically.

Post-scriptum notes

Keep wekan, nginx and openssl updated. Unfortunately, the way we installed the latest versions will prevent us from using a simple pkg upgrade to keep everything up-to-date 😦

Sources

https://www.gitmemory.com/issue/wekan/wekan/2662/538795856
https://github.com/wekan/wekan/wiki/Meteor-bundle
https://github.com/wekan/bundle/blob/master/programs/server/packages/webapp.js#L1196
https://github.com/wekan/wekan/issues/2662
https://github.com/wekan/wekan/wiki/Nginx-Webserver-Config

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Twitter picture

You are commenting using your Twitter account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s

%d bloggers like this: