Sh
Applications AWS

Synapse Homeserver 1.163.0 on AWS User Guide

| Product: Synapse Homeserver 1.163.0 on Ubuntu 24.04 on AWS

Overview

This guide covers launching and using the Synapse Homeserver 1.163.0 on Ubuntu 24.04 AMI from cloudimg on Amazon EC2. It is one version of the cloudimg team chat and video listing on AWS Marketplace; you pick it as the software version when you subscribe and launch.

Synapse is the reference server for the Matrix protocol, an open standard for secure, decentralised real time chat. It keeps your organisation's messages, rooms and files on an instance in your own AWS account, and it can either federate with the wider Matrix network or run entirely private. The image is a complete single instance stack:

  • Synapse 1.163.0 from the official packages.matrix.org repository (Ubuntu's own archive carries a much older, unsupported release), run unmodified.
  • Element Web 1.12.32, the open source Matrix web client, served from the same HTTPS address so people can sign in from a browser with nothing to install.
  • coturn 4.18.0, a TURN and STUN relay, so voice and video calls connect even when both people sit behind strict firewalls or carrier grade NAT.
  • PostgreSQL 16 as the database and nginx as the only public web listener.
  • A dedicated media volume: uploads, avatars and the remote media cache live on their own EBS volume at /var/lib/matrix-synapse/media_store, so they can grow without touching the root volume.

What cloudimg adds:

  • Every secret is generated on your instance at first boot. The homeserver signing key (its cryptographic identity), the PostgreSQL password, registration_shared_secret, macaroon_secret_key, form_secret, the secret shared between Synapse and the TURN relay, the TLS certificate and a unique administrator password. Two instances launched from this AMI are distinct servers; nothing secret is baked into the image.
  • Registration is closed. Nobody can create an account on your server unless you create it for them.
  • Fail closed. Until first boot has finished, the web address answers HTTP 503 and Synapse and coturn refuse to start, so a half configured homeserver is never reachable.
  • HTTPS from the first boot. nginx serves the web client, the client API and .well-known discovery on 443 and federation on 8448 with a per instance certificate. Port 80 redirects to HTTPS.
  • Fixed host names. nginx answers only for this server's configured name and addresses and closes the connection for anything else; redirects always go to the configured name, never to whatever a request claims.
  • Real client addresses. Synapse and nginx log the address the connection really came from; a client cannot spoof it with an X-Forwarded-For header.
  • Instance metadata blocked. Every service (nginx, Synapse, coturn, PostgreSQL and the first boot unit) is denied the EC2 instance metadata address. One small unit reads only this instance's own IP address at boot. Synapse refuses to fetch link local, loopback and private addresses, and coturn refuses to relay to them. The AMI requires IMDSv2.
  • Loopback only internals. PostgreSQL and the Synapse process listen on 127.0.0.1 only, and the Synapse admin API is not published through nginx.
  • Fully patched Ubuntu 24.04 LTS at build time, with unattended security upgrades switched on.

Default user: ubuntu · Architecture: x86_64 · Recommended instance type: m5.large (2 vCPU, 8 GiB). Synapse, PostgreSQL and coturn together need more memory than a 4 GiB type gives them.

Matrix and the Matrix logo are trademarks of The Matrix.org Foundation C.I.C. Element is a registered trademark of Element Creations Ltd. coturn is an independent open source project. This image is produced by cloudimg and is not affiliated with, sponsored by, or endorsed by The Matrix.org Foundation, Element Creations Ltd, or the coturn project. It ships the free and open source software unmodified: Synapse under the GNU AGPL v3 or later, Element Web under the GNU AGPL v3 or GPL v3 (your choice), and coturn under the BSD 3-Clause licence. Sources: https://github.com/element-hq/synapse, https://github.com/element-hq/element-web and https://github.com/coturn/coturn.

Before you start: your server name is permanent

Every Matrix homeserver has a server name. It is part of every user ID (@alice:example.com), every room ID and every message signature the server produces, and it cannot be changed once people use the server.

  • If you do nothing, first boot sets the server name to the instance's public IPv4 address. That works at once and suits evaluation, an internal deployment or a proof of concept. Use an Elastic IP if you keep it, because a stop and start otherwise changes the address.
  • For anything permanent, and for federation, use your own domain, set before the first boot through user data (see "Using your own domain").
  • If you have already booted and nobody has signed up, matrix-set-server-name re-provisions the server under a new name. It is destructive by design and tells you whether it is still safe.

Prerequisites

  • An AWS account subscribed to the listing, an EC2 key pair and a VPC subnet with a route to the internet.
  • A security group that allows:
Port Protocol Purpose Open it to
22 TCP SSH administration your own networks only
80 TCP Redirects to HTTPS; ACME http-01 if you install a trusted certificate your users
443 TCP Element Web, the Matrix client API and .well-known discovery your users
8448 TCP Matrix federation; leave closed for a private server anywhere, only if you federate
3478 TCP and UDP TURN and STUN for call setup and relay your users
5349 TCP and UDP TURN over TLS and DTLS your users
49152-49231 UDP the bounded media relay range coturn allocates calls from your users

PostgreSQL (5432) and Synapse (8008) are never reachable from outside the instance, so do not open them.

Step 1: Launch the instance

In the AWS Marketplace listing choose Continue to Subscribe, then Continue to Configuration, pick Synapse Homeserver 1.163.0 on Ubuntu 24.04 as the software version and your Region, then Launch through EC2. Choose m5.large, your key pair and a security group as above.

With the AWS CLI (replace the values in angle brackets; add --user-data file://server-name.yaml to set your own domain, see below):

aws ec2 run-instances --region <region> --image-id <ami-id-from-the-listing> \
  --instance-type m5.large --key-name <your-key-pair> \
  --security-group-ids <your-security-group> --subnet-id <your-subnet> \
  --metadata-options HttpTokens=required,HttpEndpoint=enabled \
  --tag-specifications 'ResourceType=instance,Tags=[{Key=Name,Value=synapse}]'

Step 2: Connect to your instance

Version SSH user
Synapse Homeserver 1.163.0 on Ubuntu 24.04 ubuntu
ssh -i <your-key.pem> ubuntu@<public-ip>

Step 3: Check the services

First boot generates every secret, creates the database and the administrator, then opens the front door. It takes under a minute:

systemctl is-active postgresql@16-main matrix-synapse coturn nginx matrix-stack-firstboot
sudo test -f /var/lib/cloudimg/matrix-stack-firstboot.done && echo FIRSTBOOT_DONE
active
active
active
active
active
FIRSTBOOT_DONE

If you connect in the first minute and the web address answers HTTP 503, first boot is still running; follow it with sudo journalctl -u matrix-stack-firstboot -f.

The versions on the image, and the Matrix client API answering through nginx:

dpkg-query -W -f='${Package} ${Version}\n' matrix-synapse-py3
printf 'element-web %s\n' "$(cat /var/www/element/version)"
printf 'coturn %s\n' "$(/usr/local/bin/turnserver --version)"
curl -ks -o /dev/null -w 'client API -> HTTP %{http_code}\n' https://127.0.0.1/_matrix/client/versions
matrix-synapse-py3 1.163.0+noble1
element-web v1.12.32
coturn 4.18.0
client API -> HTTP 200

Step 4: Get your credentials

sudo cat /root/matrix-stack-credentials.txt

The file is readable by root only:

matrix.server.name=<SERVER_NAME>
matrix.web.url=https://<SERVER_NAME>/
matrix.homeserver.url=https://<SERVER_NAME>
matrix.admin.user=<ADMIN_USER>
matrix.admin.pass=<ADMIN_PASSWORD>
matrix.admin.mxid=@<ADMIN_USER>:<SERVER_NAME>
matrix.federation.port=8448
matrix.turn.realm=<SERVER_NAME>

When you need to share it, for example in a support request, print it with the password masked:

sudo sed -E 's/^(matrix\.admin\.pass=).*/\1********/' /root/matrix-stack-credentials.txt | grep '^matrix\.'

The signing key at /etc/matrix-synapse/homeserver.signing.key is this server's identity. It was generated on this instance and exists nowhere else: back it up with the database, and never copy it to another server.

Step 5: Check the security model

Every socket bound to something other than loopback. This is the whole public surface, and it matches the security group table above (68 is the DHCP client):

sudo ss -lntuH | awk '{print $1, $5}' | grep -vE '127\.0\.0|\[::1\]|%lo' | sed -E 's/^(tcp|udp) (.*):([0-9]+)$/\1 \3/' | sort -u -k1,1 -k2,2n
tcp 22
tcp 80
tcp 443
tcp 3478
tcp 5349
tcp 8448
udp 68
udp 3478
udp 5349

PostgreSQL and the Synapse process listen on loopback only:

sudo ss -lntH | awk '{print $4}' | grep -E ':(5432|8008)$' | sort
127.0.0.1:5432
127.0.0.1:8008

Registration is closed, unknown host names are refused, and port 80 redirects to the configured name:

curl -ks -o /dev/null -w 'register          -> HTTP %{http_code}\n' -X POST https://127.0.0.1/_matrix/client/v3/register \
  -H 'Content-Type: application/json' -d '{"username":"walkup","password":"not-allowed-123"}'
curl -ks -o /dev/null -w 'Host: evil.example -> %{http_code} (000 = connection closed)\n' -H 'Host: evil.example' https://127.0.0.1/ || true
S=$(sudo sed -n 's/^matrix\.server\.name=//p' /root/matrix-stack-credentials.txt)
curl -s -o /dev/null -w 'http://            -> %{http_code} %{redirect_url}\n' -H "Host: $S" http://127.0.0.1/
register          -> HTTP 403
Host: evil.example -> 000 (000 = connection closed)
http://            -> 301 https://<SERVER_NAME>/

Instance metadata is denied to every service; only the small address reader may reach it:

for u in nginx matrix-synapse coturn postgresql@16-main matrix-stack-firstboot; do
  printf '%-24s %s\n' "$u" "$(systemctl show -p IPAddressDeny --value $u.service)"
done
nginx                    168.63.129.16/32 169.254.169.254/32 fd00:ec2::254/128
matrix-synapse           168.63.129.16/32 169.254.169.254/32 fd00:ec2::254/128
coturn                   168.63.129.16/32 169.254.169.254/32 fd00:ec2::254/128
postgresql@16-main       168.63.129.16/32 169.254.169.254/32 fd00:ec2::254/128
matrix-stack-firstboot   168.63.129.16/32 169.254.169.254/32 fd00:ec2::254/128

The administrator password authenticates and a wrong one is refused. The request bodies go to curl on standard input, so the password never appears in the process list or your shell history. Synapse rate limits sign ins, so a burst of attempts may answer 429 instead of 403; both mean refused:

sudo bash -c '
  F=/root/matrix-stack-credentials.txt
  U=$(sed -n "s/^matrix\.admin\.user=//p" $F)
  body() { printf "{\"type\":\"m.login.password\",\"identifier\":{\"type\":\"m.id.user\",\"user\":\"%s\"},\"password\":\"%s\"}" "$U" "$1"; }
  login() { curl -ks -o /dev/null -w "%{http_code}" -X POST https://127.0.0.1/_matrix/client/v3/login -H "Content-Type: application/json" --data-binary @-; }
  GOOD=$(sed -n "s/^matrix\.admin\.pass=//p" $F | { read -r P; body "$P"; } | login)
  BAD=$(body "wrong-password-xyz" | login)
  case "$BAD" in 403|429) B="refused ($BAD)";; *) B="UNEXPECTED ($BAD)";; esac
  echo "correct password -> $GOOD; wrong password -> $B"
  [ "$GOOD" = 200 ] && case "$BAD" in 403|429) true;; *) false;; esac
