Matrix Synapse and mautrix-whatsapp in a VPN

From Penguin Development
Jump to navigationJump to search

WhatsApp is a popular communication app for Android and iOS. It is owned by Facebook, which has a hostile attitude towards alternative clients and open source software, along with being notorious for its egregious privacy violations (and de-facto support of white supremacy and fascism). It stands to reason, then, that WhatsApp is not an app that any privacy-conscious individual should want to have installed on their mobile phone, which typically holds a vast quantity of personal data. However, the unfortunate truth is that it tends to be virtually impossible for many of us to stop using WhatsApp without permanently losing contact with several friends, family members, clients or colleagues. Thus, one may seek to find a middle ground: keeping WhatsApp well away from one's personal devices, but somehow still connecting to its network using these devices. Given Facebook's hostile attitude to third-party clients, one might expect such a task to be impossible... or is it?

Enter the Matrix. Matrix is a free (as in speech), feature-rich decentralised communication ecosystem that can serve as a platform for instant messaging, e.g. using Element as a client. Although I would highly encourage switching from WhatsApp to Element whenever possible, it turns out Matrix is in fact capable of communicating with the WhatsApp network. This is done using mautrix-whatsapp, which masquerades as a WhatsApp Web client and uses it to communicate with the actual WhatsApp client running on a phone or emulator.

A note: this guide is essentially an amalgamation of the most relevant bits of various other guides: the Debian OpenVPN guide, the OpenVPN community installation guide, the Synapse installation guide by natrius, the Nginx/TLS reverse proxy guide by Marco Paganini, the Nginx/easy-rsa guide by hoxnox, and the official mautrix-whatsapp bridge setup guide by Tulir Asokan. These individual guides explain their steps in greater depth than I do here, and I highly, highly recommend reading through all of them before tackling a project like this one. Also be very aware that I am not an expert in any of this. If you need enterprise-grade security, close this browser tab now and consult an actual expert. That said, I have made an honest attempt to hammer down any security holes I could think of, and I am presently running this set-up myself.

The set-up


This guide will explain one possible configuration to use WhatsApp from a Matrix client. In this configuration, a Matrix homeserver (Synapse) and mautrix-whatsapp will be installed on a (virtual) private server, along with OpenVPN. The setup is completely contained within the virtual private network (VPN) created by OpenVPN, and does not require any entrypoints from the outside world except through the VPN (although you may want to enable SSH access for setup and administration). It therefore has a high degree of inherent security. A reverse HTTPS proxy is set up using Nginx with certificates generated by easy-rsa: this is necessary for some clients to be able to connect.

iptables is used for setting up routing and firewalling of the server, and only IPv4 is considered for the time being. I wish to eventually migrate to nftables and a dual IPv4/IPv6 stack, however this will take more time and effort than I have available, especially considering the current setup "just works" for me.

This guide is primarily focussed on a "single owner, multiple devices" configuration: extra security precautions should be taken if one wants to allow multiple individuals in the VPN, and using multiple WhatsApp accounts in particular is beyond the scope of this guide.

N.B. it is up to you to decide where WhatsApp (the actual proprietary app) goes in the graphic above: as shown, it is put on the VPS (in an emulator or VM), but it is also possible to leave it off the VPS, e.g. running on a normal phone. Off the VPS, you additionally have the choice of whether you want to route its traffic through the VPN (recommended) or not.


  • A WhatsApp account, and the WhatsApp app running either on a phone or in an emulator on a device with a webcam
  • A (virtual) private server ((V)PS) with the following specs:
    • A static IPv4 address
    • Linux, ideally Debian 10
    • root/sudo access
    • At least 1 GB RAM
    • At least 5 GB disk space for the software and text message storage
    • More disk space for the media (videos, etc.) you send and receive
  • Decent knowledge of Linux administration
  • A free weekend or so to set up, test and tweak everything

The environment used in this guide is a VPS running Debian 10 with full root access; all commands are run as root unless otherwise stated. It is assumed that the VPS is in a fresh-off-the-shelf state with little more than sshd installed and running.

Step 0: iptables and OpenVPN

The first and foremost things to get working are the firewall and VPN server. Henceforth, we shall use as the OpenVPN address space, and port 1194 (UDP or TCP) as the OpenVPN port. Take extra care if you want to use something else. We shall use as the externally reachable IP of the server; always replace this by whatever the actual address is.

