XMPP (the Extensible Messaging and Presence Protocol) is an open, federated protocol for chat and presence: anyone can run a server, and users on different servers talk to each other the way email users do. The core is IETF standard RFC 6120, transport security is TLS with SASL login (SCRAM by preference), and end to end encryption is an add-on called OMEMO (XEP-0384). To self-host it securely, require TLS on every connection, turn off plaintext and open registration, rate limit, set retention, keep the server patched, and switch OMEMO on in clients rather than assuming it is on.
XMPP started life as Jabber in January 1999. Its core has been through two rounds of IETF review and several mature open source servers exist, but much of its security depends on choices in a server config file and on which extensions each client implements. This guide goes layer by layer, from the XML stream up to OMEMO, then walks through hardening a self-hosted server.
If you are weighing XMPP against Matrix, read this alongside our self-hosted Matrix homeserver guide. For the cryptography behind OMEMO and its relatives, our end to end encrypted messaging explainer covers the Double Ratchet and MLS in more depth than we repeat here.
Key takeaways
- XMPP is defined by the IETF in RFC 6120 (core), RFC 6121 (instant messaging and presence) and RFC 7622 (address format). Everything else, including group chat, message archives, file upload and OMEMO, is an XMPP Extension Protocol (XEP) published by the XMPP Standards Foundation (XSF).
- Every connection is a long-lived XML stream. Three stanza types carry all traffic:
message,presenceandiq. - Transport security is TLS (STARTTLS on ports 5222 and 5269, or direct TLS per XEP-0368), then SASL authentication. RFC 6120 makes SCRAM-SHA-1 and SCRAM-SHA-1-PLUS the mandatory-to-implement password mechanisms; modern servers add SCRAM-SHA-256 and TLS channel binding.
- Servers authenticate each other with certificates (SASL EXTERNAL) or with Server Dialback (XEP-0220), which the XSF itself calls weak identity verification when DNSSEC is not used.
- OMEMO gives end to end encryption with X3DH and the Double Ratchet, per device. It does not hide metadata: servers still see who talks to whom, when, from how many devices, and your contact list.
- XEP-0384 is still Experimental (version 0.9.1, 6 April 2026), and many clients still speak an older OMEMO namespace. Check that your clients interoperate before relying on it.
- Most real-world XMPP abuse comes from open registration on federated servers. Invite-only registration, rate limits and server-side blocking are the main controls.
What XMPP is and where it came from
XMPP is a protocol for exchanging small pieces of structured XML in near real time between network endpoints. Its first and best known use is instant messaging and presence (whether a contact is online), but the same plumbing has carried publish-subscribe feeds, device control, game chat and voice and video signalling.
According to the XSF's own history page, Jeremie Miller announced Jabber in January 1999 as an open technology for instant messaging and presence, and an open source server called jabberd, several clients and libraries followed that year. In August 1999 the Jabber community pledged support for the IETF standards process. The IETF formed an XMPP working group, and in October 2004 it published RFC 3920 and RFC 3921 as Proposed Standards. In March 2011 those were replaced by RFC 6120 (core) and RFC 6121 (instant messaging and presence). The address format, first split out as RFC 6122, was updated again in September 2015 as RFC 7622 to use the PRECIS framework for internationalized strings. RFC 7590, also from June 2015, tightened the TLS rules.
Timeline
| Date | Event | Source |
|---|---|---|
| January 1999 | Jeremie Miller announces Jabber; jabberd open source server follows | xmpp.org history |
| August 1999 | Jabber community pledges support for the IETF process | xmpp.org history |
| October 2004 | RFC 3920 (core) and RFC 3921 (IM and presence) published | RFC 3920, xmpp.org history |
| March 2011 | RFC 6120 and RFC 6121 obsolete 3920 and 3921 | RFC 6120, RFC 6121 |
| March 2011 | RFC 6122 address format (stringprep based) | RFC 6122 |
| June 2015 | RFC 7590: stronger TLS recommendations for XMPP | RFC 7590 |
| September 2015 | RFC 7622 obsoletes RFC 6122 (PRECIS based JIDs) | RFC 7622 |
| October 2015 | First OMEMO draft (XEP-0384 version 0.0.1) | XEP-0384 revision history |
| December 2016 | XEP-0384 version 0.1 accepted by the XMPP Council | XEP-0384 revision history |
| 2019 | XEP-0368 direct TLS SRV records reaches version 1.1.0, Stable | XEP-0368 |
| March 2025 | Prosody 13.0.0 adds RFC 9266 tls-exporter channel binding and full DANE for s2s | Prosody release notes |
| April 2026 | XEP-0384 version 0.9.1, still Experimental | XEP-0384 |
Who maintains XMPP today
The IETF owns the core RFCs. Everything built on top is published by the XMPP Standards Foundation as numbered XEPs. Per XEP-0001 and the XSF standards process page, anyone can submit a ProtoXEP; the XMPP Council, a technical committee elected by XSF members, decides whether to accept it. Accepted documents move through Experimental, Proposed, Stable and Final, or end up Deferred, Rejected, Deprecated, Obsolete or Retracted.
Those labels matter. For an Experimental XEP, the XSF advises production systems to consider carefully before deploying, and OMEMO (XEP-0384) is still Experimental. Message Archive Management (XEP-0313), Stream Management (XEP-0198) and Multi-User Chat (XEP-0045) are Stable; In-Band Registration (XEP-0077) is Final.
How streams, stanzas and JIDs work
Addresses: JIDs
Every XMPP entity has a Jabber ID, or JID. RFC 7622 defines it as up to three parts:
localpart@domainpart/resourcepart
juliet@example.com/balcony
- The domainpart is the server, for example
example.com. It is the only required part, and it is how federation finds the right server. - The localpart is the account, like the user in an email address. RFC 7622 limits each part to 1,023 bytes and maps the localpart through the PRECIS UsernameCaseMapped profile, so different Unicode spellings of the same name are compared consistently.
- The resourcepart names a specific connected session, typically one per device or client. A "bare JID" (
juliet@example.com) is the account; a "full JID" (juliet@example.com/balcony) is one session of it.
RFC 7622 has a security section on address spoofing for a reason: lookalike Unicode characters in a domain or localpart can fool people. Clients should display JIDs carefully, and admins should think about which characters they allow in new account names.
Streams
An XMPP session is two XML streams, one in each direction, over one TCP connection. The client opens with a stream:stream element, the server answers with its own opening tag and a list of stream features (for example STARTTLS and the SASL mechanisms on offer). The stream stays open for as long as the session lives. Everything that follows is a child element of that root.
Because the stream is one huge XML document, RFC 6120 restricts the XML you may send: comments, processing instructions, DTD subsets and entity references are banned, and a server that sees them closes the stream with a restricted-xml error. Unrestricted XML parsing is a well known source of entity expansion and denial of service bugs, so strict enforcement matters.
Stanzas
RFC 6120 defines exactly three kinds of stanza:
| Stanza | What it does | Example use |
|---|---|---|
message | Push a payload to an entity, fire and forget | Chat message, group chat line, notification |
presence | Broadcast availability, and manage subscriptions | Online, away, "may I see your status?" |
iq (info/query) | Request and response, with type get, set, result or error | Fetch roster, upload slot request, discovery |
RFC 6121 builds instant messaging on top: the roster (contact list stored on the server), presence subscriptions (a contact must approve before you see their status), message types such as chat and groupchat, and the server's routing rules for bare versus full JIDs.
Extensions add new child elements in their own XML namespace inside these three stanza types. An OMEMO encrypted message is still a message stanza; the ciphertext sits in an encrypted element in the urn:xmpp:omemo:2 namespace. This extensibility is why XMPP has lasted 27 years, and why two clients can both "support XMPP" yet disagree on encryption or file transfer.
How connections authenticate
There are two kinds of connection: client to server (c2s) and server to server (s2s). Both follow the same pattern: find the server, set up TLS, then authenticate.
Step 1: find the server with DNS
RFC 6120 resolves a domain through DNS SRV records. A client connecting for example.net looks up _xmpp-client._tcp.example.net; a server opening a federation link looks up _xmpp-server._tcp.example.net. If no SRV record exists, the fallback is a plain A or AAAA lookup on the IANA registered ports: 5222 for clients, 5269 for servers.
XEP-0368 (Stable, version 1.1.0) adds _xmpps-client._tcp and _xmpps-server._tcp records for direct TLS, where TLS starts immediately instead of being upgraded with STARTTLS. Its rules: STARTTLS must not be used inside a direct TLS connection, SNI must be set to the JID's domain, and if ALPN is used the protocol names are xmpp-client and xmpp-server. The spec notes two practical gains: direct TLS saves round trips, and on port 443 without ALPN it is indistinguishable from HTTPS, which helps clients behind restrictive firewalls.
_xmpp-client._tcp.example.com. 3600 IN SRV 0 5 5222 xmpp.example.com.
_xmpps-client._tcp.example.com. 3600 IN SRV 0 5 5223 xmpp.example.com.
_xmpp-server._tcp.example.com. 3600 IN SRV 0 5 5269 xmpp.example.com.
XEP-0156 also lets web clients discover BOSH and WebSocket endpoints over HTTPS, which is how browser based clients connect without raw TCP.
Step 2: TLS
With STARTTLS, the receiving server lists starttls in its stream features, the client asks to upgrade, both sides run the TLS handshake on the same TCP connection, and then restart the XML stream from scratch. RFC 6120 says no further XML may be sent until TLS completes, and old stream state is discarded, which closes the door on injecting data before the upgrade.
The weak point of STARTTLS is the moment before TLS starts. An attacker in the path can strip the starttls feature from the server's advertisement. The defence is policy: clients and servers that refuse to continue without TLS. That is what RFC 7590 asks for. Published in June 2015, it applies the general TLS best practices of BCP 195 to XMPP, covering protocol versions, fallback, TLS layer compression, session resumption, cipher suites, key lengths and forward secrecy, and it requires that implementations support and prefer the strongest options. Since RFC 7590 was written, BCP 195 itself has been updated: RFC 9325 replaced RFC 7525 in November 2022, and TLS 1.3 (RFC 8446) is now the baseline most servers should prefer.
Certificate checking matters as much as encryption. RFC 6120 originally pointed to RFC 6125 for how to match a certificate against the service name; RFC 9525 (November 2023) obsoletes RFC 6125, and Prosody's 13.0.0 release notes say it no longer checks certificate Common Names, in line with RFC 9525. In practice your certificate needs subject alternative names covering the XMPP domain itself (example.com), not only the host the SRV record points to, plus any component domains such as conference.example.com and upload.example.com. Prosody's certificate documentation is blunt about this: the certificate is for the domain users have in their JIDs.
Step 3: SASL login
After TLS, the client authenticates with SASL. The server lists mechanisms, for example:
<mechanisms xmlns='urn:ietf:params:xml:ns:xmpp-sasl'>
<mechanism>SCRAM-SHA-1-PLUS</mechanism>
<mechanism>SCRAM-SHA-1</mechanism>
<mechanism>PLAIN</mechanism>
</mechanisms>
What each mechanism means for security:
| Mechanism | How the password is used | Risk if TLS is broken or stripped | Notes |
|---|---|---|---|
| PLAIN | Sent as-is, base64 encoded | Password exposed | Acceptable only inside verified TLS |
| DIGEST-MD5 | Challenge-response | Weak, deprecated | RFC 6120 replaced it as mandatory-to-implement with SCRAM |
| SCRAM-SHA-1 / SCRAM-SHA-256 | Salted challenge-response (RFC 5802, RFC 7677) | Password not sent; server stores salted verifiers | Mutual proof: the server proves it knows the verifier too |
| SCRAM-...-PLUS | SCRAM plus TLS channel binding | Detects a man in the middle terminating TLS | Needs tls-unique (TLS 1.2) or tls-exporter (TLS 1.3, RFC 9266) |
| EXTERNAL | Identity from the TLS client certificate | No password at all | Common for s2s; see XEP-0178 for certificate rules |
Channel binding is the piece people skip. With plain SCRAM, an attacker who can get a client to accept a bad certificate can relay the SCRAM exchange. With a -PLUS variant, the SCRAM proof is tied to the specific TLS connection, so a relayed exchange fails. RFC 6120 already says implementations should enforce channel binding even when TLS is used. RFC 9266 defines tls-exporter for TLS 1.3, because the older tls-unique binding does not exist in TLS 1.3. XEP-0440 (Stable since December 2025) lets a server advertise which channel binding types it supports, and Prosody 13.0.0 implements both tls-exporter and tls-server-end-point.
Two newer XEPs build on this: XEP-0388 (SASL2, Stable) streamlines login, and XEP-0474 (Experimental) protects the offered mechanism list so a client can detect an attacker who removed the strong options. Neither is universal yet.
On the server side, the way passwords are stored is the other half. SCRAM lets the server keep salted verifiers instead of plaintext passwords. Prosody's security page recommends hashed storage with mod_auth_internal_hashed and notes the trade-off: some legacy mechanisms need the plaintext password, so hashed storage limits which mechanisms work.
Step 4: server to server trust
Federation means your server accepts connections from servers you have never heard of. Each one claims to speak for a domain. There are two ways to check the claim.
Certificates. The remote server presents a certificate for its domain during TLS, and your server verifies it against trusted CAs, then authenticates the stream with SASL EXTERNAL. XEP-0178 gives the best practices. This is strong authentication, and with DANE (TLSA records in DNSSEC signed zones) it does not even depend on the public CA system. Prosody 13.0.0 lists full DANE support for s2s.
Server Dialback. XEP-0220 defines the older fallback. When server A connects to server B claiming to be a.example, B does not accept stanzas yet. It opens a separate connection back to the authoritative server for a.example, found through DNS, and asks it to confirm a key. If it confirms, B accepts the stream. The XSF's own abstract is candid: dialback uses DNS as the basis of identity, and without DNSSEC the result is weak identity verification. RFC 6120 noted in 2011 that most deployed servers still used dialback; today the better servers try certificates first and only fall back to dialback where allowed.
Prosody exposes this choice directly. s2s_require_encryption refuses unencrypted federation links (Prosody's documentation says there should be no need to allow them anymore). s2s_secure_auth = true requires valid certificates and never falls back to DNS based dialback. If a few domains you need to talk to have broken certificates, s2s_insecure_domains lets you make named exceptions rather than weakening the whole policy. s2s_secure_domains does the opposite: require certificate authentication for specific domains while the default stays relaxed.
Connection security at a glance
| Layer | Standard | What it protects | What it does not |
|---|---|---|---|
| DNS SRV | RFC 6120, XEP-0368 | Finding the right host and port | Spoofed answers, unless DNSSEC |
| TLS (c2s, s2s) | RFC 6120, RFC 7590 | Each hop between client and server and between servers | Content on the servers themselves |
| SASL SCRAM -PLUS | RFC 5802, RFC 7677, RFC 9266 | Password and TLS relay attacks | A compromised server |
| s2s certificates or DANE | XEP-0178, RFC 9525 | Server identity in federation | What the remote server does with your data |
| Dialback | XEP-0220 | Basic domain identity | Strong identity without DNSSEC |
| OMEMO | XEP-0384 | Message content end to end, per device | Metadata, contact lists, presence |
How OMEMO end to end encryption works
TLS protects each hop. A message from alice@a.example to bob@b.example crosses Alice's client, server A, server B and Bob's client. Both servers decrypt the TLS layer, see the stanza and could store it. XEP-0384 says this is exactly the gap it closes: because of XMPP's federated nature a message may pass more than one server, so users have an interest in protecting content from every intermediate host.
The building blocks
The current XEP-0384 (version 0.9.1, 6 April 2026) defines:
- Devices. Each client instance is a device with its own numeric device ID and its own long term IdentityKey. An account with a phone and a laptop has two devices, and senders encrypt for each one.
- Device list. Each account publishes its device IDs to a PEP node (Personal Eventing Protocol, a per-account publish-subscribe service from XEP-0163 and XEP-0060) at
urn:xmpp:omemo:2:devices. Contacts subscribe, so they learn about new devices automatically. - Bundles. Each device publishes a bundle: its public IdentityKey, a signed PreKey with its signature, and a list of single use PreKeys. The spec says the bundle should hold around 100 PreKeys and must always hold at least 25, and that signed PreKeys should rotate between once a week and once a month.
- X3DH. To start a session, the sender fetches a bundle and runs the X3DH key agreement (curve Curve25519/Ed25519, SHA-256, info string "OMEMO X3DH"). Every key exchange must use a single use PreKey; exchanges without one must be rejected.
- Double Ratchet. After X3DH, messages use the Double Ratchet. XEP-0384 fixes the parameters: HKDF-SHA-256 for the root chain, HMAC-SHA-256 for chain keys, and for each message an authenticated encryption step of AES-256-CBC with HMAC-SHA-256, keyed from 80 bytes of HKDF output. It recommends keeping at most 1,000 skipped message keys per session, so a malicious sender cannot fill a device's storage.
- Stanza Content Encryption. In the current namespace, the encrypted payload is an XEP-0420
envelopecontaining the elements meant only for the recipient, plus random padding, so more than just the message body can be protected.
What one encrypted message looks like
The sender generates a random key, encrypts the payload once with it, then encrypts that key separately for every recipient device and every one of its own other devices, each through that device's ratchet session. The stanza carries one ciphertext and many small key blobs, one per device, each tagged with the recipient device ID (rid). The kex flag marks a key blob that also carries a new X3DH key exchange.
That design explains what users see. A device that was offline can still decrypt a message later from the server archive, because its key blob was included at send time. A device added after a message was sent cannot decrypt that message at all.
Group chats
OMEMO works in Multi-User Chat rooms with conditions. XEP-0384 says a room that supports OMEMO must be non-anonymous (so participants can see each other's real JIDs and fetch device lists) and should be members-only. The sender fetches the member list, then each member's device list, then encrypts the message key for every device of every member. A large room means a large header and a lot of sessions, which is one reason big public XMPP rooms are rarely end to end encrypted.
Trust and verification
XEP-0384 states that trust management is out of scope, and that the quality of identity key verification determines the protection OMEMO offers. In practice, clients show each device's fingerprint and let you mark it verified, usually by scanning a QR code in person. Many clients use a "trust on first use" or "blind trust before verification" default: new devices are trusted until you verify one, after which unverified new devices are flagged. If you never verify, a malicious or compromised server could publish an extra device for a contact and your client might encrypt to it. That is the main active attack OMEMO cannot stop without user verification.
What servers still see
The spec is explicit: OMEMO does not protect against attackers who rely on metadata and traffic analysis. Here is what stays visible to your server, and to the remote server for cross-domain chats:
| Visible to servers | Why |
|---|---|
| Sender and recipient JIDs, timestamps, message sizes | Needed for routing |
| Your roster (contact list) and subscription states | Stored server side by RFC 6121 |
| Presence: when you are online, status text | Broadcast through the server |
| Device IDs and public bundles | Published in PEP so others can encrypt |
| Group membership and room names | MUC is server side |
| Encrypted payloads in the archive | MAM stores them, encrypted, until expiry |
| File upload URLs, sizes and upload times | HTTP upload (XEP-0363) is a server component; clients that implement OMEMO media sharing (XEP-0454, Experimental) encrypt the file, but the server still stores it |
| Read receipts, typing notifications if not inside the envelope | Depends on client and on SCE support |
Older OMEMO clients encrypt only the body element, leaving other extension elements in the clear. The XEP-0420 envelope in the current namespace fixes that, but only between clients that speak it.
The two OMEMO namespaces
This is the interoperability trap. XEP-0384's revision history shows that version 0.2 (2017) switched to the eu.siacs.conversations.axolotl namespace "currently used in the wild", and later versions moved to urn:xmpp:omemo:1 and then urn:xmpp:omemo:2 with a reworked wire format. Many deployed clients still implement the older axolotl based version, sometimes called OMEMO 0.3, and a growing number support the newer one, some both. Two clients that both advertise "OMEMO" may not be able to talk end to end. Test with the exact clients your users run, and check the XSF's software listing and each client's own documentation for which version they implement.
How federation affects spam, abuse and trust
Federation makes XMPP independent of any single company, and it is also why spam is the most common operational problem: a spammer needs only an account on any server that federates with yours.
The usual source is open registration. XEP-0077 In-Band Registration lets a client create an account over XMPP itself, with no web form. If a public server leaves it open with no rate limits, bots create accounts in bulk and send unsolicited subscription requests and messages across the whole network. Prosody's mod_register documentation puts it plainly: if you allow open registration, be sure you can handle spam accounts and abuse, or disable federation with mod_s2s.
Controls that operators actually use:
- Invite-only registration. Prosody's
mod_invitesfamily lets admins or users create single use invitation links (prosodyctl shell invite create_account example.com). XEP-0401 (Experimental) standardises generating invitations from a client. - Registration throttling. Prosody's
mod_register_limitssupportsmin_seconds_between_registrations,registration_throttle_max,registration_throttle_period, an IP blocklist and an allowlist. - CAPTCHA forms. XEP-0158 defines CAPTCHA challenges inside XMPP data forms, for servers that must keep public sign-up.
- Bandwidth limits. Prosody's
mod_limitsrate limits c2s, inbound s2s, outbound s2s and component connections, in bytes per second with a burst window. - User blocking and reporting. XEP-0191 lets users block JIDs, enforced by the server. XEP-0377 (Proposed) adds a reason such as spam or abuse to a block, so the user's server can learn from it.
- Server level blocking. Admins can refuse federation with specific domains. Community maintained lists of domains and JIDs known for spam exist; treat any list as input, not as an authority.
- Real-time block lists. ejabberd's documentation describes
mod_antispam, added in release 25.07, which filters spam messages and subscription requests from remote servers using real-time block lists of known spammer JIDs and URLs, andmod_muc_rtbl, which applies a shared ban list to group chat rooms through publish-subscribe. - Strict s2s authentication. Requiring certificates makes it harder to impersonate a domain, though a spammer with a real domain and a free certificate still gets through. Authentication proves identity, not good behaviour.
Trust in a federated network is therefore layered. TLS and certificates tell you a connection really comes from spam.example; they say nothing about whether spam.example is honest. That judgement is made by admins and users through blocking, the same way email operators handle bad senders. Our self-hosted mail server guide covers the email equivalent, and the parallels are close.
XMPP compared with Matrix, centralised Signal-protocol apps and email
| XMPP | Matrix | Centralised apps on the Signal protocol | ||
|---|---|---|---|---|
| Federation | Yes, server to server over TLS | Yes, homeservers replicate room state | No, one operator runs the service | Yes, SMTP between MTAs |
| Standards body | IETF core, XSF extensions | Matrix.org Foundation spec | Operator's own protocol docs | IETF |
| E2EE by default | No; OMEMO is per client, often opt-in or per contact | Depends on client and room settings | Yes, typically always on | No; S/MIME or OpenPGP are add-ons |
| E2EE design | OMEMO: X3DH plus Double Ratchet per device | Olm and Megolm (Double Ratchet derived) | X3DH or successors plus Double Ratchet | Per-message public key encryption |
| Metadata visible to servers | Roster, presence, routing, devices, group membership | Room membership and event metadata replicated to every participating homeserver | Operator dependent; designs vary in how much they keep | Headers, sender, recipients, subjects |
| Server history | MAM archive, operator sets retention | Full room history replicated across homeservers | Usually minimal on server | Mailboxes kept indefinitely by default |
| Self-hosting effort | Low to moderate: one daemon, small footprint | Moderate to high: database, media store, more RAM | Not possible for the official service | High, mostly because of deliverability |
| Biggest operator risk | Open registration and spam; outdated modules | Federation load, media storage, room state | Not applicable | Spam, blocklisting, open relays |
In short, XMPP is the lightest to self-host and has the longest standards history, but its encryption depends on client choice. Matrix is heavier and replicates more data across servers; our Matrix guide covers its encryption defaults. Centralised apps make strong encryption the default at the cost of having one operator. Email remains federated and unencrypted by default. Our end to end encryption primer gives the general model if you are new to the topic.
How bridges and gateways change the security model
A bridge or gateway translates between XMPP and another network: IRC, Matrix, SMS, or a proprietary chat service. XEP-0100 describes the classic gateway pattern, in which a gateway is an XMPP component with its own domain that users register with, often handing over their credentials for the other network.
Bridges break end to end encryption by design. To translate a message into another protocol, the bridge must see the plaintext. If you send an OMEMO message to a contact who is really a bridged user, either your client refuses (the bridge has no OMEMO device) or you fall back to unencrypted delivery to the bridge. Some bridges hold an OMEMO identity themselves and decrypt on arrival, which means the bridge operator can read everything.
Things to check before running or trusting a bridge:
- Who operates it. A public bridge run by a stranger sees your messages and often your credentials for the other network.
- How it authenticates to your server. XMPP components connect with a shared secret over XEP-0114 (Active). Prosody listens for components on port 5347, documented as private. Never expose that port publicly, and use a long random secret.
- Its dependencies. Bridges are often small projects in Python, Go or Node.js with many third-party libraries. They are the part of an XMPP stack most likely to have an outdated dependency.
- Encryption expectations. Tell users plainly that messages to bridged contacts are not end to end encrypted.
Step by step: run a hardened XMPP server
This walkthrough uses Prosody, a lightweight open source XMPP server written in Lua (MIT licence), because its documentation covers each control clearly. The Prosody download page lists 13.0.7 as the latest version at the time of writing. The ejabberd server (Erlang, GPLv2) has equivalent settings; see its configuration documentation for the matching option names. The example domain is example.com with the server on host xmpp.example.com.
Replace every domain, path and address with your own. Commands assume a Debian or Ubuntu host with root access.
1. Install from a current source
# Debian/Ubuntu: distribution package (may lag behind upstream)
sudo apt update
sudo apt install prosody
# Check what you got
prosodyctl about
Prosody's download page recommends adding the project's own package repository because distributions may lag behind the latest release. Record the version from prosodyctl about in your inventory for the next security release.
2. Publish DNS records
Create the SRV records from earlier, plus A and AAAA records for xmpp.example.com and for any component domains (conference.example.com, upload.example.com). Then check:
dig +short SRV _xmpp-client._tcp.example.com
dig +short SRV _xmpps-client._tcp.example.com
dig +short SRV _xmpp-server._tcp.example.com
If your zone is signed with DNSSEC, consider publishing TLSA records for DANE so other servers can verify your certificate without depending only on public CAs. Our DNSSEC explainer covers what that does and does not protect.
3. Open only the ports you use
Prosody's port documentation lists 5222 (clients), 5269 (servers) and 5281 (HTTPS) as public, and 5280 (HTTP), 5347 (external components) and 5582 (telnet console) as private. Direct TLS commonly uses 5223.
sudo ufw allow 5222/tcp # client STARTTLS
sudo ufw allow 5223/tcp # client direct TLS (XEP-0368)
sudo ufw allow 5269/tcp # federation
sudo ufw allow 5281/tcp # HTTPS: file upload, BOSH, WebSocket
# Do NOT open 5280, 5347 or 5582 to the internet
4. Get certificates and keep them renewed
Use a certificate from a recognised CA covering example.com and every component domain. With an ACME client such as certbot, Prosody's Let's Encrypt page recommends importing certificates with prosodyctl rather than loosening file permissions, and automating it with a deploy hook:
sudo certbot certonly --standalone \
-d example.com -d xmpp.example.com \
-d conference.example.com -d upload.example.com
# Deploy hook: re-import after every renewal
sudo tee /etc/letsencrypt/renewal-hooks/deploy/prosody.sh >/dev/null <<'EOF'
#!/bin/sh
/usr/bin/prosodyctl --root cert import /etc/letsencrypt/live
EOF
sudo chmod +x /etc/letsencrypt/renewal-hooks/deploy/prosody.sh
sudo /etc/letsencrypt/renewal-hooks/deploy/prosody.sh
prosodyctl check certs
Standalone mode needs port 80 free; our Let's Encrypt automation guide covers webroot and DNS alternatives.
5. Require encryption and strong authentication
In /etc/prosody/prosody.cfg.lua, global section:
-- Transport security
c2s_require_encryption = true
s2s_require_encryption = true
s2s_secure_auth = true -- certificates only, no dialback fallback
-- s2s_insecure_domains = { "legacy.example.org" } -- named exceptions only
-- Never accept a plaintext password over an unencrypted stream
allow_unencrypted_plain_auth = false
-- Store salted SCRAM verifiers, not plaintext passwords
authentication = "internal_hashed"
Setting s2s_secure_auth = true cuts you off from servers with broken certificates. Watch s2s failures in your logs for a week and add narrow s2s_insecure_domains exceptions only where you must.
6. Close open registration and add limits
-- No public sign-up
allow_registration = false
modules_enabled = {
-- core
"roster"; "saslauth"; "tls"; "disco"; "pep"; "carbons";
"blocklist"; "mam"; "smacks"; "limits";
-- invite-only accounts
"register"; "invites"; "invites_adhoc"; "invites_register";
}
-- Bandwidth limits per connection type (bytes, not bits)
limits = {
c2s = { rate = "10kb/s"; burst = "2s"; };
s2sin = { rate = "30kb/s"; burst = "5s"; };
}
Then create accounts by invitation:
sudo prosodyctl shell invite create_account example.com
If you must allow public sign-up, keep allow_registration = true but add mod_register_limits with min_seconds_between_registrations and a CAPTCHA, and watch new account volume daily. The rate values above are a starting point, not a recommendation from Prosody; tune them against your own traffic.
7. Set retention for archives and uploads
-- Message Archive Management (XEP-0313)
archive_expires_after = "1w" -- Prosody's default; choose your own policy
default_archive_policy = true
VirtualHost "example.com"
Component "conference.example.com" "muc"
modules_enabled = { "muc_mam" }
Component "upload.example.com" "http_file_share"
http_file_share_expires_after = "1w"
http_file_share_size_limit = 16 * 1024 * 1024 -- 16 MiB
http_file_share_daily_quota = 100 * 1024 * 1024 -- per user per day
Prosody's documentation says archived messages are kept for one week by default and that uploaded files match that retention. Setting archive_expires_after = "never" keeps everything forever, which is a privacy and storage decision you should make deliberately. OMEMO encrypted messages are stored as ciphertext, but routing metadata in the archive is not.
8. Check, restart and test from outside
sudo prosodyctl check config
sudo prosodyctl check dns
sudo prosodyctl check certs
sudo prosodyctl check features # added in Prosody 13.0.0
sudo systemctl restart prosody
# Confirm STARTTLS and the certificate from another machine
openssl s_client -connect xmpp.example.com:5222 \
-starttls xmpp -xmpphost example.com -brief </dev/null
# Confirm direct TLS with ALPN
openssl s_client -connect xmpp.example.com:5223 \
-servername example.com -alpn xmpp-client -brief </dev/null
Look for TLS 1.3 (or at least 1.2), the right certificate chain and no plaintext fallback, then confirm a test client is offered SCRAM, ideally a -PLUS variant.
9. Turn on OMEMO in clients and verify devices
OMEMO needs no special server module beyond PEP (mod_pep, which the module list above includes), because device lists and bundles are published there. Clients decide whether to use it. In each client:
- Confirm OMEMO is enabled by default for new conversations, or turn it on per contact.
- Compare device fingerprints with each contact in person or over a channel you already trust, and mark them verified.
- Turn on the option to warn or refuse when a contact gains an unverified device, if your client has one.
- Check that your clients share an OMEMO version by sending a test message between every client type your users run.
For group chats, configure rooms as non-anonymous and members-only, as XEP-0384 requires.
10. Back up and test a restore
# Check where Prosody keeps data before you copy anything
sudo prosodyctl about | grep -i data
sudo systemctl stop prosody
sudo tar czf /root/prosody-backup-$(date +%F).tar.gz \
/etc/prosody /var/lib/prosody
sudo systemctl start prosody
The default data path on Debian packages is /var/lib/prosody, but confirm yours from the config or the about output first. The backup contains account verifiers, rosters, archives and, if certificates are imported, private keys, so encrypt it and store it off the host. A backup does not include anyone's OMEMO private keys; those live only on devices. Test restoring onto a spare machine at least once.
ejabberd equivalents
ejabberd (Erlang, GPLv2) covers the same ground in YAML. Option names below come from the ejabberd configuration documentation; check them against the release you install.
# /etc/ejabberd/ejabberd.yml (excerpt)
listen:
-
port: 5222
module: ejabberd_c2s
starttls_required: true # refuse clients that will not upgrade
s2s_use_starttls: required # documented default is false
registration_timeout: 600 # seconds between sign-ups per IP or JID
modules:
mod_register:
access: register # who may register at all
ip_access: trusted_networks # an ip type access rule
captcha_protected: true
password_strength: 32 # bits; the documented recommended minimum
mod_mam: {}
mod_http_upload: {}
mod_http_upload_quota: {}
Two details stand out in ejabberd's documentation. First, s2s_use_starttls defaults to false "for historical reasons", which means STARTTLS is prohibited on federation links until you change it; required allows only STARTTLS connections. Second, the security guide lists port 4369 (epmd) and the Erlang distribution port (5210 by default, set with ERL_DIST_PORT) and names /var/lib/ejabberd/.erlang.cookie as a sensitive file. The Erlang cookie is effectively a cluster password: anyone who can reach the distribution port with it can control the node. Firewall both ports from the internet and protect the whole /var/lib/ejabberd/ directory, as the guide advises.
Common mistakes
- Leaving registration open on a federated server. This is how most XMPP spam starts. Use invites, or throttle and CAPTCHA if you must stay public.
- No rate limits. Without
mod_limitsor equivalent, one abusive client or remote server can flood yours. - Allowing plaintext or legacy auth.
c2s_require_encryption = falseor allowing PLAIN over unencrypted streams undoes everything else. - Accepting dialback everywhere. Without DNSSEC, dialback is weak identity verification by the XSF's own description. Prefer
s2s_secure_auth = truewith narrow exceptions. - Certificates that do not cover the JID domain. A certificate for only
xmpp.example.comfails for users whose JIDs are@example.com. - Exposing component, console and cluster ports. Prosody's 5347 and 5582 are private by design, and so are ejabberd's epmd (4369) and Erlang distribution ports.
- Keeping old federation defaults. ejabberd's
s2s_use_starttlsdefaults tofalse; leaving it there means federation links run without TLS. - Outdated server modules. Community modules are maintained at different speeds from the core. Track both, and read release notes: Prosody 13.0.0, for example, shipped a known issue with manual TLS settings for direct TLS ports, which its release notes document.
- Assuming E2EE is on by default. XMPP does not encrypt end to end unless both clients enable OMEMO and share a version.
- Never verifying fingerprints. Blind trust is convenient, but it means a rogue device would be trusted too.
- Ignoring log and archive retention. Server logs can include JIDs and IP addresses; archives keep metadata even for encrypted messages. Decide how long you keep both.
- Trusting a public bridge with private conversations. Bridges see plaintext.
FAQ
Is XMPP the same as Jabber?
Yes, historically. Jabber was the original 1999 name of the protocol and the open source community around it. When the IETF standardised it in 2004, the protocol was published as XMPP, and that is the name in every RFC and XEP since. "Jabber" is still used informally, for example in older server names and addresses.
Is XMPP end to end encrypted?
Not by default. Every connection should be TLS encrypted, but the servers see message content unless both clients use OMEMO (or another end to end scheme such as OpenPGP for XMPP). Some clients switch OMEMO on automatically when the contact supports it; others need it turned on per conversation. Check the setting in your own client rather than assuming.
Is OMEMO the same as the Signal protocol?
It reuses the same building blocks, X3DH key agreement and the Double Ratchet, with its own parameters, wire format and device discovery through PEP. Earlier drafts of XEP-0384 depended directly on the Signal protocol library; current versions specify everything in the XEP.
What ports does XMPP use?
5222 for clients with STARTTLS, 5269 for server to server, and commonly 5223 for direct TLS. Servers often run HTTPS on 5281 or 443 for file upload, BOSH and WebSocket. Component and admin ports should stay private.
What is the difference between STARTTLS and direct TLS in XMPP?
STARTTLS opens a plain connection and upgrades it to TLS, so a client must refuse to continue if the upgrade is not offered. Direct TLS (XEP-0368) starts TLS immediately, removing the downgrade window and saving round trips. Support both, and require encryption on both.
Can my XMPP server read my messages?
Without OMEMO, yes: it sees and can archive the plaintext. With OMEMO it stores ciphertext, but it still sees who you talk to, when, your contact list, your presence and your group memberships.
Is self-hosting XMPP safer than using a public server?
It moves trust from someone else's admin to you. You control registration, retention and logs, which helps with metadata. You also take on patching, backups and abuse handling. A well run public server can be safer than a neglected private one.
How do I stop spam on my XMPP server?
Disable open registration or use invites, add registration throttling and bandwidth limits, enable user blocking, and block federating domains that send abuse. If users must be able to sign up publicly, add a CAPTCHA.
Does XMPP support group chats with encryption?
Yes, through Multi-User Chat with OMEMO, if the room is non-anonymous and ideally members-only, and every participant's client supports a compatible OMEMO version. Large public rooms are usually not encrypted end to end.
Why software supply chain hygiene still matters here
An XMPP server is a long-running internet facing daemon built from open source parts: the server itself, its TLS library, an XML parser, a database driver and, often, community modules, bridges and bots in other languages. Your protocol settings can be perfect and still be undone by an outdated library underneath, so knowing exactly which versions you run, and closing the gap when a fix exists, is part of running XMPP safely.
Fix it without waiting for upstream
Whatever the vulnerability, most of the risk sits in two gaps. The first is the time between a fix being published and that fix actually running in your production builds. The second, and worse, is the time before any fix exists at all, while a maintainer works out a patch and a release schedule. Waiting through both gaps is a habit, not a requirement. Safeguard gives teams three ways to close them.
1. Autonomous remediation: pull requests, not tickets
When a fixed version exists, Safeguard's Griffin models open a tested, policy gated pull request that moves the affected dependency to the safe version. You can run it on one repository or in bulk across every project that carries the vulnerable version. Remediation categories include dependencies, licenses, security posture and findings, and each one is set separately. Reachability analysis ranks the vulnerable code paths your application actually calls first, so the first pull requests you merge are the ones that matter most. Autonomous remediation ships switched off, with every category set to Manual, so your team decides how much runs on its own and what still needs a human approval.
2. Gold Open Source: the safe version in seconds, free
gold.safeguard.sh is free and needs no login. Search any CVE or package to see affected and fixed versions, CVSS, EPSS, CISA KEV status and a recommended safe version, or use the "Am I affected?" checker. The same data is available through a free read API, README status badges and RSS threat feeds.
3. Gold Registry: start from components that are already fixed
Gold Registry provides hardened, drop in replacements for upstream packages and container images. Every Gold artifact is held continuously to zero exploitable critical or high CVEs and a completed license check, with signed build provenance (signed with Safeguard's key and verifiable with cosign, SLSA Build Level 3 as the design target) and a public evidence page you can hand to an auditor. An artifact loses its Gold badge if it regresses. Teams can point their own package directory at the registry so hardened equivalents resolve without changing how developers install anything.
When upstream has no fix yet, or will not ship one, Safeguard first works to get the fix accepted upstream. If the issue is severe or upstream will not move, Safeguard builds a fork, fixes the vulnerability there and publishes a hardened Gold build, so you are not stuck on a maintainer's release schedule. You can also request a hardened, compatibility tested build of a specific package. Ask which packages it covers before you plan around it.
| Your situation | What to use |
|---|---|
| A fixed version exists | Autonomous remediation opens the upgrade pull request; Gold Open Source confirms the safe version |
| You need to know today whether you are affected | Gold Open Source lookup and "Am I affected?" checker, free and without login |
| You want vulnerable versions to stop shipping | Safeguard policies and gates in CI |
| Upstream has no fix yet, or will not ship one | A custom Gold request; fork and patch |
| You want to start clean on new projects | Pull hardened components and images from Gold Registry |
One honest limit: when the vulnerable code lives in a vendor appliance, a hosted service or an operating system runtime you do not build yourself, the fix still has to come from that vendor or distribution. The steps above apply to the open source dependencies and container images you build and ship, which is where most teams carry the largest and least tracked share of their exposure.
See how Safeguard would help
Safeguard does not configure XMPP servers, check OMEMO fingerprints or filter spam. Where it fits is the software around them: the container image you run Prosody or ejabberd in, the repository for your bridge or bot, and the SBOM of what you deploy. Gold Open Source at gold.safeguard.sh gives free CVE intelligence with affected and fixed versions for the libraries in that stack.
The first demo is a real Safeguard CLI session. It asks the assistant to fix a vulnerable lodash version in a repository, and the CLI asks before any command that changes something, then edits the manifest and runs the tests. The same flow applies to a Node.js or Python XMPP bridge or bot repository, where outdated dependencies are most likely to hide.
Click through it at your own pace, or open it full screen.
The second demo sets up the local runner on your own machine, approves it with a device code and scans from the terminal. That is how you would scan a server image (safeguard scan --image <name>) or a deployment repository without sending the code anywhere else first.
Click through it at your own pace, or open it full screen.
The third demo connects Claude to the Safeguard MCP server with OAuth and chooses which tools Claude can use. Once connected, you can ask which of your projects carry a given library version, such as a TLS or XML parsing library your XMPP stack depends on.
Click through it at your own pace, or open it full screen.
To try it on your own stack, download Safeguard or read the CLI documentation.
Quick check
Why is Server Dialback considered weak, and what should you prefer?
XEP-0220 verifies a remote server's identity through DNS, so without DNSSEC the result is weak identity verification. Prefer certificate authentication with SASL EXTERNAL (or DANE), for example with Prosody's s2s_secure_auth = true, and allow dialback only for named exceptions.
If two friends both use OMEMO, what can their servers still see?
Sender and recipient addresses, timestamps and sizes, both contact lists, presence, device IDs and public key bundles, group memberships, and file upload metadata. OMEMO protects content, not metadata.
Sources
- RFC 6120: XMPP Core
- RFC 6121: XMPP Instant Messaging and Presence
- RFC 7622: XMPP Address Format
- RFC 6122: XMPP Address Format (obsoleted)
- RFC 3920: XMPP Core (2004, obsoleted)
- RFC 3921: XMPP IM and Presence (2004, obsoleted)
- RFC 7590: Use of TLS in XMPP
- RFC 8446: TLS 1.3
- RFC 9325: Recommendations for Secure Use of TLS and DTLS (BCP 195)
- RFC 9525: Service Identity in TLS
- RFC 6125: Service Identity (obsoleted)
- RFC 5802: SCRAM
- RFC 7677: SCRAM-SHA-256
- RFC 9266: Channel Bindings for TLS 1.3
- XSF: History of XMPP
- XSF: Standards process
- XEP-0001: XMPP Extension Protocols
- XEP-0384: OMEMO Encryption
- XEP-0163: Personal Eventing Protocol
- XEP-0420: Stanza Content Encryption
- XEP-0454: OMEMO Media Sharing
- XEP-0363: HTTP File Upload
- XEP-0191: Blocking Command
- XEP-0100: Gateway Interaction
- XEP-0114: Jabber Component Protocol
- XEP-0368: SRV records for XMPP over TLS
- XEP-0220: Server Dialback
- XEP-0178: SASL EXTERNAL with Certificates
- XEP-0156: Discovering Alternative XMPP Connection Methods
- XEP-0388: Extensible SASL Profile
- XEP-0440: SASL Channel-Binding Type Capability
- XEP-0474: SASL SCRAM Downgrade Protection
- XEP-0077: In-Band Registration
- XEP-0158: CAPTCHA Forms
- XEP-0377: Blocking Command Reports
- XEP-0401: Ad-hoc Account Invitation Generation
- XEP-0045: Multi-User Chat
- XEP-0313: Message Archive Management
- XEP-0198: Stream Management
- XEP-0060: Publish-Subscribe
- Prosody: Download
- Prosody: Security
- Prosody: Certificates
- Prosody: Let's Encrypt
- Prosody: Ports
- Prosody: 13.0.0 release notes
- Prosody: mod_register
- Prosody: mod_register_limits
- Prosody: mod_invites
- Prosody: mod_limits
- Prosody: mod_mam
- Prosody: mod_http_file_share
- ejabberd: Basic configuration
- ejabberd: Listen modules
- ejabberd: Top-level options
- ejabberd: Modules
- ejabberd: Security guide
- ejabberd COPYING (licence)
- Prosody: mod_pep
- Prosody: prosodyctl
Last reviewed: 25 October 2026. XEP-0384 is still Experimental; check the XSF extensions page for its current version and status.