'
correct password -> 200; wrong password -> refused (403)

Step 6: Sign in to Element

Open https://<SERVER_NAME>/ in a browser (the public IP unless you set a domain). The certificate is self signed for this instance, so the browser warns on the first visit; accept it, or install a trusted certificate as described below. Sign in with the administrator user and password from Step 4.

The Element sign in page, with this instance's address as the account provider

After signing in you land on your own homeserver.

Element signed in as the administrator, with a room on the room list and the get started actions

Matrix verifies devices against each other so end to end encrypted history can be shared safely. The first device to sign in sets up the account's encryption identity; later devices are asked to confirm against one you already trust. That is normal Matrix behaviour.

Step 7: Create a room and send a message

Use the + beside Home to create a room, invite people and type into the composer. Rooms you create from Element are end to end encrypted by default. The room below was created through the API without encryption, which Element points out at the top of the timeline.

A room on this homeserver with messages sent from the browser and the delivery tick

Settings, Account shows the full Matrix ID your homeserver issued, in the form @user:server-name.

Element account settings showing the administrator's Matrix ID on this homeserver

Step 8: Voice and video calls

Calls between people on restrictive networks are relayed by coturn. Synapse gives each signed in client a short lived TURN credential derived from a secret it shares with the relay, so there is no static call password to leak:

