Jitsi Meet on Ubuntu 19.10

Recently, I was tasked to setup a (secure) instance of the open source video conference tool Jitsi Meet. On a recent Ubuntu, this proofed to be quite easy once you are familiar with the few config files. You can find the essential steps in the official wiki, but information on the more subtle parts are spread over the entire web. This is why I want to explain them in more detail in this tutorial.

last updated 2020/04/17

Hosting your own Jitsi Meet instance seems to be a hot-topic these days. And presumably also some less experienced Linux users/admins will be confronted with a similar task. Therefore, this tutorial will cover topics like basic server hardening as well.

In the following I will assume that your target server has the IP YOUR_IP, host name YOUR_HOSTNAME and Fully Qualified Domain Name (FQDN) YOUR_FQDN (e.g. jitsi.pandascience.net).

You should read at least the official install instructions along with this tutorial.

All commands in this tutorial are expected to be run as root except where noted otherwise.

Installing Ubuntu Server 19.10

First you need to get a copy of Ubuntu Server 19.10 from the official website. Then prepare a bootable USB drive, boot into the installer and follow the steps there. After you have finished and have rebooted your server, you probably want to:

copy over your ssh key to the user you created during the Ubuntu installation

ssh-copy-id username@YOUR_IP

connect to the remote server and add your key also for the root user

ssh username@YOUR_IP
sudo su -
cp /home/username/.ssh/authorized_keys ~/.ssh/authorized_keys

disable password login in /etc/ssh/sshd_config

++ PermitRootLogin prohibit-password
++ PasswordAuthentication no
++ PubkeyAuthentication yes
++ AuthorizedKeysFile .ssh/authorized_keys

set the correct time zone for accurate logging

timedatectl list-timezones
timedatectl set-timezone "Europe/Berlin"
timedatectl status

install and configure the uncomplicated firewall (ufw)

ufw logging medium  # for testing
ufw logging off  # for production, doesn't clutter logs...
ufw default deny incoming  # already the default
ufw allow in ssh # DO NOT forget this, you can lock out yourself!
ufw allow in http # required for webserver (http -> https redirect)
ufw allow in https # required for webserver (SSL/TLS)
ufw allow in 4443/tcp # required for jitsi videobridge
ufw allow in 10000/udp # required for jitsi videobridge
ufw status verbose
# or more restrictive via CIDR netmasks
ufw allow proto tcp from 123.456.789.0/24 to any port 80
ufw allow proto tcp from 123.456.789.0/24 to any port 443
ufw allow proto tcp from 123.456.789.0/24 to any port 4443
ufw allow proto udp from 123.456.789.0/24 to any port 10000
# for deletion of rules use
ufw status numbered
ufw delete NUMBER

check open ports from “outside” with

nmap -Pn YOUR_IP

STOP: After making these changes, you should restart your server and check if you can login and everything still works!

Request Let’s Encrypt Certificates

Some ISPs refuse to open port 80 (which is IMHO more an annoyance than a security feature). In that case you need to use the new standalone TLS-ALPN mode of Let’s Encrypt, which can work with only port 443 (https) being open. The most popular clients, dehydrated and acme.sh, support this new feature. We will use the latter here. Alternatively, with port 80 (http) open, you can safely use one of the many tutorials out there for the default mode. Jitsi Meet also ships a script that will work for you.

We first prepare the environment and install acme.sh

# required for acme.sh to work
apt install socat wget curl
# get the tool (if you can, read the source first)
curl https://get.acme.sh | sh
# prepare folders
mkdir -p /etc/letsencrypt/{rsa,ecc}
# adapt group ownership and permissions
chown -R root:ssl-cert /etc/letsencrypt
chmod -R g+rX /etc/letsencrypt

Now it’s time to request the certificates. We start with the --test flag which enables the staging environment and prevents us from getting blocked because of too many failed requests (see Let’s Encrypt rate limits). Once the following commands run without any errors, you are ready for “real” certs: Run the commands again but instead of --test use --force to overwrite the test certificates.

We will request two types of certificates: The default RSA one with 2048 bit and the newer ECC type with 256 bit. These key sizes are regarded as “sufficiently secure” for now.