Log in to your VPS and install iptables as follows:

apt install iptables iptables-persistent

Let us begin by setting up some basic security:

1 iptables -A INPUT -i lo -j ACCEPT
2 iptables -A INPUT -s -j ACCEPT
3 iptables -A INPUT -m state --state RELATED,ESTABLISHED -j ACCEPT
4 iptables -A INPUT -p icmp -m icmp --icmp-type 8 -j ACCEPT
5 iptables -A INPUT -p icmp -m icmp --icmp-type 11 -j ACCEPT
6 iptables -A INPUT -p icmp -m icmp --icmp-type 3 -j ACCEPT
7 iptables -A INPUT -p tcp -m tcp --dport 22 -j ACCEPT
8 iptables -A INPUT -j DROP
9 iptables-save >/etc/iptables/rules.v4

This will drop all incoming connections apart from SSH and a few ICMP messages. The last line preserves your iptables rules when rebooting.

The next step is to install OpenVPN. The instructions provided here mostly follow Debian's OpenVPN guide; see that page for more in-depth information. Start by installing the required packages:

apt install easy-rsa openvpn

Next, we create a certificate authority directory in /etc/openvpn and edit the config:

1 make-cadir /etc/openvpn/easy-rsa
2 cd /etc/openvpn/easy-rsa
3 editor vars

The vars file has sensible defaults, but you may want to make a few changes (see the inline documentation if you are unsure):

  1. uncomment the line with set_var EASYRSA_DN "cn_only"
  2. set the key size to 4096: set_var EASYRSA_KEY_SIZE 4096
  3. set the CA validity to something long, like 10 years: set_var EASYRSA_CA_EXPIRE 3650
  4. set the certificate validity to something similar: set_var EASYRSA_CERT_EXPIRE 3650

After writing the vars file, create the certificate authority, a server certificate and Diffie-Hellman parameters:

1 ./easyrsa init-pki
2 ./easyrsa build-ca
3 ./easyrsa build-server-full server nopass
4 ./easyrsa gen-dh

The build-ca step will prompt you for a name and password. The name can be whatever you like, e.g. "My OpenVPN CA". Choose something strong but memorable as the password. Every further interaction with the certificate authority will require this password.

Now create a certificate for every client (desktop, laptop, tablet, mobile phone, refrigerator...) you want to have access to the VPN/your WhatsApp account. It's a good idea to use a file name that allows you to tell these certificates apart, e.g. hostname.n, where hostname is the hostname of the device and n is a number indicating this is the n'th certificate issued for that hostname:

1 ./easyrsa build-client-full alpha.0 nopass
2 ./easyrsa build-client-full bravo.0 nopass
3 # And so on...

Finally, per the OpenVPN community installation guide, generate a shared secret TLS key:

1 cd /etc/openvpn/easy-rsa/pki/private
2 openvpn --genkey --secret ta.key

Now edit the OpenVPN configuration file:

1 cd /etc/openvpn
2 editor server.conf

A viable example configuration file follows. See openvpn(8) if it is unclear what an option does. Be sure to save this file as /etc/openvpn/server.conf.

 1 mode server
 2 # Enable TLS encryption.
 3 tls-server
 4 # Listen on port 1194 (the default).
 5 port 1194
 6 # Set the protocol here. UDP is the default, but it may give you trouble
 7 # connecting on low-quality public Wi-Fi. You can use `proto tcp-server'
 8 # here instead, but note that this causes extra overhead.
 9 # Choose `proto tcp-server' if you want to redirect port 443 to OpenVPN
10 # to allow connecting over some poorly configured public WiFi networks.
11 proto udp
13 # Use TUN device, as opposed to TAP. In 99% of cases, TUN is what you want.
14 # TAP configuration is outside the scope of this article.
15 dev tun
16 # IP pool to be used for OpenVPN. The parameters as given will put the server
17 # at and clients at addresses up to
18 server
20 # ca.crt file of the certificate authority we just set up.
21 ca /etc/openvpn/easy-rsa/pki/ca.crt
22 # Server certificate.
23 cert /etc/openvpn/easy-rsa/pki/issued/server.crt
24 # Server private key.
25 key /etc/openvpn/easy-rsa/pki/private/server.key
26 # Diffie-Hellman parameters.
27 dh /etc/openvpn/easy-rsa/pki/dh.pem
28 # TLS key.
29 tls-auth /etc/openvpn/easy-rsa/pki/private/ta.key 0
31 # Timeout parameters for sending pings/restarting OpenVPN. See openvpn(8).
32 keepalive 20 120
33 # Enable the following if you want clients to be able to address
34 # one another directly.
35 #client-to-client
37 # Compress the stream with LZO to limit bandwidth.
38 comp-lzo
39 # Allow at most 10 clients to connect at the same time.
40 # Increase if necessary, but make sure your network and server can handle
41 # the load.
42 max-clients 10
44 # Drop root privileges.
45 user nobody
46 group nogroup
48 # Persist keys and TUN device across restarts, since we are dropping root.
49 persist-key
50 persist-tun
52 # Status file.
53 status /var/log/openvpn-status.log

Next, configure the firewall to allow OpenVPN clients. The first step depends on the protocol you chose set in the OpenVPN server configuration. If you chose UDP:

iptables -I INPUT 1 -p udp -m udp --dport 1194 -j ACCEPT

OR, if you chose TCP:

iptables -I INPUT 1 -p tcp -m tcp --dport 1194 -j ACCEPT

Next, set up forwarding and masquerading, and start the server! N.B. it is assumed here that the ethernet device of your server is eth0, and that OpenVPN creates the device tun0 (i.e. nothing else sets up a TUN device before OpenVPN). Change tun0 and/or eth0 as necessary if this is not true.

 1 iptables -A FORWARD -i tun0 -j ACCEPT
 2 iptables -A FORWARD -i tun0 -o eth0 -m state --state RELATED,ESTABLISHED -j ACCEPT
 3 iptables -A FORWARD -i eth0 -o tun0 -m state --state RELATED,ESTABLISHED -j ACCEPT
 4 iptables -A POSTROUTING -s -o eth0 -j MASQUERADE
 5 iptables-save >/etc/iptables/rules.v4
 6 echo "net.ipv4.ip_forward = 1" >>/etc/sysctl.conf
 7 sysctl -p
 8 systemctl enable openvpn.service
 9 systemctl restart openvpn.service
10 ifconfig
11 ss -plunt

Make sure OpenVPN starts correctly: ifconfig should show tun0, along with something like inet netmask destination ss -plunt should show somewhere in the column labelled "Local Address:Port". If this is not the case, check /var/log/daemon.log for entries containing ovpn-server.

You must now copy some certificate files to your clients. As an example, if you have a Linux client named "alpha" and can access your server as root via SSH on, run the following commands as root on "alpha" (assuming OpenVPN is already installed):

1 cd /etc/openvpn
2 scp'{ca.crt,issued/alpha.0.crt,private/alpha.0.key,private/ta.key}' .

An example of a client configuration file (save as /etc/openvpn/client.conf) is listed below.

 1 client
 2 dev tun
 3 # Use `proto udp' if that's what the server uses.
 4 # Else use `proto tcp-client'.
 5 proto udp
 6 remote 1194
 7 resolv-retry infinite
 8 nobind
10 user nobody
11 group nobody
12 persist-key
13 persist-tun
15 ca /etc/openvpn/ca.crt
16 cert /etc/openvpn/alpha.0.crt
17 key /etc/openvpn/alpha.0.key
19 ns-cert-type server
21 tls-auth /etc/openvpn/ta.key 1
23 comp-lzo
25 # Uncomment the following if you want to redirect all client traffic through
26 # the VPN. (Without it, only will be routed through the VPN.)
27 #redirect-gateway def1

On "alpha", run openvpn --config /etc/openvpn/client.conf and check if it will connect. Running ifconfig on "alpha" should show a tun0 with an IP in and a destination of You should be able to SSH over the VPN too, using

ssh root@

If this connects, your VPN works and you're done with this step!

Step 1: PostgreSQL and Synapse

Synapse is the name of the Matrix homeserver reference implementation, which we will be installing in this section. This procedure loosely follows the guide by natrius, with some key differences:

  1. We will thoroughly disable federation.
  2. We will use easy-rsa to add self-signed certificates to the Nginx reverse proxy.
  3. We will use iptables instead of ufw.

Before installing Synapse, first install PostgreSQL and its Python binding:

apt install postgresql python3-psycopg2

Now su to the PostgreSQL admin account and start the PostgreSQL shell:

1 su - postgres
2 psql

As in natrius' guide, create the synapse database and a user to own it:

1 CREATE USER "synapse" WITH PASSWORD 'password';
2 CREATE DATABASE synapse ENCODING 'UTF8' LC_COLLATE='C' LC_CTYPE='C' template=template0 OWNER "synapse";
3 \q

Replace 'password' with a strong, ideally random password. That's all that's needed as far as PostgreSQL goes, so log out of the postgres account.

As in the aforementioned guide, first add the relevant repositories and perform any necessary updates, and then install Synapse:

1 apt install lsb-release wget apt-transport-https
2 wget -O /usr/share/keyrings/matrix-org-archive-keyring.gpg
3 echo "deb [signed-by=/usr/share/keyrings/matrix-org-archive-keyring.gpg] $(lsb_release -cs) main" | tee /etc/apt/sources.list.d/matrix-org.list
4 apt update && apt upgrade
5 apt install matrix-synapse-py3

When asked for a hostname, enter localhost. Next, edit the configuration file, /etc/matrix-synapse/homeserver.yaml. In the listeners section, change the uncommented lines to the following:

1   - port: 8008
2     tls: false
3     type: http
4     x_forwarded: true
5     bind_addresses: ['']
7     resources:
8       - names: [client]
9         compress: false

That is, we bind (to listen to the whole network for client connections) and remove federation.

We will be even more thorough in disabling federation: search for federation_domain_whitelist, which (for the Debian 10 configuration file of Synapse 1.23) should be a commented line with a few domains under it. Add a new line after that comment section:

federation_domain_whitelist: []

Next scroll down to federation_ip_range_blacklist and remove the IPs underneath that line. Replace them with the IPv4 and IPv6 catchalls:

1   - ''
2   - '::/0'

With federation now thoroughly disabled, look for enable_registration: false and uncomment this line. As in natrius' guide, look for registration_shared_secret: <PRIVATE STRING>, uncomment it, and replace <PRIVATE STRING> with a long, randomly generated alphanumeric string, e.g. generated by the command

cat /dev/urandom | tr -dc 'a-zA-Z0-9' | fold -w 32 | head -n 1

Write out the configuration file and exit your editor. We now need to open port 8008 to VPN clients:

1 iptables -I INPUT 1 -s -i tun0 -p tcp -m tcp --dport 8008 -j ACCEPT
2 iptables-save >/etc/iptables/rules.v4

Now start Synapse and check if it is running:

1 systemctl start matrix-synapse.service
2 ss -plunt

The last command should show that Synapse is listening on port 8008.

You can now create an account on your homeserver:

register_new_matrix_user -c /etc/matrix-synapse/homeserver.yaml http://localhost:8008

This will prompt for a username and password, as well as the shared secret you entered into the configuration file earlier.

If you have a PC or laptop that you've given access to the VPN, you can install the desktop Element client on it. You should then be able to connect to using the account you just created. At this moment, the Android and Web clients do NOT support HTTP connections, so these clients cannot connect to your Synapse installation yet. Which brings us to...

Step 2: Nginx and TLS

To allow most clients to connect to your homeserver (provided they have access to the VPN), you will need to set up a reverse proxy that adds a TLS layer so they can communicate over HTTPS. While the Synapse guide by natrius recommends Let's Encrypt certificates, in a fully private context like we are considering, it makes sense to set up your own certificate authority.

I opted to build a new certificate authority in the Synapse configuration directory and reuse the configuration I picked for OpenVPN:

 1 make-cadir /etc/matrix-synapse/easy-rsa
 2 cd /etc/matrix-synapse/easy-rsa
 3 cp /etc/openvpn/easy-rsa/vars .
 4 ./easyrsa init-pki
 5 ./easyrsa --subject-alt-name="IP:" build-ca
 6 # Choose a new CA name when prompted. The password you are prompted for
 7 # should ideally also be different from the one you used for OpenVPN.
 8 ./easyrsa --subject-alt-name="IP:" build-server-full matrix-synapse nopass
 9 # For all clients:
10 ./easyrsa --subject-alt-name="IP:" build-client-full alpha.0 nopass
11 ./easyrsa --subject-alt-name="IP:" build-client-full bravo.0 nopass
12 # ...and so on