sudo bash -c '
  F=/root/matrix-stack-credentials.txt
  U=$(sed -n "s/^matrix\.admin\.user=//p" $F)
  for i in $(seq 1 12); do
    T=$(sed -n "s/^matrix\.admin\.pass=//p" $F | python3 -c "import json,sys; print(json.dumps({\"type\":\"m.login.password\",\"identifier\":{\"type\":\"m.id.user\",\"user\":sys.argv[1]},\"password\":sys.stdin.read().strip()}))" "$U" \
        | curl -ks -X POST https://127.0.0.1/_matrix/client/v3/login -H "Content-Type: application/json" --data-binary @- \
        | python3 -c "import json,sys; print(json.load(sys.stdin).get(\"access_token\",\"\"))")
    [ -n "$T" ] && break; sleep 10
  done
  [ -n "$T" ] || { echo "sign in is rate limited; wait a few minutes and retry"; exit 1; }
  H=$(mktemp /run/turn.XXXXXX); printf "Authorization: Bearer %s\n" "$T" > $H
  curl -ks -H @$H https://127.0.0.1/_matrix/client/v3/voip/turnServer \
    | python3 -c "import json,sys; d=json.load(sys.stdin); print(\"ttl:\", d[\"ttl\"]); [print(\"uri:\", u) for u in d[\"uris\"]]"
  curl -ks -H @$H -X POST -d "{}" https://127.0.0.1/_matrix/client/v3/logout >/dev/null; rm -f $H
'
ttl: 86400
uri: turn:<SERVER_NAME>:3478?transport=udp
uri: turn:<SERVER_NAME>:3478?transport=tcp
uri: turns:<SERVER_NAME>:5349?transport=tcp

The relay refuses to forward traffic to loopback, private ranges, link local addresses and the EC2 instance metadata address 169.254.169.254, so an authenticated user cannot use it to reach your VPC:

sudo grep -cE '^denied-peer-ip=' /etc/coturn/turnserver.conf
sudo grep -E '^denied-peer-ip=169\.254|^no-multicast-peers|^use-auth-secret' /etc/coturn/turnserver.conf
20
use-auth-secret
no-multicast-peers
denied-peer-ip=169.254.0.0-169.254.255.255

Step 9: Add users

Registration is closed, so you create accounts. register_new_matrix_user reads the shared secret from this instance's own configuration. Give it the new password through a root only file rather than on the command line, so it is never written to /var/log/auth.log or your shell history:

sudo install -m 600 /dev/null /root/newuser.pass
sudo nano /root/newuser.pass          # type the new user's password, save
sudo /opt/venvs/matrix-synapse/bin/register_new_matrix_user \
  -c /etc/matrix-synapse/conf.d/90-cloudimg-instance.yaml \
  -u alice --password-file /root/newuser.pass --no-admin http://127.0.0.1:8008
