---
gitea: none
include_toc: true
---

# Reverse proxy with nginx

Clients connecting from the Internet to our Matrix environment will usually
use SSL/TLS to encrypt whatever they want to send. This is one thing that
nginx does better than Synapse.

Furthermore, granting or denying access to specific endpoints is much easier
in nginx.

Synapse listens only on localhost, so nginx has to pass connections on from
the wild west that is the Internet to our server listening on the inside.


# Installing

Installing nginx and the [Let's Encrypt](https://letsencrypt.org/) plugin is
easy:

```
apt install nginx python3-certbot-nginx
```

Get your certificate for the base domain (which is probably not the machine on which
we're going to run Synapse):

```
certbot certonly --nginx --agree-tos -m system@example.com --non-interactive -d example.com
```

Get one for the machine on which we are going to run Synapse too:

```
certbot certonly --nginx --agree-tos -m system@example.com --non-interactive -d matrix.example.com
```

Substitute the correct e-mailaddress and FQDN, or course.


## Automatic renewal {#certrenew}

Certificates have a limited lifetime, and need to be updated every once in a
while. This should be done automatically by Certbot, see if `systemctl
list-timers` lists `certbot.timer`.

However, renewing the certificate means you'll have to restart the software
that's using it. We have 2 or 3 pieces of software that use certificates:
[coturn](../coturn) and/or [LiveKit](../element-call#livekit), and [nginx](../nginx).

Coturn/LiveKit are special with regards to the certificate, see their
respective pages. For nginx it's pretty easy: tell Letsencrypt to restart it
after a renewal.

You do this by adding this line to the `[renewalparams]` in
`/etc/letsencrypt/renewal/<certificate name>.conf`:

```
renew_hook = systemctl try-reload-or-restart nginx
```


# Configuration of domain name {#configdomain}

Let's start with the configuration on the webserver that runs on the domain
name itself, in this case `example.com`.

Almost all traffic should be encrypted, so a redirect from http to https seems
like a good idea.

However, `.well-known/matrix/client` has to be available via http and https,
so that should *NOT* be redirected to https. Some clients don't understand the
redirect and will therefore not find the server if you redirect everything.

Under the `server_name` (the "domain name", the part after the username) you
will need a configuration like this:

```
server {
    listen 80;
    listen [::]:80;
    listen 443 ssl;
    listen [::]:443 ssl;

    ssl_certificate /etc/letsencrypt/live/example.com/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/example.com/privkey.pem;
    include /etc/letsencrypt/options-ssl-nginx.conf;
    ssl_dhparam /etc/ssl/dhparams.pem;

    server_name example.com;

    location /.well-known/matrix/client {
       return 200 '{
          "m.homeserver": {"base_url": "https://matrix.example.com"},
       }';
       default_type application/json;
    }

    location /.well-known/matrix/server {
       return 200 '{"m.server": "matrix.example.com"}';
       default_type application/json;
    }

    location / {
      if ($scheme = http) {
        return 301 https://$host$request_uri;
      }
    }

    access_log /var/log/nginx/example_com-access.log;
    error_log /var/log/nginx/example_com-error.log;

}
```

This defines a server that listens on both http and https. It hands out two
.well-known entries over both http and https, and every other request over
http is forwarded to https.

Be sure to substitute the correct values for `server_name`, `base_url` and the
certificate files (and [renew the certificate](#renewcert)).

See this [full configuration example](domain.conf) with some extra stuff.


# Configuration of the reverse proxy

For the actual proxy in front of Synapse, this is what you need: forward ports
443 and 8448 to Synapse, listening on localhost, and add a few headers so
Synapse know's who's on the other side of the line.

```
server {
	listen 443 ssl;
	listen [::]:443 ssl;

	# For the federation port
	listen 8448 ssl default_server;
	listen [::]:8448 ssl default_server;

	ssl_certificate /etc/letsencrypt/live/matrix.example.com/fullchain.pem;
	ssl_certificate_key /etc/letsencrypt/live/matrix.example.com/privkey.pem;
	include /etc/letsencrypt/options-ssl-nginx.conf;
	ssl_dhparam /etc/ssl/dhparams.pem;

	server_name matrix.example.com;

	location ~ ^(/_matrix|/_synapse/client) {
		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;
		proxy_http_version 1.1;
	}

}
```

Again, substitute the correct values. Don't forget to open the relevant ports
in the firewall. Ports 80 and 443 may already be open, 8448 is probably not.

This is a very, very basic configuration; just enough to give us a working
service. See this [complete example](revproxy.conf) which also includes
[Draupnir](../draupnir) and a protected admin endpoint.

# Element Web

You can host the webclient on a different machine, but we'll run it on the
same one in this documentation. You do need a different FQDN however, you
can't host it under the same name as Synapse, such as:
```
https://matrix.example.com/element-web
```
So you'll need to create an entry in DNS and get a TLS-certificate for it (as
mentioned in the [checklist](../checklist.md)).

Other than that, configuration is quite simple. We'll listen on both http and
https, and redirect http to https:

```
server {
    listen 80;
    listen [::]:80;
    listen 443 ssl http2;
    listen [::]:443 ssl http2;
    
    ssl_certificate /etc/letsencrypt/live/element.example.com/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/element.example.com/privkey.pem;
    include /etc/letsencrypt/options-ssl-nginx.conf;
    ssl_dhparam /etc/ssl/dhparams.pem;
    
    server_name element.example.com;
    
    location / {
        if ($scheme = http) {
            return 301 https://$host$request_uri;
        }
        add_header X-Frame-Options SAMEORIGIN;
        add_header X-Content-Type-Options nosniff;
        add_header X-XSS-Protection "1; mode=block";
        add_header Content-Security-Policy "frame-ancestors 'self'";
    }
    
    root /usr/share/element-web;
    index index.html;
    
    access_log /var/log/nginx/elementweb-access.log;
    error_log /var/log/nginx/elementweb-error.log;
}
```

This assumes Element Web is installed under `/usr/share/element-web`, as done
by the Debian package provided by Element.io.

# Synapse-admin {#synapse-admin}

If you also [install Synapse-Admin](../synapse-admin), you'll want to create
another vhost, something like this:

```
server {
    listen 443 ssl;
    listen [::]:443 ssl;
    
    ssl_certificate /etc/letsencrypt/live/admin.example.com/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/admin.example.com/privkey.pem;
    include /etc/letsencrypt/options-ssl-nginx.conf;
    ssl_dhparam /etc/ssl/dhparams.pem;
    
    server_name admin.example.com;
    
    root /var/www/synapse-admin;
    
    access_log /var/log/nginx/admin-access.log;
    error_log /var/log/nginx/admin-error.log;
}
```

You'll need an SSL certificate for this, of course. But you'll also need to
give it access to the `/_synapse/admin` endpoint in Synapse.

You don't want this endpoint to be available for just anybody on the Internet,
so restrict access to the IP-addresses from which you expect to use
Synapse-Admin.

In `/etc/nginx/sites-available/synapse` you want to add this bit:

```
location ~ ^/_synapse/admin {
    allow 127.0.0.1;
    allow ::1;
    allow 111.222.111.222;
    allow dead:beef::/64;
    deny all;
    
    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;
    proxy_http_version 1.1;
}
```

This means access to `/_synapse/admin` is only allowed for the addresses
mentioned, but will be forwarded to Synapse in exactly the same way as
"normal" requests.


# LiveKit {#livekit}

If you run an SFU for Element Call, you need a virtual host for LiveKit. Make
sure you install, configure and run [Element Call LiveKit](../element-call#livekit).
Then create a virtual host much like this:

```
server {
    listen 443 ssl;
    listen [::]:443 ssl;
    
    ssl_certificate /etc/letsencrypt/live/livekit.example.com/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/livekit.example.com/privkey.pem;
    include /etc/letsencrypt/options-ssl-nginx.conf;
    ssl_dhparam /etc/ssl/dhparams.pem;
    
    server_name livekit.example.com;
    
    # This is lk-jwt-service
    location ~ ^(/sfu/get|/healthz) {
        proxy_pass http://[::1]:8080;
        proxy_set_header Host $host;
        proxy_set_header X-Forwarded-Server $host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto $scheme;
    }
    
    location / {
        proxy_pass http://[::1]:7880;
        proxy_set_header Connection "upgrade";
        proxy_set_header Upgrade $http_upgrade;
        
        proxy_set_header Host $host;
        proxy_set_header X-Forwarded-Server $host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto $scheme;
    }
    
    access_log /var/log/nginx/livekit-access.log;
    error_log /var/log/nginx/livekit-error.log;
}
```


# Element Call widget {#callwidget}

If you self-host the [Element Call widget](../element-call#widget), this
should be the configuration to publish that:

```
server {
    listen 443 ssl;
    listen [::]:443 ssl;
    
    ssl_certificate /etc/letsencrypt/live/call.example.com/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/call.example.com/privkey.pem;
    include /etc/letsencrypt/options-ssl-nginx.conf;
    ssl_dhparam /etc/ssl/dhparams.pem;
    
    server_name call.example.com;
    
    root /var/www/element-call;
    
    location /assets {
        add_header Cache-Control "public, immutable, max-age=31536000";
    }
    
    location /apple-app-site-association {
        default_type application/json;
    }
    
    location /^config.json$ {
        alias public/config.json;
        default_type application/json;
    }
    
    location / {
        try_files $uri /$uri /index.html;
        add_header Cache-Control "public, max-age=30, stale-while-revalidate=30";
    }
    
    access_log /var/log/nginx/call-access.log;
    error_log /var/log/nginx/call-error.log;
}
```


# Firewall

For normal use, at least ports 80 and 443 must be openend, see [Firewall](../firewall).