# requeset RSA and ECC certs
acme.sh --issue -d YOUR_FQDN --keylength 2048  --alpn --test
acme.sh --issue -d YOUR_FQDN --keylength ec-256 --alpn --test
# show them
acme.sh --list

For installing the new certificates into the folders we created in the first step, use

# for rsa files
acme.sh --install-cert -d YOUR_FQDN \
--key-file       /etc/letsencrypt/rsa/key.pem \
--fullchain-file /etc/letsencrypt/rsa/fullchain.pem \
--ca-file        /etc/letsencrypt/rsa/chain.pem \
--cert-file      /etc/letsencrypt/rsa/cert.pem \
--reloadcmd "service nginx force-reload"

# same for ecc
acme.sh --install-cert --ecc -d YOUR_FQDN \
--key-file       /etc/letsencrypt/ecc/key.pem \
--fullchain-file /etc/letsencrypt/ecc/fullchain.pem \
--ca-file        /etc/letsencrypt/ecc/chain.pem \
--cert-file      /etc/letsencrypt/ecc/cert.pem \
--reloadcmd "service nginx force-reload"

If you like, you can also disable or adapt the automatic cron certificate renewal using

crontab -e

Installing Jitsi Meet

Installing Jitsi Meet on a Ubuntu platform is super easy since the project maintainers provide distribution-specific pre-built packages. This means we only have to add their repository for apt with

echo "deb https://download.jitsi.org stable/" > /etc/apt/sources.list.d/jitsi-stable.list

We use the stable branch, because this is (at least at the time of writing) updated more often than the testing branch. You can verify the current versions yourself here:

After installing some prerequisites

apt install git apt-transport-https htop glance nginx

and setting a FQDN alias in /etc/hosts

-- localhost
++ localhost YOUR_FQDN

we can add the Jitsi repository-key

wget -qO -  https://download.jitsi.org/jitsi-key.gpg.key | sudo apt-key add -

and finally install Jitsi Meet

apt update && apt upgrade
apt install jitsi-meet
# check installed versions
dpkg -l | grep jitsi

During installation you should use YOUR_FQDN as hostname and set the correct locations for the RSA key and certificate (we use the RSA key instead of ECC for better compatibility)

# key
# certificate chain, i.e. your requested cert + root cert

That’s it! Easy, right?

The official install guide recommends to further increase system resources. In Ubuntu 19.10 they already match the requirements as you can test with

cat /proc/$(cat /var/run/jitsi-videobridge/jitsi-videobridge.pid)/limits
# --> DefaultLimitNOFILE=65000
# --> DefaultLimitNPROC=65000
# --> DefaultTasksMax=65000

Secure Domain

After the basis installation, everyone who knows your FQDN can create a room in your Jitsi Meet instance. For security and legal reasons, that’s probably something you don’t want. Fortunately, Jitsi Meet offers (at least*) three options to secure your domain with password-authentication:

  1. Plain prosody (an XMPP client which is used behind the scenes anyway)
  2. JSON Web Tokens (JWT)
  3. LDAP authentication

*NOTE: Prosody owns an entire zoo of authentication modules, but I have not tested them and I’m not sure if they are supported by Jitsi Meet.

You can setup Jitsi Meet in a way only persons with accounts may create rooms. Guests, on the other hand, can only enter already created rooms or have to wait for the moderator. In turn, they do not need an account for participating.

Before deciding for an authentication option, you should think about the consequences for you as admin in charge and the workload you are going to put on yourself. My preferred option would always be #3, because this allows you to utilize an existing and (hopefully) secure method. Besides the Jitsi Meet configuration, it generates zero work for the admin. Since I have no official access to our university’s LDAP server this was no option for me.

Option #2 is certainly a nice toy, but you would have to create tokens for every lecturer and every course, because room names are predefined by the token. You further had to trust your users to not accidentally give away the room link + token. Yes, you can also issue wildcard tokens and give them an expiration date but IMHO this is even worse from a security perspective. Thinking about the pros and cons, #2 is a good option if you have enough time to setup some automated way of creating time-limited tokens on request. So also no workable option for me.

