Articles / Privacy

  • matrix
  • synapse
  • metadata
  • federation
  • self-hosting
  • privacy

Why Your Self-Hosted Matrix Server Leaks Who Talks to Whom

You switched on end-to-end encryption, so the content of your Matrix chats is safe. The surprise is how much else is not. Every homeserver in a room can see who is in it, who spoke, when, and often who read what.

This is not a bug. It is what federation needs in order to work. Here is what actually crosses the wire, what sits in your database, and which settings genuinely help.

What an encrypted event looks like to a server

Start with what your homeserver stores for a message in an encrypted room. It is roughly this (trimmed, ciphertext shortened):

{
  "type": "m.room.encrypted",
  "room_id": "!abc123:example.org",
  "sender": "@alice:example.org",
  "origin_server_ts": 1760000000000,
  "content": {
    "algorithm": "m.megolm.v1.aes-sha2",
    "session_id": "k3J...",
    "ciphertext": "AwgAEpA..."
  }
}

Only the ciphertext is secret. The room, the sender, the timestamp and the event type are all in the clear, because the server has to route, order and authorise events without being able to read them.

Federation copies the room to every server in it

A Matrix room does not live on one server. It is a shared event graph replicated to every homeserver that has a user in it. Once one of your users joins a room, every server in that room can see your server's users in the membership list, and you can see theirs.

Room state is also unencrypted in a normal room: the name, topic, avatar, power levels and the join and leave events. Megolm encrypts message events, not the state that describes the room.

So a server admin on any participating server can answer "which accounts were in this room, and when did they join?" with a database query. For a large public room, that is a lot of strangers holding a list that includes you.

The chatter that is not even messages

Beyond events, servers swap ephemeral data units (EDUs) with each other. These are small, frequent and rarely thought about:

  • typing notifications (m.typing), which say who is composing a message in which room right now
  • read receipts (m.receipt), which say who has read up to which event, and when
  • presence (m.presence), which says whether someone is online
  • device list updates, which say when someone adds or removes a device

Receipts are the quiet one. Even though nobody outside the room can read the message, a remote server can see that Bob read event X eleven seconds after Alice sent it. That is a conversation pattern, and it is plain text.

Quick detour: why servers can see this at all

Hang on, why not just encrypt the metadata too? Because the server is a participant in the protocol, not a dumb pipe. It must check that a sender is allowed to post, resolve conflicting state, and decide which events to hand to which client.

Hiding all of that would mean a very different design, closer to a sealed-sender or mixnet approach. Matrix chose a federated, replicated model instead, and the metadata is the price of that choice.

Leaks outside the protocol

Your own infrastructure adds more. None of these are Matrix-specific, but all of them are easy to forget:

  • reverse proxy access logs, which record every client IP, path and timestamp
  • federation discovery, where DNS lookups and .well-known fetches show which servers yours talks to
  • TLS connections between servers, where the server name is visible on the network unless you use something like Encrypted Client Hello
  • push notifications, which pass through a push gateway and, for mobile, Apple or Google

On push: clients such as Element can ask for notifications that carry only an event ID rather than the message content, so the message text does not reach the push provider. But the provider still sees that your device got a notification, and when.

What you can actually do about it

Be honest about the goal first. If your threat is "a stranger's server in a big public room", you can cut that. If your threat is "my own server logs", that is your own hygiene. Neither fixes the fact that federation shares membership.

The strongest option is to stop federating with everyone. In Synapse you can allowlist the servers you talk to:

# homeserver.yaml
federation_domain_whitelist:
  - friends.example.net
  - family.example.org

presence:
  enabled: false

With an allowlist, your server only exchanges events with those names. Turning presence off removes the "is online" signal. Check the Synapse documentation for your version, since option names and defaults do move between releases.

Other steps worth taking:

  1. Disable or shorten access logging on the reverse proxy, or at least rotate it aggressively.
  2. Keep sensitive conversations in private, invite-only rooms with a small, known set of servers.
  3. Avoid joining large public rooms with the same account you use for private chats.
  4. Use a self-hosted push gateway if you are comfortable running one, and prefer event-ID-only notifications.
  5. Do not publish your room directory over federation unless you mean to.

Verifying what your server holds

If you run Synapse on PostgreSQL, you can see for yourself what a room exposes. The membership table is the obvious place to look:

SELECT user_id, membership
FROM room_memberships
WHERE room_id = '!abc123:example.org';

Table and column names can differ between versions, so inspect the schema first. The point stands regardless: this is data any homeserver admin in the room can produce about you, and a legal request to them does not need to go through you.

If you want a single rule of thumb, it is this: Matrix encryption protects what you said, not who you said it to. Federation makes the second part visible to people you will never meet, and the only real lever is to meet fewer of them.