Setting up Mattermost
When you live far away from some of your best friends, you want to find some reasonable ways to stay in touch. After a while, we found out that social media and instant messaging apps are not really an answer to our needs, so we decided to give a chance to something that easily engages everyone. Most, if not all, of us, know about Slack and how it (usually) improves communication within teams that use it. However, if you want to fully own your data and e.g. have access to all archives without high pricing, you might start with some cheaper alternative. It appears there is one - Mattermost Team Edition. And that’s what we decided to try out.
In this post, I want to describe how I configured my Mattermost server step by step. I made some assumptions about used hosts, distros, and setup that simplified the whole process. With those assumptions in mind, I compiled knowledge about basic server security guidelines from Mattermost’s documentation and several tutorials. I’ll show that, as long as you are not afraid of command line, you can quickly setup your own secured chat server from scratch. After that, I will describe how its UX compares to Slack’s and whether or not it’s something right for you.
Originally this post was published on ScalaC blog at 2016-10-27.
Prerequisites
The setup we decided to try out is a single Debian server with TLS secured connection and daily backups of all configs and user data in the cloud. We decided to go with the cheapest Linode server, obtain a TLS certificated from Let’s Encrypt and back up our data using both Linode backup functionality and AWS S3 storage. Altogether, it should be under $13 per month. Since Let’s Encrypt won’t allow you to register a certificate to a domain like li[number]].members.linode.com
, you should also obtain some nice domain if you don’t have one already.
Easy to setup
I was honestly surprised, how easy it was to set up a Mattermost server to the point where it just works. Merely following the tutorial let me set things up in about 2 hours, from the moment I registered a Linode account to the moment when I registered as the first user on working site. Most of that time I spend reading comprehensive tutorials to learn how to tweak things in the future (links to useful materials at the end of the post).
Preparing a server
First, I registered at the Linode site and configured my planning - 1 node with 2 GB of RAM, 1 CPU Core, 24 GB of space and 2TB of transfer. With that, I created a node with a Debian Jessie distribution, set up the root password and booted it.
From Remote Access
tab, I read the server’s IP and used it to log in via ssh (for the sake of this post, let’s assume that the server’s IP is 1.1.1.1
):
ssh root@1.1.1.1
I chose a password that is easy to remember, but long enough that no one should hack into my server during the next hour or so - I wanted to block logging in with root
and password
as soon as possible.
Next, I created a new admin
account on the server and added it to the sudoers group. Then I logged out of the root account:
adduser admin
usermod -a -G sudo admin
exit
Then I generated an RSA key and set up ssh to use it when connecting to my server. I connected to the server using the admin
user (ssh gave me a warning about not being able to connect using RSA key - that is expected as I was just about to configure it):
ssh admin@1.1.1.1
Then I added my public key on the server:
mkdir .ssh
echo "[copy pasted id_rsa.pub]" > .ssh/authorized_keys
chown -R admin:admin .ssh
chmod 700 .ssh
chmod 600 .ssh/authorized_keys
I logged out and logged in to check if logging with the RSA key works as expected. Once I confirmed that I can do that, I could apply first security measures: blocking login by password and logging into the root account.
sudo nano /etc/ssh/sshd_config
I changed the default port, let’s say to 222
(any port < 1024
and != 22
should do) and checked that some options are set like below:
Port 222
...
PermitRootLogin
...
RSAAuthentication yes
PubkeyAuthentication yes
AuthorizedKeysFile %h/.ssh/authorized_keys
...
PermitEmptyPasswords no
...
PasswordAuthentication no
To make sure that everything worked as expected I checked that:
ssh root@1.1.1.1 # would fail
ssh admin@1.1.1.1 # would fail as well
ssh admin@1.1.1.1 -p 222 # would log in using RSA key
Finally, I made sure that the server is up to date:
sudo apt-get update
sudo apt-get upgrade
That is a nice moment to take care of configuring the domain. Usually, DNS propagation can take up to 72 hours and Let’s Encrypt would require us to have it already configured. Considering that I wanted my connection to be secure, outdated DNS would be a blocker. I assume that it is chat.mypage.com
.
And now Mattermost!
Setup database
Official guide for Debian suggests using 3 machines:
- database and files storage,
- Mattermost node,
- nginx load balancer.
However, if your team is really small (as mine), you can get away with a simpler single-server setup. Here nginx will be used to handle SSL termination, HTTP to HTTPS redirection and port mapping.
I decided to go with a PostgreSQL database. I installed it and logged into postgres
:
sudo apt-get install postgresql postgresql-contrib -y
sudo -i -u postgres
pql
and created a database:
CREATE DATABASE mattermost;
CREATE USER mmuser WITH PASSWORD 'mmuser_password';
GRANT ALL PRIVILEGES ON DATABASE mattermost to mmuser;
\q;
view raw
then quit the postgres
account (and a postgres REPL) with exit
.
You don’t have to name your database mattermost
, your user mmuser
and make password mmuser_password
. And I strongly suggest you change them.
Official guide suggests you to make some changes to the PostgreSQL configuration, but as in this example I’m not setting up the DB as a separate server, it is not needed.
Installing Mattermost
I downloaded and extracted the Mattermost installation inside /opt
with the data directory under /opt/mattermost/data
:
wget https://releases.mattermost.com/3.4.0/mattermost-team-3.4.0-linux-amd64.tar.gz
tar -xvzf mattermost-team-3.4.0-linux-amd64.tar.gz
rm mattermost-team-3.4.0-linux-amd64.tar.gz
sudo mkdir -p /opt
sudo mv mattermost /opt
sudo mkdir -p /opt/mattermost/data
According to good practices, I created a dedicated account for the Mattermost service (since I run it as a service) and made my admin
part of the group allowed to mess with the installation:
sudo useradd -r mattermost -U
sudo chown -R mattermost:mattermost /opt/mattermost
sudo chmod -R g+w /opt/mattermost
sudo usermod -aG mattermost admin
Halfway through the setup! I opened the settings with en editor:
nano /opt/mattermost/config/config.json
to change "DataSource"
’s value into "postgres://mmuser:mmuser_password@127.0.0.1:5432/mattermost?sslmode=disable&connect_timeout=10"
. At this point I could check if the server could be actually be run:
cd /opt/mattermost/bin
./platform
When I connected to 1.1.1.1:8065
I could see that the server was working as expected. I used that opportunity to quickly register the first user (which automatically becomes server administrator).
I got back to ssh and terminated the server with ctrl+c. The only thing left was turning it into service. I created the file /etc/systemd/system/mattermost.service
and filled it with:
[Unit]
Description=Mattermost is an open source, self-hosted Slack-alternative
After=syslog.target network.target
[Service]
Type=simple
User=mattermost
Group=mattermost
ExecStart=/opt/mattermost/bin/platform
PrivateTmp=yes
WorkingDirectory=/opt/mattermost
Restart=always
RestartSec=30
LimitNOFILE=49152
[Install]
WantedBy=multi-user.target
and then loaded my newly created service with:
sudo systemctl daemon-reload
sudo systemctl enable mattermost
sudo systemctl start mattermost
With that, I had Mattermost running at (default) port 8065
. Personally, I preferred to change that port (because why not) by editing /etc/systemd/system/mattermost.service
and restarting the service.
Configure nginx
Well, actually at this point I already had some functioning chat service, right? But I didn’t intend to stop on that - it still required passing the port number and the connection was not secure. To address these issues, first I had to install nginx.
sudo apt-get install nginx -y
After accessing http://1.1.1.1
I could confirm that nginx was working. So now I had to configure it to redirect :80
calls to Mattermost. I’ve created file /etc/nginx/sites-available/mattermost
:
server {
server_name chat.mypage.com;
location / {
client_max_body_size 50M;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
proxy_set_header Host $http_host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header X-Frame-Options SAMEORIGIN;
proxy_pass http://127.0.0.1:8065;
}
}
Now I had to link it as an enabled site, remove default config and restart nginx:
sudo rm /etc/nginx/sites-enabled/default
sudo ln -s /etc/nginx/sites-available/mattermost /etc/nginx/sites-enabled/mattermost
sudo service nginx restart
With that, I had a nicely working server… which still needed some security tweaks.
Securing server
At that moment I’d only adjusted ssh logging in a little bit. I had to add a few more things to introduce minimal security of the server. First I started by making sure that one won’t try to attack ssh by brute force. Such an attacker could be banned with fail2ban.
fail2ban
sudo apt-get install fail2ban -y
sudo cp /etc/fail2ban/jail.conf /etc/fail2ban/jail.local
Now we would like to make sure that ssh configuration point to the right port:
...
[ssh]
...
port = 222
filter = sshd
...
[sshd]
...
port = 222
filter = sshd-ddos
...
sudo service fail2ban restart
AFAIR the rest of the default setup on Debian Jessie was good enough. Then I needed to adjust iptables to filter out any other port scanning attempts.
iptables
I created a file with iptables rules with:
sudo mkdir /etc/iptables
sudo nano /etc/iptables/rules
and filled it with the following content:
*filter
:INPUT DROP [0:0]
:FORWARD DROP [0:0]
:OUTPUT DROP [0:0]
# Accept any related or established connections
-I INPUT 1 -m state --state RELATED,ESTABLISHED -j ACCEPT
-I OUTPUT 1 -m state --state RELATED,ESTABLISHED -j ACCEPT
# Allow all traffic on the loopback interface
-A INPUT -i lo -j ACCEPT
-A OUTPUT -o lo -j ACCEPT
# Allow outbound DHCP request - Some hosts (Linode) automatically assign the primary IP
-A OUTPUT -p udp --dport 67:68 --sport 67:68 -j ACCEPT
# Outbound DNS lookups
-A OUTPUT -o eth0 -p udp -m udp --dport 53 -j ACCEPT
# Outbound PING requests
-A OUTPUT -p icmp -j ACCEPT
# Outbound Network Time Protocol (NTP) request
-A OUTPUT -p udp --dport 123 --sport 123 -j ACCEPT
# SSH
-A INPUT -i eth0 -p tcp -m tcp --dport 222 -m state --state NEW -j ACCEPT
# Outbound HTTP
-A OUTPUT -o eth0 -p tcp -m tcp --dport 80 -m state --state NEW -j ACCEPT
-A OUTPUT -o eth0 -p tcp -m tcp --dport 443 -m state --state NEW -j ACCEPT
# Inbound HTTP
-A INPUT -m state --state NEW -p tcp --dport 80 -j ACCEPT
-A INPUT -m state --state NEW -p tcp --dport 443 -j ACCEPT
COMMIT
It is slightly different from the original file from the tutorial I based my setup on - I had to uncomment outbound DHCP request
as I used Linode host and it was essential for the server to work. Another change was allowing inbound HTTP - without it, some Mattermost requests were misbehaving: no message send confirmation, etc.
I had to test these rules before accepting them permanently:
sudo iptables-apply --timeout 60 /etc/iptables/rules
With command above I had 60 seconds to check if I could create new connections to the server. I recommend checking:
- whether a new ssh connection can be made (in another terminal/console),
- whether one can establish a connection to Mattermost server, log in and send a message with confirmation.
If the answer to both questions was true, then I could confirm that iptables were set up correctly and apply these settings permanently. I needed a few tries before it worked as intended, but once I introduced changes described above anything worked as expected. I only needed to add a hook to make sure settings would be applied on network startup:
sudo nano /etc/network/if-pre-up.d/iptables
sudo chmod +x /etc/network/if-pre-up.d/iptables
where /etc/network/if-pre-up.d/iptables
was filled with:
#!/bin/sh
iptables-restore < /etc/iptables/rules
TSL and nginx again
Now the only thing I had to take care of was creating a SSL (TLS) certificate and using it to secure the connection. Let’s Encrypt allows us to get a certificate for free so I gave it a shot. First, I added jessie-backports
to /etc/apt/sources.list
and updated apt cache. With that I was able to install certbot from repositories:
#!/bin/sh
iptables-restore < /etc/iptables/rules
view raw
At this point I already had my domain - I really recommend setting it up early on as I REALLY had to wait about 72 hours before my ISP’s DNS cache had it. And with the settings I want to describe here, I could no longer access the site through IP or Linode’s domain. (A workaround was to move away from my ISP’s DNS cache as it surprisingly worked more reliably and faster, but that’s a tale for another time). I’d like to remind that I assume here that the domain is chat.mypage.com
.
Since certbot’s support for nginx is experimental and not turned on by default I used standalone routine - it required me to turn off nginx for a moment, so that certbot could set up a temporary server, and use it for serving content to Let’s Encrypt servers to prove that the domain truly belongs to me (or specifically that the domain points to the server that cerbot is currently run on).
sudo apt-get install certbot -t jessie-backports -y
Sometimes running certbot requires several attempts - to avoid spamming with unnecessary requests, their server has quotas for certificate requests. If your request was denied, just try again later on. At some point, you will succeed. Once I obtained the certificate, I was able to access it in /etc/letsencrypt/live/chat.mypage.com
. With that, I could finally make nginx use it to secure HTTP(S) connections. I changed the content of /etc/nginx/sites-available/mattermost
to:
sudo service nginx stop
sudo certbot certonly --standalone -d chat.mypage.com
Finally, I started nginx again:
server {
listen 80;
server_name chat.mypage.com;
return 301 https://$server_name$request_uri;
}
server {
listen 443 ssl;
server_name chat.mypage.com;
ssl on;
ssl_certificate /etc/letsencrypt/live/chat.mypage.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/chat.mypage.com/privkey.pem;
ssl_session_timeout 5m;
ssl_protocols TLSv1 TLSv1.1 TLSv1.2;
ssl_ciphers 'EECDH+AESGCM:EDH+AESGCM:AES256+EECDH:AES256+EDH';
ssl_prefer_server_ciphers on;
ssl_session_cache shared:SSL:10m;
location / {
gzip off;
proxy_set_header X-Forwarded-Ssl on;
client_max_body_size 50M;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
proxy_set_header Host $http_host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header X-Frame-Options SAMEORIGIN;
proxy_pass http://127.0.0.1:6324;
}
}
to confirm that http://chat.mypage.com
redirects to https://chat.mypage.com
which in turn uses secure TLS connections.
To make sure that certificate would be renewed every 30 days (Let’s Encrypt gives certificates valid for only 90 days) I also created a cron job:
sudo nano /etc/cron.monthly/certbot_renewal
sudo chmod +x /etc/cron.monthly/certbot_renewal
with content:
#!/bin/sh
certbot renew --standalone --pre-hook "service nginx stop" \
--post-hook "service nginx start" \
--force-renew >> /var/log/certificate_renewal.log 2>&1
Basic security was done.
Back up your data
While the application was ready to use and I could actually start allowing users on the server (after I played around with Mattermost’s settings), I needed to make sure that whatever data would be generated - user’s archives, uploaded files and so on - would be preserved for certain. First thing I could do was enabling backup on Linode - it costs me $2.5 a month and creates a snapshot of my whole node once a week.
But once a week is not enough. What if I wanted to restore content 6 days after the last backup? What if I wanted to move it to another node/server? For that, I needed to find some additional solution.
Upload to AWS S3
I decided to go with AWS S3 storage. Uploading all the config files and user data once a day should give me enough confidence and with AWS’ prices, it is even cheaper than Linode’s backup.
Installation of AWS command line tools is rather simple:
cd /opt
curl "https://s3.amazonaws.com/aws-cli/awscli-bundle.zip" -o "awscli-bundle.zip"
unzip awscli-bundle.zip
sudo ./awscli-bundle/install -i /usr/local/aws -b /usr/local/bin/aws
rm awscli-bundle.zip
For AWS S3 I needed an Amazon account paired with my credit card (for billings). There, in S3 services settings, I created a new bucket for my backups. Finally, I used Security Credentials for creating keys just for my backups. I used them for creating an aws_upload
bash script:
#!/bin/bash
aws_upload () {
AWS_ACCESS_KEY_ID=[my access key] \
AWS_SECRET_ACCESS_KEY=[secret access key] \
AWS_DEFAULT_REGION=[bucket region] \
aws s3 cp "$backup_enc" "s3://my.backup.bucket/$s3_new_backup_name"
}
After sourcing it into my backup script I would be able to upload backup
file as backup-from-${current-date}.tar.gz
with command like:
backup_enc='backup.tgz.enc'
s3_new_backup_name="backup-from-$(date '+%Y-%m-%d').tgz.enc"
aws_upload
Since it keeps credentials in a separate file I could check backup script into git ignoring aws_upload
and avoiding credentials exposure.
Encryption
Because I have limited trust to third-party storages I would still encrypt the backup with something like mcrypt
before the upload.
Installation of mcrypt was even faster:
sudo apt-get install mcrypt -y
Since it requires some key for encryption I decided to generate a random one:
cat /dev/urandom | tr -dc 'a-zA-Z0-9' | fold -w 512 | head -n 1 > ./keyfile
Then I could encrypt/decrypt backups with something like:
cat backup | mcrypt --keyfile ./keyfile > backup_enc
cat backup_enc | mcrypt --decrypt --keyfile ./keyfile > backup
Similarly like with credentials I added the keyfile
to ignored so that the key would not be published in a repository.
My backup scripts
Long story short - backup scripts would just copy all config/data files into one place, archive them and publish on AWS S3. The only exception would be Mattermost DB which would be backed up with pqdump
to create a dump file which can just be read into the DB like any other PostgreSQL script. To avoid typos in hardcoded paths, I extracted all paths into variables and wrote a few helpers to make the scripts more readable (to me :D).
.common.sh
:
#!/bin/bash
# Task runners
run_task () {
echo " - $1"
eval "$2" > /dev/null
if [ $? -eq 0 ]; then
echo " [done]"
else
echo " [failed]"
exit -1
fi
}
run_grouped () {
echo " - $1"
eval "$2"
echo " [done]"
}
run_subtask () {
echo " - $1"
eval "$2" > /dev/null
if [ $? -eq 0 ]; then
echo " [done]"
else
echo " [failed]"
exit -1
fi
}
# Common directories and files
encryption_key="$(dirname $0)/keyfile"
backup_dir=/var/backup
backup_rel=`python -c "import os.path; print os.path.relpath('$backup_dir', '/')"`
backup_tar=/var/backup.tgz
backup_enc=/var/backup.tgz.enc
postgresql_dir=/etc/postgresql/9.4
postgresql_config_original="$postgresql_dir/main/postgresql.conf"
postgresql_config_backup="$backup_dir/postgresql.conf"
postgresql_hba_original="$postgresql_dir/main/pg_hba.conf"
postgresql_hba_backup="$backup_dir/pg_hba.conf"
mm_dir=/opt/mattermost
mm_config_original="$mm_dir/config/config.json"
mm_config_backup="$backup_dir/config.json"
nginx_config_original=/etc/nginx/sites-available/mattermost
nginx_config_backup="$backup_dir/nginx.conf"
ssh_config_original=/etc/ssh/sshd_config
ssh_config_backup="$backup_dir/sshd.config"
iptables_config_original=/etc/iptables/rules
iptables_config_backup="$backup_dir/iptables.rules"
cron_backup_original=/etc/cron.daily/complete_backup
cron_backup_backup="$backup_dir/complete_backup"
cron_certbot_reneval_original=/etc/cron.monthly/certbot_renewal
cron_certbot_reneval_backup="$backup_dir/certbot_renewal"
network_if_pre_up_iptables_original=/etc/network/if-pre-up.d/iptables
network_if_pre_up_iptables_backup="$backup_dir/iptables.if-pre-up"
admins_original=/home
admins_backup="$backup_dir/admins"
database_backup="$backup_dir/db.dump"
local_data_original="$mm_dir/data"
local_data_backup="$backup_dir/files"
certificates_original=/etc/letsencrypt/live/chat.mypage.com
certificates_backup="$backup_dir/certificates"
s3_new_backup_name="backup-from-$(date '+%Y-%m-%d').tgz.enc"
# S3 Upload functions
# Should follow this convention:
#
# aws_upload () {
# AWS_ACCESS_KEY_ID=[my access key] \
# AWS_SECRET_ACCESS_KEY=[secret access key] \
# AWS_DEFAULT_REGION=[bucket region] \
# aws s3 cp "$backup_enc" "s3://chat.mypage.com/$s3_new_backup_name"
# }
source "$(dirname $0)/aws_upload" # should define aws_upload
Then backup is just a matter of defining tasks:
#!/bin/bash
source "$(dirname $0)/.commons.sh"
echo "Backing up Server"
if [ ! -d $backup_dir ]; then
mkdir $backup_dir -p > /dev/null
fi
run_grouped "backing up configs" "$(cat <<-GROUPED
run_subtask "backing up postgres config" "$(cat <<-TASK
cp -fp "$postgresql_config_original" "$postgresql_config_backup" && \
cp -fp "$postgresql_hba_original" "$postgresql_hba_backup"
TASK
)"
run_subtask "backing up MatterMost config" "$(cat <<-TASK
cp -fp "$mm_config_original" "$mm_config_backup"
TASK
)"
run_subtask "backing up Nginx config" "$(cat <<-TASK
cp -fp "$nginx_config_original" "$nginx_config_backup"
TASK
)"
run_subtask "backing up sshd settings" "$(cat <<-TASK
cp -fp "$ssh_config_original" "$ssh_config_backup"
TASK
)"
run_subtask "backing up iptables" "$(cat <<-TASK
cp -fp "$iptables_config_original" "$iptables_config_backup"
TASK
)"
run_subtask "backing up cron jobs" "$(cat <<-TASK
cp -fp "$cron_backup_original" "$cron_backup_backup" && \
cp -fp "$cron_certbot_reneval_original" "$cron_certbot_reneval_backup"
TASK
)"
run_subtask "backing up network jobs" "$(cat <<-TASK
cp -fp "$network_if_pre_up_iptables_original" \
"$network_if_pre_up_iptables_backup"
TASK
)"
GROUPED
)"
run_task "backing up admins" "$(cat <<-TASK
if [ ! -d "$backup_dir/admins" ]; then
mkdir "$backup_dir/admins" -p > /dev/null
fi
cp -rfp "$admins_original"/* "$admins_backup"
TASK
)"
run_grouped "backing up data" "$(cat <<-GROUPED
run_subtask "backing up database" "$(cat <<-TASK
sudo -u postgres pg_dump mattermost > "$database_backup"
TASK
)"
run_subtask "backing up local data" "$(cat <<-TASK
if [ ! -d "$local_data_backup" ]; then
mkdir "$local_data_backup" -p > /dev/null
fi
cp -rfp "$local_data_original"/* "$local_data_backup"
TASK
)"
run_subtask "backing up certificates" "$(cat <<-TASK
if [ ! -d "$certificates_backup" ]; then
mkdir "$certificates_backup" -p > /dev/null
fi
cp -rfp "$certificates_original"/* "$certificates_backup"
TASK
)"
GROUPED
)"
run_grouped "upload backup to S3" "$(cat <<-GROUPED
run_subtask "compressing backup" "$(cat <<-TASK
tar -pcvzf "$backup_tar" -C / "$backup_rel"
TASK
)"
run_subtask "encrypting backup" "$(cat <<-TASK
cat "$backup_tar" | \
mcrypt --keyfile "$encryption_key" > "$backup_enc" 2> /dev/null
TASK
)"
run_subtask "uploading backup" "$(cat <<-TASK
mjk_upload && jelz_upload
TASK
)"
GROUPED
)"
echo "[done]"
Similarly restoring:
#!/bin/bash
source "$(dirname $0)/.commons.sh"
echo "Restoring Server setup"
run_grouped "restoring backup files" "$(cat <<-GROUPED
run_subtask "decrypting backup" "$(cat <<-TASK
cat "$backup_enc" | \
mcrypt --decrypt --keyfile "$encryption_key" > "$backup_tar"
TASK
)"
run_subtask "restoring backup directory" "$(cat <<-TASK
ls "$backup_dir" > /dev/null || tar -pxvfz "$backup_tar" -C /
TASK
)"
GROUPED
)"
run_grouped "restoring configs" "$(cat <<-GROUPED
run_subtask "Restoring postgres config" "$(cat <<-TASK
cp -fp "$postgresql_config_backup" "$postgresql_config_original" && \
cp -fp "$postgresql_hba_backup" "$postgresql_hba_original"
TASK
)"
run_subtask "restoring MatterMost config" "$(cat <<-TASK
cp -fp "$mm_config_backup" "$mm_config_original"
TASK
)"
run_subtask "restoring Nginx config" "$(cat <<-TASK
cp -fp "$nginx_config_backup" "$nginx_config_original"
TASK
)"
run_subtask "restoring sshd settings" "$(cat <<-TASK
cp -fp "$ssh_config_backup" "$ssh_config_original"
TASK
)"
run_subtask "restoring iptables" "$(cat <<-TASK
cp -fp "$iptables_config_backup" "$iptables_config_original"
TASK
)"
run_subtask "restoring cron jobs" "$(cat <<-TASK
cp -fp "$cron_backup_backup" "$cron_backup_original" && \
cp -fp "$cron_certbot_reneval_backup" "$cron_certbot_reneval_original"
TASK
)"
run_subtask "backing up network jobs" "$(cat <<-TASK
cp -fp "$network_if_pre_up_iptables_backup" \
"$network_if_pre_up_iptables_original"
TASK
)"
GROUPED
)"
run_task "restoring admins" "$(cat <<-TASK
cp -rfp "$admins_backup"/* "$admins_original"
TASK
)"
run_grouped "restoring data" "$(cat <<-GROUPED
run_subtask "restoring database" "$(cat <<-TASK
sudo -u postgres \
psql --set ON_ERROR_STOP=on mattermost < "$database_backup" 2> /dev/null
TASK
)"
run_subtask "restoring local data" "$(cat <<-TASK
cp -rfp "$local_data_backup"/* "$local_data_original"
TASK
)"
run_subtask "restoring certificates" "$(cat <<-TASK
if [ ! -d "$certificates_original" ]; then
mkdir "$certificates_original" -p > /dev/null
fi
cp -rfp "$certificates_backup"/* "$certificates_original"
TASK
)"
GROUPED
)"
echo "[done]"
The last thing to do is write a cron job to perform a backup once a day:
sudo nano /etc/cron.daily/complete_backup
sudo chmod +x /etc/cron.daily/complete_backup
#!/bin/sh
/opt/backup/backup.sh >> /var/log/complete_backup.log 2>&1
Mattermost tweaks
Out of the box, Mattermost would have some generic site name. Of all settings that we would like to tweak after installation, that is the only one we can only access by editing /opt/mattermost/config.config.json
(TeamSettings
/SiteName
). Any other config that would be interesting to us could be accessed via the System Console window of Mattermost. That change would require us to restart Mattermost manually to take effect.
Once we change the site name to something more preferable we can also change the title of notification emails (if we’ll use them) and admin’s email address. If we decide to allow push notifications for mobile devices (Android and iOS clients), then we’ll have to decide on the server - we can use test server (TPNS) which does not encrypt our messages (then I would recommend sending generic description with user and channel names) or set up an encrypted push notification server manually - problem with the later is that official clients only support Mattermost servers. So one has to either pay for Mattermost’s encrypted push notification service or fork clients and release one’s own version.
I would also recommend skimming through other options to e.g. regenerate hashes for links and turning off public user registration. If we decide to have several unrelated teams on one server, we might also disable option for cross-team communication.
As for customization, for now, we can only set up global default localization options. An option to change default fonts or themes is still an open ticket on the issue tracker.
Mattermost vs Slack
How does Mattermost actually compare to Slack in action? I won’t deny it - for me, it looks less polished. Of 10 possible fonts, only 2 (Open Sans and Roboto) seems to handle Polish characters correctly and 1 of them (Roboto) seems to fail when we use bold (e.g. when in notifications from some channel). We cannot change the size of the font, and by default, they seem a little bit too small (after zooming in, they seem to be a bit too big). The default theme has annoying sets of colors and I am unable to change these settings without manually editing source code files and preparing my own build.
Some other potentially useful options are also lacking. I am unable to configure the server so that only admin could invite users. Instead, there is a registration link with a hash which could be hidden from the landing page and (optionally) requiring email registration from a fixed domain. As an admin, I can only block any registration to prevent users from sending it to whoever they want. Alternatively, I can disable any registration and use Mattermost command line interface for creating my users (probably the best solution for enterprises). And newly registered users cannot be automatically invited into a predefined list of channels.
Mobile clients seem to be build using an embedded browser - I only tested the Android client, and it has significant startup overhead before I’ll see that my chat is even loading. I attribute it to the browser initialization. When I receive a notification and click it to go to the message, client reloads the browser, even if I just have the said conversation opened. Currently, the client does not have an option to open menus by sliding the screen to the side, though that’s exactly how they open once we click on a menu icon. You can also only have one server configured in your app. (And you cannot remove it easily - when I switched from HTTP
to HTTPS
the app stuck on an infinite server load and I had to reinstall it in order to enter a new URL). Overall, I got the feeling that the mobile clients are slow and heavy (at least Android one is. AFAIK iOS client is built the same way so I assume it shares it’s brother’s issues).
Other quirks that brought my attention was no ability to add reaction emoticons (we use them on Slack heavily) and worse, viewing thumbnails for assigned files - however the former should be solved with 3.5 release. When you remove a message ugly message removed
notice would stay (it disappears after reloading but still). If you spent a lot of time on Slack, these small quirks will add up and you won’t be able to help but notice the difference in the user experience. So does Mattermost have any advantages besides the pricing?
I’d say yes. Even if currently each user would have to apply customization settings manually, they are still able to change the color of almost everything. (I weren’t able to locate the option to disable my own messages’ highlighting - each message you send has a different background than all the rest. This color is also used for highlighting the message your cursor is over it.) Having the ability to respond to a message directly (kind of like on Reddit) might also be a nice thing if 2 parallel discussions emerge on one channel.
When looking at the message bar you’ll notice that it lacks Slack’s plus button, it only has an assignment. There is no distinction on a photo or generic file upload (which is a minus) and there is no option to publish a snippet. The latter is not that much of a problem, though, as Mattermost has better markdown support than Slack, so you can paste your code like on GitHub:
val s = Seq()
and with the ability to respond to a message it works similar to commenting assignments.
It is also useful to share server for several teams because if you want those teams to communicate, you can still allow that. And anyone on the server could then talk to anyone else, without the need to register on each team separately.
Having access to the whole archive out-of-the-box and without additional fees is also a big plus. And if you are really concerned about the security, privacy, and ownership of your data, having your own secured server will always beat trusting third-party vendor locked-in solutions.
Summary
Setting up my own Mattermost server was a great learning experience. And a lot of fun. Though the service is less polished than a Slack, it suits my use case and my pocket. I am looking forward to new releases which will bring new functionalities and get rid of aforementioned quirks.
I cannot recommend this solution to everyone - limited ability to control who can register might be a deal breaker for many companies. However, if you are managing a small team (of friends?) or your company is OK with manually managing users via command line, or the Mattermost server is only available in the intranet or via VPN it might be a quite good Slack alternative. In such a case, though, you should consider subscribing for paid push-notifications to make sure they are distributed via secured connections. Or, if your company is reaally concerned about owning everything, forking mobile clients to use your own dedicated push-notification server (for a while forking the platform might be the only way of providing your own color-scheme defaults). Or perhaps just go straight to Mattermost Enterprise Edition which should ease all those issues and add some additional perks like rebranding. But here I wanted to focus on Team Edition.
In the end, it’s all about the tradeoffs. If your primary concerns are flawless user experience and polished UI - go with Slack. If you want to go an extra mile to make sure that you actually own your data - Mattermost is the way to go.
Links
These are the sources I read while configuring my server. I admit that a major part of my text is just a creative ripoff ;)
- Production Install on Debian Jessie - complete manual on setting up Mattermost on a server. I just skipped all settings required by 3-servers’ setup and replaced cloning Let’s Encrypt with certbot already available in
jessie-backports
. - Securing a Linux Server - a source I used when I configured SSH, iptables, and fail2ban.
Articles I just read through to see if I’ve got everything covered: