Matrix is an open standard for decentralized and end-to-end encrypted communication. It is a collection of servers and services that communicate with each other using a standardized API that synchronizes in real time. It uses homeservers to store account information and chat history. If one home server goes down, other servers can continue communication without issues due to the nature of decentralization. You can use a Matrix home server hosted by someone else or host your own to maintain control over your data.

In this tutorial, you will learn how to install and create a chat server using Synapse, a home server implementation of Matrix. Element is a Matrix web client built using Matrix React SDK. This will allow you to offer Matrix chat on the web. You can also use the server with any other Matrix client. We will also install the Coturn server to enable Voice and Video calling. The Coturn service is optional if you are not interested in using it.

Prerequisites

  1. A server running Debian 12.

  2. A non-sudo user with root privileges.

  3. The uncomplicated Firewall(UFW) is enabled and running.

  4. Fully Qualified Domain Names(FQDN) for Matrix, Element, and Coturn pointing to your server. We will be using matrix.example.com, element.example.com, and coturn.example.com respectively for the three services.

  5. Ensure that everything is updated.

    $ sudo dnf update
    
  • Few packages that your system needs.

    $ sudo dnf install wget curl nano unzip yum-utils policycoreutils-python-utils -y
    

    Some of these packages may already be installed on your system.

Step 1 – Configure Firewall

Before installing any packages, the first step is configuring the firewall to open ports for HTTP, HTTPS, and Synapse. Rocky Linux uses Firewalld Firewall. Check the firewall’s status.

$ sudo firewall-cmd --state
running

The firewall works with different zones, and the public zone is the default one that we will use. List all the services and ports active on the firewall.

$ sudo firewall-cmd --zone=public --list-all

It should show the following output.

public
  target: default
  icmp-block-inversion: no
  interfaces: enp1s0
  sources: 
  services: cockpit dhcpv6-client ssh
  ports: 
  protocols: 
  forward: yes
  masquerade: no
  forward-ports: 
  source-ports: 
  icmp-blocks: 
  rich rules:

Open the HTTP, HTTPS, and Synapse ports in the firewall.

$ sudo firewall-cmd --zone=public --add-port=8448/tcp
$ sudo firewall-cmd --zone=public --add-service=http
$ sudo firewall-cmd --zone=public --add-service=https

Recheck the status of the firewall.

$ sudo firewall-cmd --zone=public --list-all

You should see a similar output.

public
  target: default
  icmp-block-inversion: no
  interfaces: enp1s0
  sources: 
  services: cockpit dhcpv6-client http https ssh
  ports: 8448/tcp
  protocols: 
  forward: yes
  masquerade: no
  forward-ports: 
  source-ports: 
  icmp-blocks: 
  rich rules:

Make all the changes permanent and reload the firewall to enable the changes.

$ sudo firewall-cmd --runtime-to-permanent
$ sudo firewall-cmd --reload

Step 2 – Install and Configure PostgreSQL

Rocky Linux 9 ships with PostgreSQL 13 by default. We will be using PostgreSQL 16 from its official repository.

Install the PostgreSQL RPM repository.

$ sudo dnf install -y https://download.postgresql.org/pub/repos/yum/reporpms/EL-9-x86_64/pgdg-redhat-repo-latest.noarch.rpm

Disable the built-in PostgreSQL module.

$ sudo dnf -qy module disable postgresql

Now, you can install PostgreSQL using the following command.

$ sudo dnf install -y postgresql16-server postgresql16-contrib postgresql16-devel

sudo dnf install postgresql16-devel

export PATH=$PATH:/usr/pgsql-16/bin

The postgresql-contrib package contains some extra utilities.

Verify the version.

$ psql --version
psql (PostgreSQL) 16.1

Initialize the PostgreSQL database.

$ sudo /usr/pgsql-16/bin/postgresql-16-setup initdb
Initializing database ... OK

Enable the PostgreSQL service.

$ sudo systemctl enable postgresql-16

Start the PostgreSQL service.

$ sudo systemctl start postgresql-16

Check the status of the PostgreSQL service.

$ sudo systemctl status postgresql-16
? postgresql-16.service - PostgreSQL 16 database server
     Loaded: loaded (/usr/lib/systemd/system/postgresql-16.service; enabled; preset: disabled)
     Active: active (running) since Tue 2024-01-30 11:55:26 UTC; 10s ago
       Docs: https://www.postgresql.org/docs/16/static/
    Process: 9610 ExecStartPre=/usr/pgsql-16/bin/postgresql-16-check-db-dir ${PGDATA} (code=exited, status=0/SUCCESS)
   Main PID: 9615 (postgres)
      Tasks: 7 (limit: 12225)
     Memory: 17.4M
        CPU: 98ms
     CGroup: /system.slice/postgresql-16.service
             ??9615 /usr/pgsql-16/bin/postgres -D /var/lib/pgsql/16/data/
             ??9616 "postgres: logger "
             ??9617 "postgres: checkpointer "
             ??9618 "postgres: background writer "
             ??9620 "postgres: walwriter "
             ??9621 "postgres: autovacuum launcher "
             ??9622 "postgres: logical replication launcher "

Jan 30 11:55:26 matrix.example.com systemd[1]: Starting PostgreSQL 16 database server...
Jan 30 11:55:26 matrix.example.com postgres[9615]: 2024-01-30 11:55:26.482 UTC [9615] LOG:  redirecting log output to logging collector process
Jan 30 11:55:26 matrix.example.com postgres[9615]: 2024-01-30 11:55:26.482 UTC [9615] HINT:  Future log output will appear in directory "log".
Jan 30 11:55:26 matrix.example.com systemd[1]: Started PostgreSQL 16 database server.

You can see that the service is enabled and running by default.

Log in to the postgres system account.

$ sudo -su postgres

Create a new database user and a database for PostgreSQL. You will be prompted for a password when creating the user.

$ createuser --pwprompt synapse
$ createdb --encoding=UTF8 --locale=C --template=template0 --owner=synapse synapsedb

Exit the postgres account.

$ exit

Step 3 – Install Matrix Synapse

Matrix Synapse doesn’t have a pre-built package for CentOS-based distributions like Rocky Linux. Therefore, we will install it as a Python module from PyPI.

The first step is to install the prerequisites required for Synapse to work. We also need the EPEL repository which includes the python3-virtualenv package.

$ sudo dnf install epel-release
$ sudo dnf install --enablerepo=crb libtiff-devel libjpeg-devel libzip-devel 
                 freetype-devel libwebp-devel libxml2-devel libxslt-devel 
                 libpq-devel  python3-virtualenv libffi-devel openssl-devel 
                 python3-devel libicu-devel
$ sudo dnf groupinstall "Development Tools"
$ sudo dnf install postgresql16-devel

Create a directory for Synapse.

$ mkdir -p ~/synapse

Create a Python virtual environment in the said directory.

$ virtualenv -p python3 ~/synapse/env

Activate the virtual environment.

$ source ~/synapse/env/bin/activate

Ensure that you have the latest version of pip and setuptools.

(env) $ pip install --upgrade pip
(env) $ pip install --upgrade setuptools

Install Matrix Synapse.

(env) $ pip install "matrix-synapse[postgres]"

This will download Synapse from PyPI and install it, along with the Python libraries it uses into a virtual environment under the ~/synapse/env folder. It will also install the PostgreSQL Python library required for Synapse to connect with the PostgreSQL server. The above command is a combination of the pip install matrix-synapse and pip install psycopg2 commands.

The next step is to generate a configuration file with some basic settings. Switch to the Synapse directory.

(env) $ cd ~/synapse

Run the following command to generate the configuration file.

(env) $ python -m synapse.app.homeserver 
    --server-name matrix.example.com 
    --config-path homeserver.yaml 
    --generate-config 
    --report-stats=[yes|no]

The value of the --server-name variable forms a part of your Matrix ID. Enter your Matrix domain name as its value. This will act as your homeserver address.

You can choose whether or not you wish to send anonymized statistics about your homeserver back to Matrix via the --report-stats variable. Choose yes or no as per your choice.

You will get the following output.

Generating config file homeserver.yaml
Generating log config file /home/navjot/synapse/matrix.example.com.log.config which will log to /home/navjot/synapse/homeserver.log
Generating signing key file /home/navjot/synapse/matrix.example.com.signing.key
A config file has been generated in 'homeserver.yaml' for server name 'matrix.example.com'. Please review this file and customise it to your needs.

You can change these settings later in the file ~/synapse/homeserver.yaml or /home//synapse/homeserver.yaml file. The keys generated by the above command will allow your homeserver to identify itself to other homeserver.

It is time to run the Matrix server. Make sure you are inside the Synapse directory and the virtual environment is active.

Start the homeserver.

(env) $ synctl start

You should see a similar output.

This server is configured to use 'matrix.org' as its trusted key server via the
'trusted_key_servers' config option. 'matrix.org' is a good choice for a key
server since it is long-lived, stable and trusted. However, some admins may
wish to use another server for this purpose.

To suppress this warning and continue using 'matrix.org', admins should set
'suppress_key_server_warning' to 'true' in homeserver.yaml.
--------------------------------------------------------------------------------
started synapse.app.homeserver(homeserver.yaml)

To stop the server, run the following command.

(env) $ synctl stop

Deactivate the virtual environment.

(env) $ deactivate

Step 4 – Install Nginx

For the production environment, it is recommended to run the Synapse server using an Nginx proxy.

Rocky Linux 9 ships with an older version of Nginx. You need to use the official Nginx repository to install the latest version.

Create and open the file /etc/yum.repos.d/nginx.repo for editing.

$ sudo nano /etc/yum.repos.d/nginx.repo

Paste the following code in it.

[nginx-stable]
name=nginx stable repo
baseurl=http://nginx.org/packages/centos/$releasever/$basearch/
gpgcheck=1
enabled=1
gpgkey=https://nginx.org/keys/nginx_signing.key
module_hotfixes=true

[nginx-mainline]
name=nginx mainline repo
baseurl=http://nginx.org/packages/mainline/centos/$releasever/$basearch/
gpgcheck=1
enabled=0
gpgkey=https://nginx.org/keys/nginx_signing.key
module_hotfixes=true

Once you are finished, save the file by pressing Ctrl X and entering Y when prompted.

We will be installing the Nginx mainline so enable the package for it.

$ sudo dnf config-manager --enable nginx-mainline

Install Nginx.

$ sudo dnf install nginx -y

Verify the installation.

$ nginx -v
nginx version: nginx/1.25.3

Enable and start the Nginx server service.

$ sudo systemctl enable nginx --now

Check the status of the service.

$ sudo systemctl status nginx
? nginx.service - nginx - high performance web server
     Loaded: loaded (/usr/lib/systemd/system/nginx.service; enabled; preset: disabled)
     Active: active (running) since Tue 2024-01-30 12:50:34 UTC; 4s ago
       Docs: http://nginx.org/en/docs/
    Process: 10810 ExecStart=/usr/sbin/nginx -c /etc/nginx/nginx.conf (code=exited, status=0/SUCCESS)
   Main PID: 10811 (nginx)
      Tasks: 3 (limit: 12225)
     Memory: 3.1M
        CPU: 24ms
     CGroup: /system.slice/nginx.service
             ??10811 "nginx: master process /usr/sbin/nginx -c /etc/nginx/nginx.conf"
             ??10812 "nginx: worker process"
             ??10813 "nginx: worker process"

Step 5 – Install SSL

We need to install Certbot to generate the SSL certificate. We will use the Snapd package installer for that. Since Rocky Linux doesn’t ship with it, install the Snapd installer. It requires the EPEL (Extra Packages for Enterprise Linux) repository to work. But since we already installed it in step 3, we can directly move ahead.

Install Snapd package.

$ sudo dnf install snapd -y

Enable and Start the Snap service.

$ sudo systemctl enable snapd --now

Install the Snap core package, and ensure that your version of Snapd is up to date.

$ sudo snap install core
$ sudo snap refresh core

Create necessary links for Snapd to work.

$ sudo ln -s /var/lib/snapd/snap /snap
$ echo 'export PATH=$PATH:/var/lib/snapd/snap/bin' | sudo tee -a /etc/profile.d/snapd.sh

Install Certbot.

$ sudo snap install --classic certbot

Use the following command to ensure that the Certbot command can be run by creating a symbolic link to the /usr/bin directory.

$ sudo ln -s /snap/bin/certbot /usr/bin/certbot

Check the Certbot version.

$ certbot --version
certbot 2.8.0

Run the following command to generate an SSL Certificate.

$ sudo certbot certonly --nginx --agree-tos --no-eff-email --staple-ocsp --preferred-challenges http -m [email protected] -d matrix.example.com

The above command will download a certificate to the /etc/letsencrypt/live/matrix.example.com directory on your server.

Generate a Diffie-Hellman group certificate.

$ sudo openssl dhparam -dsaparam -out /etc/ssl/certs/dhparam.pem 4096

Check the Certbot renewal scheduler service.

$ systemctl list-timers

You will find snap.certbot.renew.service as one of the services scheduled to run.

NEXT                        LEFT         LAST                        PASSED      UNIT                         ACTIVATES                   ----------------------------------------------------------------------------------------------------------------------------------  
Tue 2024-01-30 14:37:50 UTC 29min left   Tue 2024-01-30 13:05:13 UTC 1h 3min ago dnf-makecache.timer          dnf-makecache.service
Tue 2024-01-30 15:13:00 UTC 1h 4min left -                           -           snap.certbot.renew.timer     snap.certbot.renew.service
Wed 2024-01-31 00:00:00 UTC 9h left      Tue 2024-01-30 06:35:44 UTC 7h ago      logrotate.timer              logrotate.service

Do a dry run of the process to check whether the SSL renewal is working fine.

$ sudo certbot renew --dry-run

If you see no errors, you are all set. Your certificate will renew automatically.

Step 6 – Configure SELinux

SELinux prevents you from running a system service where the binary is in a user’s home directory. For this reason, if we create a service file for Synapse, it will be blocked by SELinux. To get around the issue, we need to run the following command to allow the Python binary to be run as a system service.

$ sudo chcon -R -t bin_t /home/username/synapse/env/bin/python

Apply the policy to allow connections to be made to outside hosts.

$ sudo setsebool -P httpd_can_network_connect 1

Apply the policy to allow Nginx to give access to PostgreSQL.

$ sudo setsebool -P httpd_can_network_connect_db 1

Step 7 – Create Matrix Systemd Service

The Synapse service status doesn’t get saved across a reboot which means if you restart the server, you will need to start the server manually. To get around this issue, we need to create the Systemd file for it.

Before proceeding with that, make sure the Synapse service is stopped.

$ cd ~/synapse
$ source env/bin/activate
(env) $ synctl stop
(env) $ deactivate

Create and open the matrix-synapse.service file in the /etc/systemd/system/ directory for editing.

$ sudo nano /etc/systemd/system/matrix-synapse.service

Paste the following code in it. Replace the username variable with your Linux system user under which you installed Synapse.

# This assumes that Synapse has been installed by a user named username.
# This assumes that Synapse has been installed in a virtualenv in
# the user's home directory: `/home/username/synapse/env`.

[Unit]
Description=Synapse Matrix homeserver
After=postgresql-16.service

[Service]
Type=notify
NotifyAccess=main
ExecReload=/bin/kill -HUP $MAINPID
Restart=on-abort

WorkingDirectory=/home/username/synapse
ExecStart=/home/username/synapse/env/bin/python -m synapse.app.homeserver --config-path=/home/username/synapse/homeserver.yaml
SyslogIdentifier=matrix-synapse

# adjust the cache factor if necessary
# Environment=SYNAPSE_CACHE_FACTOR=2.0

[Install]
WantedBy=multi-user.target

Once finished, save the file by pressing Ctrl X and entering Y when prompted.

Reload the service daemon.

$ sudo systemctl daemon-reload

Enable the Matrix Synapse service.

$ sudo systemctl enable matrix-synapse

Start the Matrix Synapse service.

$ sudo systemctl start matrix-synapse

Check the service status.

$ sudo systemctl status matrix-synapse

You should see the following output.

? matrix-synapse.service - Synapse Matrix homeserver
     Loaded: loaded (/etc/systemd/system/matrix-synapse.service; enabled; preset: disabled)
     Active: active (running) since Tue 2024-01-30 14:20:20 UTC; 5s ago
   Main PID: 14687 (python)
      Tasks: 8 (limit: 12225)
     Memory: 77.6M
        CPU: 3.527s
     CGroup: /system.slice/matrix-synapse.service
             ??14687 /home/navjot/synapse/env/bin/python -m synapse.app.homeserver --config-path=/home/navjot/synapse/homeserver.yaml

Jan 30 14:20:16 matrix.example.com systemd[1]: Starting Synapse Matrix homeserver...
Jan 30 14:20:19 matrix.example.com matrix-synapse[14687]: This server is configured to use 'matrix.org' as its trusted key server via the
Jan 30 14:20:19 matrix.example.com matrix-synapse[14687]: 'trusted_key_servers' config option. 'matrix.org' is a good choice for a key
Jan 30 14:20:19 matrix.example.com matrix-synapse[14687]: server since it is long-lived, stable and trusted. However, some admins may
Jan 30 14:20:19 matrix.example.com matrix-synapse[14687]: wish to use another server for this purpose.
Jan 30 14:20:19 matrix.example.com matrix-synapse[14687]: To suppress this warning and continue using 'matrix.org', admins should set
Jan 30 14:20:19 matrix.example.com matrix-synapse[14687]: 'suppress_key_server_warning' to 'true' in homeserver.yaml.
Jan 30 14:20:19 matrix.example.com matrix-synapse[14687]: --------------------------------------------------------------------------------
Jan 30 14:20:20 matrix.example.com systemd[1]: Started Synapse Matrix homeserver.

Step 8 – Configure Matrix Synapse

Before proceeding ahead, create a secret registration key first. The key should be secured because it will allow anyone to register a new user, even if registration is disabled.

$ echo "registration_shared_secret: '$(cat /dev/urandom | tr -cd '[:alnum:]' | fold -w 256 | head -n 1)'"
registration_shared_secret: '1y75ja0RUxvbWcS6SdZhakenukUwHHEjOXWC9Mu3FpO0QenOnpKRsc6NBZSxuzPcHYsOEuIQziwVjSZcP87dlWK4ZkIGYniurMqNsCYL4xg5xXs4bJDuTJH2CUXab4U9liv399lmkIZFaMpJCLxV9lVWB9mKHILYsjeLgGY5wAARv1SiK07bFsQOwKJGFqIvsUXmxymx5QCNDzTHw8R4ShqZ7elnnZrbdYk4r2f7qogERNHvQaRV7IEYUIOtMhVP'

Copy the output from the above line and save it.

You can configure the Matrix server via the file /home/username/synapse/homeserver.yaml. Open it for editing.

$ nano ~/synapse/homeserver.yaml

Paste the output of the registration key command at the bottom of the file.

By default, Synapse is configured to use the SQLite database. We need to edit it to add PostgreSQL credentials.

Find the following section in the file and comment it out as shown.

#database:
#  name: sqlite3
#  args:
#    database: /home/navjot/synapse/homeserver.db

Add the following section below it. Replace the your-password field with the PostgreSQL user password you created in step 3. Replace localhost with the IP address of your server, if you are hosting the database elsewhere.

database:
  name: psycopg2
  args:
    user: synapse
    password: 'your-password'
    database: synapsedb
    host: localhost
    cp_min: 5
    cp_max: 10

By default, Synapse enables presence indicators that show if a person is online. It can cause high CPU usage, therefore you can disable it. Paste the following line at the bottom.

presence:
  enabled: false

Once finished, save the file by pressing Ctrl X and entering Y when prompted.

Create a new matrix user. You will be asked for your username and password. Since this is the first user we are creating, type yes when asked whether to make the user an administrator. You will need to activate the virtual environment to create the user.

$ cd ~/synapse
$ source env/bin/activate
(env) $ register_new_matrix_user -c homeserver.yaml http://localhost:8008
New user localpart [navjot]: navjot
Password: 
Confirm password: 
Make admin [no]: yes
Sending registration request...
Success!
(env) $ deactivate

You can see that we used port 8008 in the above command. That is because it is the default port that Matrix Synapse listens on.

By default, registration of new users is disabled which means you need to register every user manually like above. If you want to open public registrations, you can do so by editing the homeserver.yaml file.

Open it for editing again.

$ nano ~/synapse/homeserver.yaml

Paste the following line at the bottom.

enable_registration: true

By default, Synapse does not allow registrations without email verification. To enable email verification, paste the following lines.

registrations_require_3pid:
  - email

email:
  smtp_host: mail.example.com
  smtp_port: 587

  # If mail server has no authentication, skip these 2 lines
  smtp_user: '[email protected]'
  smtp_pass: 'password'

  # Optional, require encryption with STARTTLS
  require_transport_security: true

  app_name: 'HowtoForge Example Chat'  # defines value for %(app)s in notif_from and email subject
  notif_from: "%(app)s <[email protected]>"

To disable email verification, paste the following line instead.

enable_registration_without_verification: true

Once finished, save the file by pressing Ctrl X and entering Y when prompted.

Restart Synapse to apply the configuration.

$ sudo systemctl restart matrix-synapse

Step 9 – Configure Nginx

Open the file /etc/nginx/nginx.conf for editing.

$ sudo nano /etc/nginx/nginx.conf

Add the following line before the line include /etc/nginx/conf.d/*.conf;.

server_names_hash_bucket_size 64;

Save the file by pressing Ctrl X and entering Y when prompted.

Create and open the file /etc/nginx/conf.d/synapse.conf for editing.

$ sudo nano /etc/nginx/conf.d/synapse.conf

Paste the following code in it.

# enforce HTTPS
server {
    # Client port
    listen 80;
    listen [::]:80;
    server_name matrix.example.com;
    return 301 https://$host$request_uri;
}

server {
    server_name matrix.example.com;

    # Client port
    listen 443 ssl;
    listen [::]:443 ssl;
    http2 on;

    # Federation port
    listen 8448 ssl default_server;
    listen [::]:8448 ssl default_server;

    access_log  /var/log/nginx/synapse.access.log;
    error_log   /var/log/nginx/synapse.error.log;

    # TLS configuration
    ssl_certificate /etc/letsencrypt/live/matrix.example.com/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/matrix.example.com/privkey.pem;
    ssl_trusted_certificate /etc/letsencrypt/live/matrix.example.com/chain.pem;
    ssl_session_timeout 1d;
    ssl_session_cache shared:MozSSL:10m;
    ssl_session_tickets off;
    ssl_prefer_server_ciphers on;
    ssl_stapling on;
    ssl_stapling_verify on;
    ssl_dhparam /etc/ssl/certs/dhparam.pem;
    resolver 1.1.1.1 1.0.0.1 [2606:4700:4700::1111] [2606:4700:4700::1001] 8.8.8.8 8.8.4.4 [2001:4860:4860::8888] [2001:4860:4860::8844] valid=60s;
    resolver_timeout 2s;

    ssl_protocols TLSv1.2 TLSv1.3;
    ssl_ciphers ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:DHE-RSA-AES128-GCM-SHA256:DHE-RSA-AES256-GCM-SHA384;

    tcp_nopush on;
    gzip on;

    location ~ ^(/_matrix|/_synapse/client) {
            proxy_pass http://localhost:8008;
            proxy_http_version 1.1;

            proxy_set_header X-Forwarded-For $remote_addr;
            proxy_set_header X-Forwarded-Proto $scheme;
            proxy_set_header Host $host;

            # Nginx by default only allows file uploads up to 1M in size
            # Increase client_max_body_size to match max_upload_size defined in homeserver.yaml
            client_max_body_size 50M;
    }
}

Save the file by pressing Ctrl X and entering Y when prompted once finished. The above configuration works on the assumption that the IP address of the domains example.com and matrix.example.com are pointing to the same server. If they are not, then use the following configuration file for the example.com server.

server {
    server_name example.com;

    listen 443 ssl http2;
    listen [::]:443 ssl http2;

    # TLS configuration
    ssl_certificate /etc/letsencrypt/live/matrix.example.com/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/matrix.example.com/privkey.pem;
    ssl_trusted_certificate /etc/letsencrypt/live/matrix.example.com/chain.pem;
    ssl_session_timeout 1d;
    ssl_session_cache shared:MozSSL:10m;
    ssl_session_tickets off;
    ssl_prefer_server_ciphers on;
    ssl_stapling on;
    ssl_stapling_verify on;
    ssl_dhparam /etc/ssl/certs/dhparam.pem;
    resolver 1.1.1.1 1.0.0.1 [2606:4700:4700::1111] [2606:4700:4700::1001] 8.8.8.8 8.8.4.4 [2001:4860:4860::8888] [2001:4860:4860::8844] valid=60s;
    resolver_timeout 2s;

    ssl_protocols TLSv1.2 TLSv1.3;
    ssl_ciphers ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:DHE-RSA-AES128-GCM-SHA256:DHE-RSA-AES256-GCM-SHA384;

    # Redirect
    location ~ ^(/_matrix|/_synapse/client) {
            return 301 "https://matrix.example.com$request_uri";
    }

    # Client homeserver autodiscovery
    location /.well-known/matrix/client {
        default_type application/json;
        add_header Access-Control-Allow-Origin *;

        return 200 '{ "m.homeserver": { "base_url": "https://matrix.example.com" } }';
    }

    # Domain delegation
    location /.well-known/matrix/server {
        default_type application/json;
        add_header Access-Control-Allow-Origin *;

        return 200 '{ "m.server": "matrix.example.com" }';
    }
}

Verify the Nginx configuration file syntax.

$ sudo nginx -t
nginx: the configuration file /etc/nginx/nginx.conf syntax is ok
nginx: configuration file /etc/nginx/nginx.conf test is successful

Restart the Nginx service.

$ sudo systemctl restart nginx

Step 10 – Install Coturn

You will need to install a Traversal Using Relays around the NAT (TURN) server to enable voice and video calling. For this purpose, we will install the Coturn package. If you don’t need this functionality, you can skip this step.

Install Coturn.

$ sudo dnf install coturn

Open the TURN and UDP ports.

$ sudo firewall-cmd --zone=public --add-port=3478/{tcp,udp}
$ sudo firewall-cmd --zone=public --add-port=5349/{tcp,udp}
$ sudo firewall-cmd --zone=public --add-port=49152-65535/udp
$ sudo firewall-cmd --runtime-to-permanent
$ sudo firewall-cmd --reload

Generate an SSL certificate for Turn (coturn.example.com).

$ sudo certbot certonly --nginx --agree-tos --no-eff-email --staple-ocsp --preferred-challenges http -m [email protected] -d coturn.example.com

Generate an authentication secret for Coturn.

$ echo "static-auth-secret=$(cat /dev/urandom | tr -cd '[:alnum:]' | fold -w 256 | head -n 1)"
static-auth-secret=u3ksgJ9X876sFMO00A8KabXwZvzVwCmV30wWvZ7DzGiepRwNRYSCTYzf2E8olNzdDHu7oO3XpT3l5bv5mVdhXEQ3pnoTWBquSVPFFBJtyD6y9gBxiMaD2TYTD2WohQLE9s7OKJVpkDeKTnZQnkmjrFadE3M9DRxPL1W5Lgdg6XLLKABgb5XGkutRgkJOV0JRH4sBYsot63fWq9mcGTm6nAMdIycvDVDOs7vhoeqRzcTbMF0wsfAHVJNhXpGGFDyZ

Open the configuration file for editing.

$ sudo nano /etc/coturn/turnserver.conf

We will go through the file and enable the settings that we need. First, enable the authentication method by uncommenting the use-auth-secret line.

# Be aware that use-auth-secret overrides some parts of lt-cred-mech.
# The use-auth-secret feature depends internally on lt-cred-mech, so if you set
# this option then it automatically enables lt-cred-mech internally
# as if you had enabled both.
#
# Note that you can use only one auth mechanism at the same time! This is because,
# both mechanisms conduct username and password validation in different ways.
#
# Use either lt-cred-mech or use-auth-secret in the conf
# to avoid any confusion.
#
use-auth-secret

Next, we will configure the static-auth-secret variable with the value we created above. We will also need this secret to configure Synapse later on.

# 'Static' authentication secret value (a string) for TURN REST API only.
# If not set, then the turn server
# will try to use the 'dynamic' value in the turn_secret table
# in the user database (if present). The database-stored  value can be changed on-the-fly
# by a separate program, so this is why that mode is considered 'dynamic'.
#
static-auth-secret=u3ksgJ9X876sFMO00A8KabXwZvzVwCmV30wWvZ7DzGiepRwNRYSCTYzf2E8olNzdDHu7oO3XpT3l5bv5mVdhXEQ3pnoTWBquSVPFFBJtyD6y9gBxiMaD2TYTD2WohQLE9s7OKJVpkDeKTnZQnkmjrFadE3M9DRxPL1W5Lgdg6XLLKABgb5XGkutRgkJOV0JRH4sBYsot63fWq9mcGTm6nAMdIycvDVDOs7vhoeqRzcTbMF0wsfAHVJNhXpGGFDyZ

The next value we need to change is the realm. It refers to the domain we picked for Coturn.

# The default realm to be used for the users when no explicit
# origin/realm relationship is found in the database, or if the TURN
# server is not using any database (just the commands-line settings
# and the userdb file). Must be used with long-term credentials
# mechanism or with TURN REST API.
#
# Note: If the default realm is not specified, then realm falls back to the host domain name.
#       If the domain name string is empty, or set to '(None)', then it is initialized as an empty string.
#
realm=coturn.example.com

Next, define the number of sessions and sessions per user. Choose a value that works for your server.

# Per-user allocation quota.
# default value is 0 (no quota, unlimited number of sessions per user).
# This option can also be set through the database, for a particular realm.
#
user-quota=12

# Total allocation quota.
# default value is 0 (no quota).
# This option can also be set through the database, for a particular realm.
#
total-quota=1200

Next, we need to disable TCP since VOIP is UDP.

# Uncomment if no TCP relay endpoints are allowed.
# By default TCP relay endpoints are enabled (like in RFC 6062).
#
no-tcp-relay

We also need to configure the location of the certificates we created for Coturn.

# Certificate file.
# Use an absolute path or path relative to the
# configuration file.
# Use PEM file format.
#
cert=/etc/letsencrypt/live/coturn.example.com/fullchain.pem

# Private key file.
# Use an absolute path or path relative to the
# configuration file.
# Use PEM file format.
#
pkey=/etc/letsencrypt/live/coturn.example.com/privkey.pem

Next, we need to disable traffic to private IP ranges and disallow peers on multicast addresses to improve security.

# Flag that can be used to disallow peers on well-known broadcast addresses (224.0.0.0 and above, and FFXX:*).
# This is an extra security measure.
#
no-multicast-peers

...............

# Option to allow or ban specific ip addresses or ranges of ip addresses.
# If an ip address is specified as both allowed and denied, then the ip address is
# considered to be allowed. This is useful when you wish to ban a range of ip
# addresses, except for a few specific ips within that range.
#
# This can be used when you do not want users of the turn server to be able to access
# machines reachable by the turn server, but would otherwise be unreachable from the
# internet (e.g. when the turn server is sitting behind a NAT)
#
# Examples:
# denied-peer-ip=83.166.64.0-83.166.95.255
# allowed-peer-ip=83.166.68.45
denied-peer-ip=0.0.0.0-0.255.255.255
denied-peer-ip=10.0.0.0-10.255.255.255
denied-peer-ip=100.64.0.0-100.127.255.255
denied-peer-ip=127.0.0.0-127.255.255.255
denied-peer-ip=169.254.0.0-169.254.255.255
denied-peer-ip=172.16.0.0-172.31.255.255
denied-peer-ip=192.0.0.0-192.0.0.255
denied-peer-ip=192.0.2.0-192.0.2.255
denied-peer-ip=192.88.99.0-192.88.99.255
denied-peer-ip=192.168.0.0-192.168.255.255
denied-peer-ip=198.18.0.0-198.19.255.255
denied-peer-ip=198.51.100.0-198.51.100.255
denied-peer-ip=203.0.113.0-203.0.113.255
denied-peer-ip=240.0.0.0-255.255.255.255
denied-peer-ip=::1
denied-peer-ip=64:ff9b::-64:ff9b::ffff:ffff
denied-peer-ip=::ffff:0.0.0.0-::ffff:255.255.255.255
denied-peer-ip=100::-100::ffff:ffff:ffff:ffff
denied-peer-ip=2001::-2001:1ff:ffff:ffff:ffff:ffff:ffff:ffff
denied-peer-ip=2002::-2002:ffff:ffff:ffff:ffff:ffff:ffff:ffff
denied-peer-ip=fc00::-fdff:ffff:ffff:ffff:ffff:ffff:ffff:ffff
denied-peer-ip=fe80::-febf:ffff:ffff:ffff:ffff:ffff:ffff:ffff

Once finished, save the file by pressing Ctrl X and entering Y when prompted.

Restart Coturn to apply the configuration.

$ sudo systemctl restart coturn

Open the Synapse configuration (homeserver.yaml) file for adding Coturn settings.

$ nano ~/synapse/homeserver.yaml

Paste the following lines in it. Replace turn_shared_secret value with the value of static-auth-secret from the etccoturnturnserver.conf file.

turn_uris: [ "turn:coturn.example.com?transport=udp", "turn:coturn.example.com?transport=tcp" ]
turn_shared_secret: 'static-auth-secret'
turn_user_lifetime: 86400000
turn_allow_guests: True

# vim:ft=yaml

Save the file by pressing Ctrl X and entering Y when prompted.

Restart Synapse to apply the changes.

$ sudo systemctl restart matrix-synapse

Step 11 – Access Matrix

You can access Matrix Chat using Element’s web client at https://app.element.io. Click the Sign in button to proceed.

<img alt="Element Web Login Page" data-ezsrc="https://kirelos.com/wp-content/uploads/2024/06/echo/element-web-login.png66696fa746a7d.jpg" ezimgfmt="rs rscb10 src ng ngcb9" height="551" loading="lazy" referrerpolicy="no-referrer" src="data:image/svg xml,” width=”709″>

Click the Edit link under Homeserver. Enter matrix.example.com as your homeserver.

<img alt="Element Client Custom Homeserver" data-ezsrc="https://kirelos.com/wp-content/uploads/2024/06/echo/element-client-custom-homeserver.png66696fa77624c.jpg" ezimgfmt="rs rscb10 src ng ngcb9" height="435" loading="lazy" referrerpolicy="no-referrer" src="data:image/svg xml,” width=”514″>

If the client detects your homeserver correctly, the boundary and the text will become green colored else it will be shown in red. Click Continue to proceed.

<img alt="Element Custom Homeserver Login" data-ezsrc="https://kirelos.com/wp-content/uploads/2024/06/echo/element-custom-homeserver-login.png66696fa79b6f9.jpg" ezimgfmt="rs rscb10 src ng ngcb9" height="510" loading="lazy" referrerpolicy="no-referrer" src="data:image/svg xml,” width=”750″>

Enter the username and password you created in step 8 while configuring Synapse. Click the Sign in button to log in. You will be asked to create a secure and encrypted backup.

<img alt="Matrix Secure Backup" data-ezsrc="https://kirelos.com/wp-content/uploads/2024/06/echo/matrix-secure-backup.png66696fa7d0509.jpg" ezimgfmt="rs rscb10 src ng ngcb9" height="464" loading="lazy" referrerpolicy="no-referrer" src="data:image/svg xml,” width=”606″>

Select the Enter a Security Phrase option to create a security phrase that will be required every time you log in. Click Continue to proceed.

<img alt="Element Security Phrase" data-ezsrc="https://kirelos.com/wp-content/uploads/2024/06/echo/element-security-phrase.png66696fa7f062f.jpg" ezimgfmt="rs rscb10 src ng ngcb9" height="276" loading="lazy" referrerpolicy="no-referrer" src="data:image/svg xml,” width=”602″>

Enter a security phrase and click the Continue button to proceed. You will be asked to confirm it again on the next screen.

<img alt="Element Confirm Security Phrase" data-ezsrc="https://kirelos.com/wp-content/uploads/2024/06/echo/element-confirm-security-phrase.png66696fa81dcc7.jpg" ezimgfmt="rs rscb10 src ng ngcb9" height="254" loading="lazy" referrerpolicy="no-referrer" src="data:image/svg xml,” width=”607″>

Enter the phrase again and click Continue to proceed.

<img alt="Element Security Key Popup" data-ezsrc="https://kirelos.com/wp-content/uploads/2024/06/echo/element-security-key-popup.png66696fa855f4c.jpg" ezimgfmt="rs rscb10 src ng ngcb9" height="376" loading="lazy" referrerpolicy="no-referrer" src="data:image/svg xml,” width=”603″>

You will be given a set of security keys that you can use if you forget your security phrase. Click the Download button to save them.

Click the Continue button to proceed.

<img alt="Element Password Verify" data-ezsrc="https://kirelos.com/wp-content/uploads/2024/06/echo/element-password-verify.png66696fa8802ce.jpg" ezimgfmt="rs rscb10 src ng ngcb9" height="273" loading="lazy" referrerpolicy="no-referrer" src="data:image/svg xml,” width=”552″>

You will be asked for your account password. Enter the password and click the Continue button to finish setting up the encrypted backup.

<img alt="Element Web Client Dashboard" data-ezsrc="https://kirelos.com/wp-content/uploads/2024/06/echo/element-web-client-dashboard.png66696fa8a2625.jpg" ezimgfmt="rs rscb10 src ng ngcb9" height="628" loading="lazy" referrerpolicy="no-referrer" src="data:image/svg xml,” width=”750″>

We created a group chat room named HowtoForge using the Create a Group Chat button on the homepage. You will get the following popup when you click the button.

<img alt="Create a Private Room in Matrix" data-ezsrc="https://kirelos.com/wp-content/uploads/2024/06/echo/matrix-create-private-room.png66696fa8c6d78.jpg" ezimgfmt="rs rscb10 src ng ngcb9" height="489" loading="lazy" referrerpolicy="no-referrer" src="data:image/svg xml,” width=”499″>

You can restrict members to the domain by expanding the advanced menu and selecting the option. Finish creating the room by clicking the Create room button.

Step 12 – Install Element

Create a directory for Element.

$ sudo mkdir -p /var/www/html/element

Create a new file for grabbing the latest Element release.

$ sudo nano /var/www/html/element/update.sh

Add the following lines to it.

#!/bin/sh
set -e

install_location="https://www.howtoforge.com/var/www/html/element"
latest="$(curl -I https://github.com/element-hq/element-web/releases/latest | awk -F '/' '/^location/ {print  substr($NF, 1, length($NF)-1)}')"

cd "$install_location"

[ ! -d "archive" ] && mkdir -p "archive"
[ -d "archive/element-${latest}" ] && rm -r "archive/element-${latest}"
[ -f "archive/element-${latest}.tar.gz" ] && rm "archive/element-${latest}.tar.gz"

wget "https://github.com/element-hq/element-web/releases/download/${latest}/element-${latest}.tar.gz" -P "archive"
tar xf "archive/element-${latest}.tar.gz" -C "archive"

[ -L "${install_location}/current" ] && rm "${install_location}/current"
ln -sf "${install_location}/archive/element-${latest}" "${install_location}/current"
ln -sf "${install_location}/config.json" "${install_location}/current/config.json"

Save the file by pressing Ctrl X and entering Y when prompted.

Make the file executable.

$ sudo chmod  x /var/www/html/element/update.sh

Run the script to download Element.

$ sudo /var/www/html/element/update.sh

Step 13 – Configure Element

Copy the same element configuration file.

$ sudo cp /var/www/html/element/current/config.sample.json /var/www/html/element/config.json

Open the configuration file for editing.

$ sudo nano /var/www/html/element/config.json

Find the following lines.

"m.homeserver": {
    "base_url": "https://matrix-client.matrix.org",
    "server_name": "matrix.org"
},

Change the default Matrix homeserver address to your homeserver and remove the server_name variable.

"m.homeserver": {
    "base_url": "https://matrix.example.com",
    "server_name": "example.com"
},

If you want to use your name instead of Element in the website title, change the brand name.

"brand": "HowtoForge Example Chat",

Set the disable_guests variable as true to disallow Guests from using Element.

"disable_guests": true,

Save the file by pressing Ctrl X and entering Y when prompted.

Generate an SSL certificate for the Element client.

$ sudo certbot certonly --nginx --agree-tos --no-eff-email --staple-ocsp --preferred-challenges http -m [email protected] -d element.example.com

Create and open the file /etc/nginx/conf.d/element.conf for editing.

$ sudo nano /etc/nginx/conf.d/element.conf

Paste the following lines in it.

server {
    listen 80;
    listen [::]:80;
    server_name element.example.com;
    return 301 https://$host$request_uri;
}

server {
    listen 443 ssl;
    listen [::]:443 ssl;
    http2 on;

    server_name element.example.com;

    root /var/www/html/element/current;
    index index.html;

    access_log  /var/log/nginx/element.access.log;
    error_log   /var/log/nginx/element.error.log;

    add_header Referrer-Policy "strict-origin" always;
    add_header X-Content-Type-Options "nosniff" always;
    add_header X-Frame-Options "SAMEORIGIN" always;

    # TLS configuration
    ssl_certificate /etc/letsencrypt/live/element.example.com/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/element.example.com/privkey.pem;
    ssl_trusted_certificate /etc/letsencrypt/live/element.example.com/chain.pem;
    ssl_session_timeout 1d;
    ssl_session_cache shared:MozSSL:10m;
    ssl_session_tickets off;
    ssl_prefer_server_ciphers on;
    ssl_stapling on;
    ssl_stapling_verify on;
    ssl_dhparam /etc/ssl/certs/dhparam.pem;
    resolver 1.1.1.1 1.0.0.1 [2606:4700:4700::1111] [2606:4700:4700::1001] 8.8.8.8 8.8.4.4 [2001:4860:4860::8888] [2001:4860:4860::8844] valid=60s;
    resolver_timeout 2s;

    ssl_protocols TLSv1.2 TLSv1.3;
    ssl_ciphers ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:DHE-RSA-AES128-GCM-SHA256:DHE-RSA-AES256-GCM-SHA384;
}

Save the file by pressing Ctrl X and entering Y when prompted.

Verify the Nginx configuration file syntax.

$ sudo nginx -t
nginx: the configuration file /etc/nginx/nginx.conf syntax is ok
nginx: configuration file /etc/nginx/nginx.conf test is successful

Restart the Nginx service.

$ sudo systemctl restart nginx

We need to configure SELinux settings before accessing Element. Run the following commands to permit Element to run.

$ sudo semanage fcontext -a -t httpd_sys_rw_content_t "https://www.howtoforge.com/var/www/html/element(/.*)?"
$ sudo restorecon -Rv /var/www/html/element/

You can access the Element client via the URL https://element.example.com in your browser. Log in, and you will be taken to the app dashboard. You will be asked to verify the app with the https://app.element.io first, make sure you are logged in at the original Element app and you will be asked to match the emoji characters. Once you are verified, you will get the following dashboard.

<img alt="Self Hosted Element Dashboard" data-ezsrc="https://kirelos.com/wp-content/uploads/2024/06/echo/element-self-hosted-dashboard.png66696fa907010.jpg" ezimgfmt="rs rscb10 src ng ngcb9" height="517" loading="lazy" referrerpolicy="no-referrer" src="data:image/svg xml,” width=”750″>

Conclusion

This concludes our tutorial on installing the Matrix Synapse Chat server along with Coturn and Element web client using Nginx as a proxy server on a Debian 12 machine. If you have any questions, post them in the comments below.