For reference (the Jitsi wiki entry is not too verbose on that), you do not have to use https://jwt.io/ and enter your secret there! Instead use any of the existing libraries listed on this very page in order to issue tokens in a safe way locally. Nowadays, I call on python for most stuff, so I went for the authlib module. The maintainers provide an extra wiki page for JTW. For Jitsi-specific tokens, you can use the following python code

#!/usr/bin/env python3
from authlib.jose import jwt
header = {
    "typ": "JWT",
    "alg": "HS256",
payload = {
    "context": {
        "user": {
            "avatar": "http://www.gravatar.com/avatar",
            "name": "YOUR_ROOM_NAME",
            "email": "USER_EMAIL",
        "aud": "YOUR_JITSI_INSTANCE",
        "iss": "YOUR_NAME_OR_ID",
        "sub": "YOUR_FQDN",
        "room": "sessions",
secret = "cryp706r4ph1c_c0d3"
s = jwt.encode(header, payload, secret)
print("token: ", s)

Thus, only option #1 left for me. Here, you directly access the prosody user database and create (or delete!) users on the fly which gives me as admin the most direct control. If a user tells me that the credentials have been lost or accidentally given away, I simply re-create the user. With JWT this is not possible. The downside is that you need to ssh-connect to the server for user handling. However, if you have to create accounts for only a handful of people, say all lecturers of your institute, then it is not too much pain.

For setting up option #1 (plain prosody) you can safely refer to this wiki page.

The relevant commands for user handling are (man prosodyctl)

prosodyctl adduser user@YOUR_FQDN
prosodyctl deluser user@YOUR_FQDN
prosodyctl passwd user@YOUR_FQDN
# only for compatibility with ejabberd but similar to adduser
prosodyctl register user jitsi.pandascience.net [password]

Users should be able to change their passwords after you created an account for them. Since prosody is an XMPP server, you can of course directly connect to it after opening the XMPP port with

ufw allow in 5222/tcp

However, only few clients implement the XEP-0077 protocol that let’s users make changes to passwords via the client. Pidgin is one of them. My preferred way would be to include converse.js via nginx and call it with something like https://YOUR_FQDN/reset-password. Unfortunately, converse.js does not support XEP-0077 yet. At least there is already a GitHub issue asking for this feature.

In case you get untrusted certificate errors from your client when connecting, make sure that prosody uses the correct certificate and key in /etc/prosody/conf.avail/YOUR_FQDN.cfg.lua

ssl = {
    key         = "/etc/letsencrypt/rsa/key.pem";
    certificate = "/etc/letsencrypt/rsa/fullchain.pem";

instead of the self-signed one created during installation. Also check that you have set the correct permissions for these files as shown above.

Data Security / Privacy Policy

The best data security policy is to not gather any personalized data in the first place.

For that reason, we will set the following options:

  • In /etc/logrotate.conf set daily rotation and disable archives (rotate 0)
  • In /etc/jitsi/videobridge/logging.properties set .level=WARNING instead of .level=INFO
  • In /etc/nginx/nginx.conf disable logging entirely with access_log off; and error_log /dev/null; (Note: concrete lines depend on your nginx version but these should work for all)

As suggested by Mike Kuketz, you probably want to change the default google STUN servers. Since my server is hosted in Germany, I changed them in /etc/jitsi/meet/YOUR_FQDN-config.js to

stunServers: [
    { urls: 'stun:stun.schlund.de:3478' },
    { urls: 'stun:stun.t-online.de:3478' },
    { urls: 'stun:stun.1und1.de:3478' },
    { urls: 'stun:stun.einsundeins.de:3478' },
    { urls: 'stun:stun.gmx.de:3478' },

Don’t forget to restart services after changing any of the Jitsi Meet configuration files!

systemctl restart prosody.service jicofo.service jitsi-videobridge2.service nginx.service

Basic Server Hardening

We already configured SSH to only allow login with public keys and set up ufw as interface to the Linux built-in firewall iptables. For increasing security further, you can install and configure fail2ban. This will automatically block IP addresses that repeatedly failed to authenticate within a certain time interval.

Since we are using ufw already, we want fail2ban to also use it instead of calling iptables directly. Therefore, after installing fail2ban with

apt install fail2ban

create a new configuration file /etc/fail2ban/jail.local (where only rules deviating from defaults need to be placed) and enter

banaction = ufw

enabled = true
maxretry = 3
bantime = 86400

enabled = true
port = 5222
filter = prosody-auth
logpath = /var/log/prosody/prosody*.log
maxretry = 6
bantime = 300

Here we already added the proper configuration for prosody.

In order to have fail2ban recognize failed prosody login attempts, you need to install the mod_log_auth module and follow the steps there (we already did half of it). Installing prosody modules is easy, you just have to place the files in the correct path. You can do it like that for exmaple:

hg clone https://hg.prosody.im/prosody-modules/ /usr/lib/prosody/modules_available
ln -s /usr/lib/prosody/modules{_available,}/mod_log_auth

Don’t forget to add the new module in /etc/prosody/conf.avail/YOUR_FQDN.cfg.lua

modules_enabled = {
    "log_auth";    -- put the module here

Keep in mind that fail2ban (or ufw respectively) only bans IP addresses but does not drop already established connections. Because of the way Jitsi Meet includes prosody in its web interface we need to tweak the ufw ban action. First install the conntrack tool

apt install conntrack

then add a line to /etc/fail2ban/action.d/ufw.conf

actionban = [ -n "<application>" ] && app="app <application>"
            ufw insert <insertpos> <blocktype> from <ip> to <destination> $app
            conntrack --flush  # <-- add this

After an IP has been banned, this will disconnect all sessions and the potential attacker gets kicked out of the Jitsi Meet web interface. This should not effect other users in active sessions – at least not notably.

Restart the relevant services for changes to take effect

systemctl restart fail2ban.service prosody.service jicofo.service jitsi-videobridge2.service 

Simple Monitoring

As usual, you can use (h)top and glances to monitor your system’s resources

apt install htop glances

You can also list all prosody user accounts with an additional module

ln -s /usr/lib/prosody/modules{_available,}/mod_listusers
prosodyctl mod_listusers
# immediately after installation, this should result in something like
#>> jvb@auth.YOUR_FQDN
#>> focus@auth.YOUR_FQDN

If you like to check active Jitsi Meet sessions (i.e. open rooms, number of participants etc.), you can use this statistics module. Paste its code into a new file in the module install directory like shown above. Alternatively, you can also put it into /usr/share/jitsi-meet/prosody-plugins/mod_muc_status.lua. Obviously, I chose the name mod_muc_status.lua, so the module entry in the prosody config (see below) must refer to muc_status.

This module requires some additional tools. I took the steps from this forum post, adapted it to my needs and ended up with the following for Ubuntu 19.10:

# check that prosody is in group ssl-cert
su - prosody -s /bin/bash -c groups

# luacrypt requires libssl1.0-dev but Ubuntu 19.10 only
# has libssl1.1 in its repository, so add the bionic sources
echo "deb http://security.ubuntu.com/ubuntu bionic-security main" > /etc/apt/sources.list.d/libssl1.0-bionic.list
apt update && apt-cache policy libssl1.0-dev

# install lua modules
apt install luarocks
apt install liblua5.2-dev
luarocks install basexx
luarocks install luacrypt
luarocks install luajwtjitsi
luarocks remove lua-cjson
luarocks install lua-cjson 2.1.0-1

You don’t need any other step from the forum post because we don’t want to access session data via web tokens. Instead, we configure prosody to only allow local connections by adding the following in /etc/prosody/conf.avail/localhost.cfg.lua

VirtualHost "localhost"
    authentication = "anonymous"
    modules_enabled = {
        "muc_status"; -- must named after your module file

That way, the information can only be examined by the admin and is thus not a potential security leak for so called “room bombing”.

For convenience, you can prepare the little script jitsi_status.sh containing

curl -s  http://localhost:5280/status?domain=YOUR_FQDN | python -m json.tool

which you can call from your remote computer with

ssh root@YOUR_IP "~/jitsi_status.sh"

For making the module’s output better readable, I piped it to python’s built-in JSON module.

There is also a netdata plugin, but I haven’t tested it yet.

Customizing the Web Interface

Since we used the quick-install mode under Ubuntu/Debian, the easiest way to change the welcome page is to edit the master CSS-file. Unfortunately, the default one is minified, so we need to prettify it first to make it human-readable. You can follow this blog post on how to install and use the cssbeautify tool. Alternatively, you can use any of the online prettifiers.

Changes have to be made in the following files, depending on what you want to achieve:

  • Overwrite title (and optionally also subtitle) for all languages in /usr/share/jitsi-meet/css/all.css (prettify first!). This post may help CSS-newbies.
  • Language-dependent texts: search for “appDescription:” in /usr/share/jitsi-meet/libs/app.bundle.min.js (EN/default) or /usr/share/jitsi-meet/lang/main-de.json (DE)

After changing the CSS file, don’t forget to clear your local cache and reload the page for changes to take effect on your browser.

You can also change the watermark by replacing the file /usr/share/jitsi-meet/images/watermark.png with your own logo.

In order to create a footer with e.g. links to your privacy policy, you can adapt /usr/share/jitsi-meet/static/welcomePageAdditionalContent.html to your needs. If links are not clickable, make sure to set a proper size for the watermark in all.css.

Besides the look, you can also change some default interface settings like the app name by adapting the code in /usr/share/jitsi-meet/interface_config.js and in /etc/jitsi/meet/YOUR_FQDN-config.js. For example, I require all participants to set their name and start their session with disabled video to save resources

requireDisplayName: true,
startAudioOnly: true,

Note: After updating the Jitsi Meet packages on Ubuntu via apt, most of the above mentioned files will be overwritten! You should back them up and replace them after the update accordingly.

Scaling / Adding Videobridges

If you have a large number of users spawning rooms with more than 10 participants each, you might want to add additional video bridges for load balancing. There is basically an old and a new way to do this. I have not looked too far into this topic, but can confirm that the new way using MUC works when you follow this unofficial official wiki entry. Make sure you open all required ports!

Perhaps you might also be interested in the octo feature.


After updating, all customizations are gone and the Jitsi Meet web interface looks again like the original.
➙ You should have backed up the files like explained above. On update they will be overwritten. Sorry, but you have to start all over again.

Participants can connect, but there is no video or audio output. Instead, they see only a black screen.
➙ You must open all required ports, especially port 10000/udp.

When connecting via XMPP/Pidgin to change an account’s password, I get a untrusted certificate warning.
➙ You must replace the self-signed ones with the correct Let’s encrypt files as explained here.


The basic installation of Jitsi Meet on Ubuntu 19.10 is super easy. You have a lot of customization options and by adding additional video bridges it can also be scaled to some extend. However, for running a save instance you definitely need to put some more effort into the project.

You must at least secure your server with basic hardening concepts such as a firewall and a more restrictive SSH configuration. The most effective way to protect your user’s privacy is to not gather any personalized data like IP addresses in the first place.

In order to prevent random people to use your service, block resources and possibly conduct illegal activities, you should decide for one of the authentication options that Jitsi Meet offers.

Finally, monitoring is not the best, but with working SSH and a simple shell script it’s quite painless. There’s even a netdata plugin, but I haven’t tested that one yet.


  1. Frank Brokken June 1, 2020 at 12:08

    Nice tutorial! Thanks for making this available 🙂
    One small comment: in the Troubleshooting section you write:

    Participants can connect, but there is no video or audio output. Instead, they see only a black screen.
    ➙ You must open all required ports, especially port 1000/udp.

    There’s probably a little typo there: shouldn’t it be port 10000/udp (so ten thousand, instead of one thousand)?


    1. René Wirnata June 1, 2020 at 12:43

      Yep, it must be 10k/udp of course. Thanks for reporting! On this occasion, I also replaced flyinghuman’s “unofficial” wiki entry in the videobridge section by its merged copy from the official docs.

Leave a comment

Your email address will not be published.

By continuing to use the site, you agree to the use of cookies. more information

The cookie settings on this website are set to "allow cookies" to give you the best browsing experience possible. If you continue to use this website without changing your cookie settings or you click "Accept" below then you are consenting to this. We only use session cookies!