Note that we are now including --subject-alt-name="IP:" in our commands. HTTPS requires certificates to be bound to a domain or IP, and browsers tend to get ornery if they are not. We do not need Diffie-Hellman parameters now, but we do need to generate a certificate revocation list so Nginx will be able to reject revoked certificates, and we must also export our client certificates in a format browsers and mobile OSes understand:

# For all clients:
./easyrsa export-p12 alpha.0
./easyrsa export-p12 bravo.0
# ...and so on
./easyrsa gen-crl

The export step asks you to provide a password, which allows you to securely transport the exported certificates to the clients they are to be installed on.

Next up, Nginx. Install the package, remove the default configuration, and create a new one:

1 apt install nginx
2 systemctl stop nginx.service
3 unlink /etc/nginx/sites-enabled/default
4 ln -s /etc/nginx/sites-available/synapse /etc/nginx/sites-enabled/synapse
5 editor /etc/nginx/sites-available/synapse

An example configuration that sets up an HTTPS reverse proxy on port 10443 is given below.

 1 server {
 2     listen 10443 ssl;
 3     server_name localhost;
 5     ssl on;
 6     ssl_certificate /etc/matrix-synapse/easy-rsa/pki/issued/matrix-synapse.crt;
 7     ssl_certificate_key /etc/matrix-synapse/easy-rsa/pki/private/matrix-synapse.key;
 8     ssl_client_certificate /etc/matrix-synapse/easy-rsa/pki/ca.crt;
 9     ssl_crl /etc/matrix-synapse/easy-rsa/pki/crl.pem;
10     # Enable mutual TLS. This option provides maximal security by requiring
11     # per-client certificates. Unfortunately, client-side support for this
12     # tends to be shaky at best, so turn it off and restart Nginx if you are
13     # not able to connect.
14     ssl_verify_client on;
16     access_log /var/log/nginx-synapse-access_log.log;
17     error_log /var/log/nginx-synapse-error_log.log;
19     # There is no need to serve files, and we will not be using
20     # .well-known, since we don't use federation. So we just reverse-proxy
21     # Synapse's HTTP at the root and that's it.
22     location / {
23         proxy_pass;
24         proxy_set_header X-Forwarded-For $remote_addr;
25     }
26 }

Now unblock port 10443 for the VPN and start Nginx:

1 iptables -I INPUT 1 -s -i tun0 -p tcp -m tcp --dport 10443 -j ACCEPT
2 iptables-save >/etc/iptables/rules.v4
3 systemctl enable nginx.service
4 systemctl start nginx.service
5 # Check if it's running
6 ss -plunt

The last command should show Nginx listening on port 10443. Now copy /etc/matrix-synapse/easy-rsa/pki/ca.crt to all clients and import it as a trusted system certificate. Also copy each p12 certificate in /etc/matrix-synapse/easy-rsa/pki/private/ to its respective client and import it.

The desktop and android apps should now be able to at least communicate with the server via HTTPS at If, however, you get an error stating the client did not send the correct certificate, you may have to disable ssl_verify_client in the Nginx configuration file and restart Nginx. Note that the canonical Element-web implementation at may not be able to talk to your server regardless; I believe this is because browsers will restrict accessing local networks from sites loaded from remote servers. Installing element-web locally will help; see Optional Extras at the end of this guide.

Step 3: mautrix-whatsapp

Being a rather niche package, mautrix-whatsapp is not in the Debian repositories and must be self-compiled. It also has some dependencies on versions of packages too recent to be found in the repositories of Debian 10 (buster)

First and foremost, version 1.14 of Go (golang-1.14) is needed at the time of writing, which has been backported to buster. If you are running buster, install Go as follows:

1 echo 'deb buster-backports main' >>/etc/apt/sources.list
2 apt update
3 apt install -t buster-backports golang

Alternatively, if you are using Debian 11 (bullseye) or Unstable (sid), simply install golang directly:

apt install golang

Regardless of your Debian version, you will need to install ffmpeg, a GNU toolchain, and git, if they are not already installed:

apt install ffmpeg make autoconf automake libtool gcc cmake git

Next, we need to install a very recent version of olm, which must be hand-compiled at the time of writing. It is probably best to perform the following steps as a regular user instead of root, but I couldn't be bothered creating a new user. Caveat emptor.

