Introduction
Matrix is an open standard for decentralized and end-to-end encrypted communication. It federates using homeservers that communicate with each other over the internet. Meaning it using
Matrix is built on a decentralized network of servers and clients, which allows users to communicate securely without relying on a central authority.
Why use matrix ?
- Decentralization
- Open Source
- End-to-end encryption
- Interoperability (meaning you can use bridges to connect to other apps like whatsupp…)
Why this guide ?
In this guide
- I’ve combined tutorials on how to deploy matrix and element the easiest, but still reliable way
- Researched the right settings and the best practices for the performance
- Added own separate TURN server for proper voice and video calls
- Used token based User registration, provisioned by the chat-bot for easy management
Prerequisites
- debian 12 instance (VPS or selfhosted)
- public IPv4 (or cloudflare tunnel but I won’t cover that here)
- 3 DNS subdomains pointing to your public IP (matrix.example.com, turn.example.com, element.example.com)
- Open ports (I’ll explain which ones and why down bellow)
Installation
Add Matrix apt repo
1
2
| sudo wget -O /usr/share/keyrings/matrix-org-archive-keyring.gpg https://packages.matrix.org/debian/matrix-org-archive-keyring.gpg
echo "deb [signed-by=/usr/share/keyrings/matrix-org-archive-keyring.gpg] https://packages.matrix.org/debian/ $(lsb_release -cs) main" | sudo tee /etc/apt/sources.list.d/matrix-org.list
|
Install Matrix Synapse
1
2
| sudo apt update
sudo apt install matrix-synapse-py3
|
You will be promted to fill your matrix homeserver name. In this example we are going to use “matrix.example.com”
Setup PostgreSQL
By default, Matrix synapse uses SQlite, but it’s recommended to use PostgreSQL for better performance.
Install
1
| sudo apt install postgresql
|
Switch to postgres user
Create user and db
1
2
3
| createuser --pwprompt synapse
createdb --encoding=UTF8 --locale=C --template=template0 --owner=synapse synapse
exit
|
Open ports
If you are using cloud instance, you will most likely need to allow these port in the firewall.
Or maybe you are just using linux firewall, so just allow those ports with ufw.
We will need
- 80,443 for http/s
- 8448 for matrix federation communication with other servers
- 3478, 5349, 49152:65535 udp/tcp for TURN (voip of matrix)
1
2
3
4
5
6
7
8
| sudo ufw allow 80
sudo ufw allow 443
sudo ufw allow 8448
sudo ufw allow 3478
sudo ufw allow 5349
sudo ufw allow 49152:65535/udp
sudo ufw allow 49152:65535/tcp
|
Setup Nginx
By default matrix doesn’t come with any reverse proxy so we need to install one by ourselves. Because of how easy is to use nginx and certbot together I’m going with them.
Install
1
| apt install nginx certbot python3-certbot-nginx
|
Certbot TLS for matrix domain
1
| sudo certbot certonly --nginx -d matrix.example.com -d example.com
|
Create new nginx site
1
| vim /etc/nginx/sites-available/synapse
|
By default matrix synapse API in nginx template is available only on localhost, but I’ve changed it so we can play with the API later.
Source 1
Source 2
Synapse documentation on reverse proxy
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
| server {
server_name matrix.example.com;
# Client port
listen 80;
listen [::]:80;
return 301 https://$host$request_uri;
}
server {
server_name matrix.example.com;
# Client port
listen 443 ssl http2;
listen [::]:443 ssl http2;
# Federation port
listen 8448 ssl http2 default_server;
listen [::]:8448 ssl http2 default_server;
# TLS configuration
ssl_certificate /etc/letsencrypt/live/matrix.example.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/matrix.example.com/privkey.pem;
ssl_protocols TLSv1.2 TLSv1.3;
ssl_prefer_server_ciphers on;
location / {
proxy_pass http://localhost:8008;
proxy_set_header X-Forwarded-For $remote_addr;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header Host $host;
client_max_body_size 50M;
}
}
|
Change all 4 occurences of matrix.example.com to your domain.
Enable and reload new site
1
2
| sudo ln -s /etc/nginx/sites-available/synapse /etc/nginx/sites-enabled
sudo systemctl reload nginx.service
|
We are going to add all new changes to “/etc/matrix-synapse/conf.d/” directory, but you can also add them directly to “/etc/matrix-synapse/homeserver.yaml”.
But this is just cleaner way to do it and you won’t get prompted every time you update the system, if you really want to over-ride the config.
Create new DB
1
| vim /etc/matrix-synapse/conf.d/database.yaml
|
1
2
3
4
5
6
7
8
9
| database:
name: psycopg2
args:
user: synapse
password: 'your_password'
database: synapse
host: localhost
cp_min: 5
cp_max: 10
|
Change ‘your_password’ for your actuall password you have used.
Create registration key
With this key you (or anyone if API is open) can register new accounts.
1
| echo "registration_shared_secret: '$(cat /dev/urandom | tr -cd '[:alnum:]' | fold -w 256 | head -n 1)'" | sudo tee /etc/matrix-synapse/conf.d/registration_shared_secret.yaml
|
Create Synapse admin account
We will need this account later for API uses as well
1
| register_new_matrix_user -c /etc/matrix-synapse/conf.d/registration_shared_secret.yaml http://localhost:8008
|
I’ll call mine “test”.
Get access token of this user
We will need token for later use
1
2
3
4
5
| curl -X POST --header 'Content-Type: application/json' -d '{
"identifier": { "type": "m.id.user", "user": "test" },
"password": "your_password",
"type": "m.login.password"
}' 'https:/matrix.example.com/_matrix/client/r0/login'
|
Change user, password, and domain to yours.
You will get response looking like this
1
| {"user_id":"@test:matrix.example.com","access_token":"syt_cmVnaXN0css9uLWJvdA_rIWGddmIVnQoaYgZUqcq_15eW1E","home_server":"matrix.example.com","device_id":"MNSSSEWQAZ"}
|
Save the token access_token.
Disable presence (optional)
For better performance, you can disable presence (the green dot showing presence). By default this is enabled, but takes up a lot of resources and I just think it’s not that necessary.
1
| vim /etc/matrix-synapse/conf.d/presence.yaml
|
1
2
| presence:
enabled: false
|
Enable token requirement for registration new account
If you don’t want to manually create every account for your friend, but you also don’t want to let server open for anyone to just register and possibly DDOS you, well I have solution for you.
The solution is to make registration token based, and only people who will know this token are going to be able to register. Later in this guide I’ll also show you how to make it easier with chat bot.
1
| sudo vim /etc/matrix-synapse/conf.d/registration.yaml
|
1
2
| enable_registration: true
registration_requires_token: true
|
Restart Matrix Synapse server
1
| sudo systemctl restart matrix-synapse.service
|
Verify the matrix site is running
- Simply type your matrix domain to the browser
- use federation tester tool to see if your server can communicate with other servers
Setup VOIP
By default VOIP on matrix is not a great experience. Often it’s very slow and it’s dropping connection.
The main and simplified reason is that NAT, firewall, and compliance network policies, the process of reaching the other peer becomes complex.
Default config on your synapse would use the matrix.org turn server. Turn is only used for opening the call if a user is behind a NAT or Firewall. The call itself is done via “web-RTC” which is always encrypted.
Why use coturn as a TURN server?
InternetSource
Synapse documentation
Install coturn
1
| sudo apt install coturn -y
|
Create TLS certificate for TURN coturn server
1
| certbot certonly --nginx -d turn.example.com
|
Generate an authentication secret
1
| echo "static-auth-secret=$(cat /dev/urandom | tr -cd '[:alnum:]' | fold -w 256 | head -n 1)" | sudo tee /etc/turnserver.conf
|
Edit the config
1
| vim /etc/turnserver.conf
|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
| use-auth-secret
realm=turn.example.com
cert=/etc/letsencrypt/live/turn.example.com/fullchain.pem
pkey=/etc/letsencrypt/live/turn.example.com/privkey.pem
# VoIP is UDP, no need for TCP
no-tcp-relay
# Do not allow traffic to private IP ranges
no-multicast-peers
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
# Limit number of sessions per user
user-quota=12
# Limit total number of sessions
total-quota=1200
|
Change all 3 occurences of turn.example.com to your domain.
Restart coturn service
1
| sudo systemctl restart coturn.service
|
Implement coturn into matrix Synapse config
1
| sudo vim /etc/matrix-synapse/conf.d/turn.yaml
|
1
2
3
4
| turn_uris: [ "turn:turn.example.com?transport=udp", "turn:turn.example.com?transport=tcp" ]
turn_shared_secret: 'static-auth-secret'
turn_user_lifetime: 86400000
turn_allow_guests: True
|
Change all 2 occurences of turn.example.com to your domain.
Restart synapse to apply the new TURN server
1
| sudo systemctl restart matrix-synapse.service
|
Check if new TURN server is working
“test.voip.librepush.net”
You can use this test voip open-source online tool. Just type in your
- Homeserver URL: https://turn.example.com
- user ID & password: admin account we have created with register_new_matrix_user
Test.voip.librepush.net is great, but not that reliable, so if you get some errors, it wont necesarrly means that your TURN server doesnt work.
Webrtc
More complex and useful site to check TURN server is webrtc.github.io
To get the right credentials you will need to use this command in the terminal of matrix.
1
2
| curl -X POST --header "Authorization: Bearer syt_YOUR_TOKEN_HERE" -d {} -X GET http://127.0.0.1:8008/_matrix/client/r0/voip/turnServer
|
Change syt_YOUR_TOKEN_HERE for YOUR TOKEN that we got earlier in this guide, while creating synapse matrix admin account.
And you will receive
- TURN username
- TURN password
- TURN URI
1
| {"username":"1700394074:@test:matrix.example.com","password":"YOUR_pasword,"ttl":86400,"uris":["turn:turn.example.com?transport=udp","turn:turn.example.com?transport=tcp"]}root@matrix:~#
|
And finally feed these into WEB UI.
If the TURN server is working correctly, you should see at least one relay entry in the results.
Element (web interface)
Setup
We have successfully created a backend for our chat application and we can already connect to it with some public clients like Elemenet
But because we want everything private and selfhosted, we are going to install element on our server as well.
Install “jq”
Create working dir
1
| sudo mkdir -p /var/www/element
|
Crete a script for regular Element updates
1
| vim /var/www/element/update.sh
|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
| #!/bin/sh
set -e
install_location="/var/www/element"
latest="$(curl -s https://api.github.com/repositories/39487546/releases/latest | jq -r .tag_name)"
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/vector-im/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"
|
Make file executable
1
| sudo chmod +x /var/www/element/update.sh
|
Start the script
To download element for the first time just run the script.
1
| sudo /var/www/element/update.sh
|
To update Element in the future, re-run the command, or setup cron like so
Add the script to crontab
1
| 0 0 * * 0 /bin/bash /var/www/element/update.sh
|
Copy the config template
1
| sudo cp /var/www/element/current/config.sample.json /var/www/element/config.json
|
Change it to your server name
1
| sudo vim /var/www/element/config.json
|
1
2
3
4
| "m.homeserver": {
"base_url": "https://matrix.example.com",
"server_name": "example.com"
},
|
Change both matrix.example.com and example.com to your domain.
Create new element nginx site
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
| server {
listen 80;
listen [::]:80;
server_name element.example.com;
return 301 https://$host$request_uri;
}
server {
listen 443 ssl http2;
listen [::]:443 ssl http2;
server_name element.example.com;
root /var/www/element/current;
index index.html;
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_protocols TLSv1.2 TLSv1.3;
ssl_prefer_server_ciphers on;
}
|
Change all 4 occurences of turn.example.com to your domain.
Create TLS certificate for element site
1
| sudo certbot certonly --nginx -d element.example.com
|
Enable the new element site
1
| sudo ln -s /etc/nginx/sites-available/element /etc/nginx/sites-enabled
|
1
2
| nginx -t
sudo systemctl reload nginx.service
|
Congratulation, you can now access your element web interface!
Automatic certification renewal
Because we don’t want to one day find out that our app is not working because we forgot to update “FreeTlS” certificate, we are going to set up automatic renewal with crontab.
Type in the terminal
1
| 0 12 * * * /usr/bin/certbot renew --quiet
|
Token user registration
As I’ve mentioned before, in this post we are going to setup matrix & element in token user registration.
Meaning. Registration is allowed, as long as user has registration token. Which can be 1-time use, 3-time user, never expire etc..
Usually, these tokens are managed with synapse API . For example to create new token we type.
1
| curl -X POST --header "Authorization: Bearer syt_YOUR TOKEN" -d {} -X POST http://127.0.0.1:8008/_synapse/admin/v1/registration_tokens/new
|
This is very inconvenient for regular non-tech user, or just for the user who doesn’t want to SSH into his machine every single time.
Matrix-registration-bot
I’ve chosen this github project from a moan0s so all credit goes to him.
Create admin account for a bot
Or you can just use account admin, but I prefer when all accounts have their own purpose.
1
2
| register_new_matrix_user -c /etc/matrix-synapse/conf.d/registration_shared_secret.yaml http://localhost:8008
|
1
2
3
4
5
6
| New user localpart [root]: registration-bot
Password:
Confirm password:
Make admin [no]: yes
Sending registration request...
Success!
|
Get an access token from a bot account
1
2
3
4
5
6
| curl -X POST --header 'Content-Type: application/json' -d '{
"identifier": { "type": "m.id.user", "user": "registration-bot" },
"password": "YOURpassword",
"type": "m.login.password"
}' 'https://matrix.example.com/_matrix/client/r0/login'
{"user_id":"@registration-bot:matrix.example.com","access_token":"syt_YOURTOKEN","home_server":"matrix.example.com","device_id":"RVVCPTXXYH"}
|
Install python and pip
1
| sudo apt-get install python3 pip3 -y
|
Install bot
1
| pip3 install matrix-registration-bot --break-system-packages
|
Create working dir
1
| mkdir -p /matrix/matrix-registration-bot
|
Create config file
1
| vim /matrix/matrix-registration-bot/config.yml
|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
| bot:
server: "https://matrix.example.com"
username: "registration-bot"
access_token: "yourTOKEN"
# It is also possible to use a password based login by commenting out the access token line and adjusting the line below
# password: "secretpassword"
prefix: ""
api:
# API endpoint of the registration tokens
base_url: 'matrix.example.com'
# Access token of an administrator on the server. If you configured the bot to be an admin on the sever you can use the same token as above.
token: "yourTOKEN"
logging:
level: ERROR
|
Change botj occurences of matrix.example.com to your domain and username/password to credentials of your bot (which we got earlier)
Create a service
Create a service that will automatically run and restart the bot.
1
| sudo vim /etc/systemd/system/matrix-registration-bot.service
|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
| [Unit]
Description=matrix-registration-bot
[Service]
Type=simple
WorkingDirectory=/matrix/matrix-registration-bot
ExecStart=python3 -m matrix_registration_bot.bot
Restart=always
RestartSec=30
SyslogIdentifier=matrix-registration-bot
[Install]
WantedBy=multi-user.target
|
Enable and start the service
1
2
3
| sudo systemctl daemon-reload
sudo systemctl start matrix-registration-bot
sudo systemctl enable matrix-registration-bot
|
Test the bot
Now go to your https://element.example.com and login with admin credentials. (or just create a new user)
- Click on “Send a new message”
- Fill out the bot name: “@registration-bot:matrix.example.com” and click GO.
- The room should open and you can type in the command. Try typing
If everything is working correctly the bot should answer like this.
If you receive the error message
1
| Failed to decrypt your message. Make sure encryption is enabled in my config and either enable sending messages to unverified devices or verify me if possible.
|
You need to verify the user. To do that:
- click on in profile picture
- Click on session under verify
- click on Manually verify by text
- click on verify session
- Now leave the room with the bot
- Finally, just start the chat with him again and it should work.
Create token
To create a new one time use token just type in
And give this token to the person you want to able to register to your matrix synapse server.
Congratulation
You’ve successfully deployed matrix synapse server with an element interface with VOIP over your own TURN server and chatbot token-managed registration.
Useful
ADMIN API commands
All can be done both
- locally http://127.0.0.1:8008/_synapse/admin…
- remotely https://matrix.example.com/_synapse/admin…
Create token
1
| curl -X POST --header "Authorization: Bearer syt_YOURTOKEN" -d {} -X POST http://127.0.0.1:8008/_synapse/admin/v1/registration_tokens/new
|
List all Users
1
| curl -X POST --header "Authorization: Bearer syt_YOURTOKEN" -d {} -X GET http://127.0.0.1:8008/_synapse/admin/v2/users?from=0&limit=10&guests=false
|
Delete specific user
1
| curl -X POST --header "Authorization: Bearer syt_YOURTOKEN" -d {} -X POST http://127.0.0.1:8008/_synapse/admin/v1/deactivate/@test:matrix.example.com
|
All synapse ADMIN API commands
Can be found on synapse documentation website