sudo shred -u /root/newuser.pass

Use --admin instead of --no-admin for another server administrator. Run it without --password-file and it prompts for the password instead.

To let people sign up themselves, set enable_registration: true in /etc/matrix-synapse/conf.d/10-cloudimg.yaml, pair it with email verification or a registration token so you are not an open relay for spam, and sudo systemctl restart matrix-synapse. Synapse also supports SAML, CAS and OpenID Connect single sign on; configure your provider in the same directory.

The Synapse admin API listens on loopback only. Reach it over an SSH tunnel, for example ssh -L 8008:127.0.0.1:8008 ubuntu@<public-ip>, then call http://127.0.0.1:8008/_synapse/admin/... with an administrator's access token.

Using your own domain

Check whether it is still safe to change the server name:

sudo matrix-set-server-name --check

On a freshly launched instance this reports only the first boot administrator and no rooms. Once people have signed up it says the change is no longer safe.

The best way is to never boot with the wrong name. Pass user data at launch so the name is in place before first boot:

#cloud-config
write_files:
  - path: /etc/cloudimg/matrix-stack/server-name
    permissions: '0644'
    content: |
      matrix.your-domain.example

If the instance is already running and nobody has signed up, re-provision it. This destroys the database, the signing key and every account and creates a new administrator, because a server name cannot be renamed in place:

sudo matrix-set-server-name matrix.your-domain.example --yes
sudo cat /root/matrix-stack-credentials.txt

Point a DNS A record for the name at the instance's Elastic IP, then install a trusted certificate:

sudo apt-get install -y certbot python3-certbot-nginx
sudo certbot --nginx -d matrix.your-domain.example

Point coturn at the same certificate by editing cert= and pkey= in /etc/coturn/turnserver.conf, then sudo systemctl restart coturn.

For short user IDs (@alice:your-domain.example) while the server runs at matrix.your-domain.example, set the server name to the short domain before first boot and publish the two .well-known/matrix documents this instance serves from the web server at that short domain.

Federation

Federation lets your users talk to people on other Matrix servers. It needs port 8448 open, a real domain and a certificate other servers trust. What this server advertises:

curl -ks https://127.0.0.1/.well-known/matrix/server; echo
curl -ks -o /dev/null -w 'federation API -> HTTP %{http_code}\n' https://127.0.0.1:8448/_matrix/federation/v1/version
{ "m.server": "<SERVER_NAME>:8448" }
federation API -> HTTP 200

For a private deployment, leave 8448 closed in the security group. Everything else keeps working; your users simply cannot reach or be reached by other homeservers. Synapse never fetches link local, loopback or private addresses when talking to other servers or downloading remote media (ip_range_blacklist), and URL previews are off by default.

Media storage

Uploads, avatars and cached remote media live on their own EBS volume, so you can grow it without touching the root volume:

df -h --output=source,size,target /var/lib/matrix-synapse/media_store | tail -1

To grow it, modify the volume in the EC2 console, then sudo resize2fs the device shown above.

Backups

Back up three things together: the PostgreSQL database, the signing key and the media store. The signing key cannot be regenerated; without it a restored server is a different server.

sudo -u postgres pg_dump synapse | gzip > /var/backups/synapse-$(date +%F).sql.gz
sudo cp /etc/matrix-synapse/homeserver.signing.key /var/backups/
sudo tar czf /var/backups/media-$(date +%F).tar.gz -C /var/lib/matrix-synapse media_store

EBS snapshots of both volumes, taken together, are an equally good whole server backup.

Updates

Ubuntu security updates install automatically. Synapse updates come from the official repository:

sudo apt-get update && sudo apt-get install --only-upgrade matrix-synapse-py3

Support

cloudimg supports this image. Contact support@cloudimg.co.uk with your AWS account ID, the Region and the instance ID. For choosing between the versions in this listing, see the team chat and video guide.