git clone
cd olm
cmake . -Bbuild
cd build
# if you dropped root, use `sudo make install' here instead
make install

We are now ready to compile mautrix-whatsapp. The following steps may also be executed as a regular user if you want. Clone the repo and compile:

1 cd
2 git clone
3 cd mautrix-whatsapp
4 export LD_LIBRARY_PATH=/usr/local/lib
5 ./
6 # Only execute the following if you executed the above as a regular user
7 cd ..
8 sudo cp -r mautrix-whatsapp /root/mautrix-whatsapp

Again as root, copy /root/mautrix-whatsapp/example-config.yaml to /root/mautrix-whatsapp/config.yaml and edit the latter.

  • Under homeserver, set address to http://localhost:8008 and domain to localhost.
  • Under appservice, set hostname to
  • Under database (in the appservice section), set type to postgres and uri to postgres://synapse:<password>@localhost/synapse?sslmode=disable, where <password> should be replaced with the PostgreSQL password you picked earlier.
  • Under bridge, set private_chat_portal_meta to true.

The permissions block under bridge should be something like this:

1     permissions:
2         "*": relaybot
3         "localhost": user
4         "@alice:localhost": admin

Here alice should be replaced with the username you chose when issuing the register_new_matrix_user command in Step 1.

Finally, set directory under logging to something more suitable, like /var/log/mautrix-whatsapp.

Now generate the appservice registration file, copy it to the Synapse configuration directory, and change its ownership so Synapse can read it:

1 cd /root/mautrix-whatsapp
2 export LD_LIBRARY_PATH=/usr/local/lib
3 ./mautrix-whatsapp -g
4 cp registration.yaml /etc/matrix-synapse/wa_registration.yaml
5 chown matrix-synapse /etc/matrix-synapse/wa_registration.yaml

Edit /etc/matrix-synapse/homeserver.yaml. Find the line with app_service_config_files (which is commented out) and, below the comment, add

1 app_service_config_files:
2    - "/etc/matrix-synapse/wa_registration.yaml"

Now restart Synapse:

systemctl restart matrix-synapse.service

It is now time to test mautrix-whatsapp. For testing purposes, start the bridge in the foreground:


Connect to your homeserver with Element-Desktop or Element-Android and start a direct chat with @whatsappbot:localhost. You should get a message saying the room has been set up as the bridge management/status room. Type help and send the message. If the bot replies, you're good; if not, check the logs, and check/adjust your configuration carefully and restart Synapse and mautrix-whatsapp.

If all is working, go back to the terminal where you started /root/mautrix-whatsapp/mautrix-whatsapp and terminate the process with Ctrl-C. Now create a new file /etc/systemd/system/mautrix-whatsapp.service and enter the following:

 1 [Unit]
 2 Description=WhatsApp to Matrix bridge
 3 Wants=matrix-synapse.service
 5 [Service]
 6 Type=exec
 7 Environment="LD_LIBRARY_PATH=/usr/local/lib"
 8 WorkingDirectory=/root/mautrix-whatsapp
 9 ExecStart=/root/mautrix-whatsapp/mautrix-whatsapp
10 Restart=always
11 RestartSec=10
12 SyslogIdentifier=mautrix-whatsapp
14 [Install]

You should now be able to start mautrix-whatsapp using systemd:

1 systemctl start mautrix-whatsapp.service
2 systemctl enable mautrix-whatsapp.service

Wait a few seconds for mautrix-whatsapp to finish loading, then message help to your bot again to make sure it still works. If it does, send it login, and use the actual (official) WhatsApp client to scan the WhatsApp Web QR code that the bot sends you. It should immediately start pulling a few chats, but most likely not your entire history. Ignore it for now. Open your Element client's settings, go to Help & Advanced, and find your access token. Copy it. Then send the bridge bot login-matrix <access_token>, replacing <access_token> with the access token you just copied. This will enable double puppeting; see the Authentication guide.

If you are not content with the meagre amount of chats/messages pulled by the bot, go into each room it created and send !wa delete-portal. Then go back to your terminal/SSH session and edit /root/mautrix-whatsapp/config.yaml again. In the bridge section, edit these settings:

  • initial_chat_sync_count -- this is the amount of WhatsApp group portals that should be created. You probably want to set this to at least the number of WhatsApp groups you're in, including "one-on-one" groups for people you've direct-messaged. If unsure, just pick a number that's definitely larger than that.
  • initial_history_fill_count -- this is the amount of old messages that will be fetched. If you have some very active or old rooms, you will need a very large number here. One caveat: mautrix-whatsapp can get a bit wonky if it's syncing a large amount of messages, and may need to be killed and restarted a few times for the process to complete. That means you should also be monitoring the sync process.
  • sync_max_chat_age -- this is the age cutoff in seconds for synced messages. Set it to something like 2000000000 (two billion seconds, or roughly 63 years) to effectively disable the age cutoff.
  • initial_history_disable_notifications -- this determines whether you get a "new message" notification for every synced old message. If you set initial_history_fill_count to a large number, do yourself a huge favour and set this to true.

Now restart mautrix-whatsapp:

systemctl restart mautrix-whatsapp.service

Finally, send the bridge bot sync --create-all. It will now start to pull the chats you chose. Bear in mind that this can take a very long time. If you notice it stops making progress, restart mautrix-whatsapp and send sync --create-all again: it will continue at the point it was stopped.

You are now basically done, although you probably still have WhatsApp running on your phone. The next step will explain how to migrate it to a VM.

Step 4: migrate WhatsApp proper to a VM

To be added. The official guide by Alistair Francis should get you quite far.

Caveat: if you do not have access to KVM on your server, you will need to use an ARM AVD in softemu mode. In my experience, Android versions higher than 4.1.x are too slow to be usable. However, 4.1.2 on an emulated Nexus One API 16 (WVGA) in softemu mode can run WhatsApp on a very modest VPS (albeit slowly).

Step 5: optional extras

Finally, there are a few things you can do to extend this setup.

Redirect port 443 to OpenVPN

Some public Wi-Fi networks only allow access to a very restricted set of ports so clients are limited to web-browsing and checking email. This annoying bit of shit-tier network administration is easily circumvented by piping your traffic through a VPN, which, of course, you just set up -- but you have to be able to connect to the VPN in the first place, and port 1194 (OpenVPN's default port) is practically guaranteed to be blocked by such networks. Luckily, the default HTTPS port, TCP 443, is practically guaranteed to NOT be blocked, so if your OpenVPN server is using TCP, you can just redirect traffic from 443 to 1194 and log in to your VPN on basically any network. N.B. switch your OpenVPN over to TCP if you want to do this but chose UDP earlier.

A few commands are all that's needed:

1 iptables -A PREROUTING -i eth0 -p tcp -m tcp --dport 443 -j REDIRECT --to-ports 1194
2 iptables -I INPUT 1 -p tcp -m tcp --dport 443 -j ACCEPT
3 iptables-save >/etc/iptables/rules.v4

You can now use remote 443 in your client configuration files. Also be sure to add redirect-gateway def1 on clients that connect to public Wi-Fi!

Install element-web

The official element-web implementation at does not work with a private homeserver, but element-web will work if you install it on your homeserver. Note that the authors of element-web specifically recommend against running element-web on the same domain as the homeserver. However, as access is limited to the VPN, the risk should be limited as long as only trusted and experienced users are allowed access to the VPN in the first place.

Download the latest release (1.75-rc1 at the time of writing; replace the version as necessary in what follows, obviously) from [1], extract it, move it to the proper place, and create the config file:

1 wget
2 tar -xvzf element-v1.7.15-rc.1.tar.gz
3 mv element-v1.7.15-rc.1 /var/www/element-web
4 cd /var/www/element-web
5 cp config.sample.json config.json

Edit config.json. Set "base_url" to "" and "server_name" to "localhost" in the "m.homeserver" block near the top. In the root block, set "disable_custom_urls" to true and "default_federate" to false.

Since we've already set up Nginx, we can just tell it to serve element-web as well, and we can even reuse the certificate authority. Edit the configuration file you created earlier, /etc/nginx/sites-available/synapse and add the following block at the end:

 1 server {
 2     listen 10444 ssl;
 3     server_name localhost;
 5     root /var/www/element-web;
 6     index index.html;
 8     ssl on;
 9     ssl_certificate /etc/matrix-synapse/easy-rsa/pki/issued/element-web.crt;
10     ssl_certificate_key /etc/matrix-synapse/easy-rsa/pki/private/element-web.key;
11     ssl_client_certificate /etc/matrix-synapse/easy-rsa/pki/ca.crt;
12     ssl_crl /etc/matrix-synapse/easy-rsa/pki/crl.pem;
13     ssl_verify_client on;
15     access_log /var/log/nginx-element-web-access_log.log;
16     error_log /var/log/nginx-element-web-error_log.log;
17 }

If you did not manage to get ssl_verify_client to work earlier, remove that line here too. Also, if you did not use port 443 for OpenVPN, you may use listen 443 ssl; instead of listen 10444 ssl; if you want (so you can just point your browser at Finally, add an iptables rule to allow connecting from within the VPN, and restart Nginx:

1 # Replace 10444 with 443 if necessary
2 iptables -I INPUT 1 -s -i tun0 -p tcp -m tcp --dport 10444 -j ACCEPT
3 iptables-save >/etc/iptables/rules.v4
4 systemctl restart nginx.service

Block Facebook on clients

On all clients (except the phone running WhatsApp, if you have not moved it to a VM on the server yet), you can now completely block all Facebook-owned IPs and still retain access to WhatsApp using your Matrix homeserver. While tedious to do by hand (considering Facebook's IP pool grows like an aggressive tumour), it is easily automated on Linux with a cron job and iptables.

First of all, create a new iptables chain that will be used for automated IP blocking, and add it to the front of your INPUT and OUTPUT chains:

1 iptables -N BLOCK
2 iptables -I INPUT 1 -j BLOCK
3 iptables -I OUTPUT 1 -j BLOCK

Next, we will create a script that finds the list of IPs owned by Facebook and adds them to the BLOCK chain. N.B. this requires whois, which should be available in any package manager under that name, so install it first if necessary. Then create the file /etc/cron.hourly/ with the following contents:

 1 #!/bin/bash
 2 iptables=/sbin/iptables
 4 $iptables -F BLOCK
 6 asns="AS32934"
 7 for asn in asns
 8 do
 9     ips="$(whois -h -- -i origin -T route $asn | grep route: | awk '{print $2}')"
10     for i in $ips
11     do
12         $iptables -A BLOCK -d $i -j REJECT --reject-with icmp-host-unreachable
13         $iptables -A BLOCK -s $i -j REJECT --reject-with icmp-host-unreachable
14     done
15 done

This script queries to find all IPs registered under the autonomous system number AS32934, which is Facebook. You may add other ASNs too if you want. Note that although the list "only" contains 200 entries or so (at the time of writing), they include mask bits in CIDR notation: this list in fact represents tens of thousands of IP addresses, all owned by Facebook.

Make sure to make the script executable, and run it once if you don't want to wait until the next hour for cron to do it.

1 chmod +x /etc/cron.hourly/
2 /etc/cron.hourly/
3 iptables-save >/etc/iptables/rules.v4

Honourable mention: Pi-hole

This is an honourable mention because I will not describe the installation process here and it is not related to mautrix-whatsapp. Nevertheless, as following this guide leaves you with a perfectly usable VPN server, and people reading this are likely to be privacy-conscious, I feel it is worthwhile to link Pi-hole here. Pi-hole is a very powerful ad-blocking framework acting at DNS level. Like Synapse, you can configure Pi-hole to only be accessible to clients inside your VPN (by allowing only connections from on device tun0). On your VPN clients, you can set as your primary DNS server, so all clients are protected from malicious ads when connected to the VPN.

Honourable mention: Tor proxy

Your VPS can also serve as a VPN-wide proxy that sends traffic through Tor. Marcus Povey has created a guide on how to set up a Tor proxy with polipo.

Considering Tor's abysmal transfer speed, you probably don't always want to redirect traffic over Tor. Most browsers support proxy auto-config (PAC), which allows you to fine-tune which sites get piped through the proxy. For example, the following PAC uses the Tor proxy to handle .onion domains:

1 function FindProxyForURL (url, host)
2 {
3     if (shExpMatch (host, "*.onion"))
4     {
5         return "PROXY"
6     }
7     return "DIRECT;"
8 }

Much more advanced configurations are possible with proxy auto-config, e.g. redirecting all HTTP traffic but not HTTPS, whitelisting/blacklisting domains.