Use case · Message broker

Mosquitto MQTT server on bare metal, with 1883 kept private

A Mosquitto MQTT server for devices on the internet needs three things set by hand: a listener, a password file with an ACL, and TLS on port 8883, because since version 2.0 the broker refuses remote anonymous clients and plain 1883 sends passwords in the clear. This page installs Eclipse Mosquitto on Ubuntu, keeps 1883 on 127.0.0.1, puts a Let's Encrypt certificate on 8883, and covers persistence, backups, a restore, upgrades and the licence.

See the machines

What changed in Mosquitto 2.0, exactly

The project's "Migrating from 1.x to 2.0" page describes two cases. Run the broker with no configuration file, or with one that defines no listener, and it binds only to 127.0.0.1 and ::1; in that mode alone, anonymous clients are allowed. Define any listener, say listener 1883, and it binds to all interfaces, but allow_anonymous now defaults to false: clients without a username are refused until you configure a password file and ACL, an authentication plugin, or allow_anonymous true, which the page reserves for a specific need.

Two more 2.0 changes shape this setup. The broker reads its configuration and immediately drops to the unprivileged mosquitto user, so the password file, ACL, certificates and persistence file must be readable by that user. And tls_version became a minimum, with TLS 1.0 disabled.

A password sent to port 1883 crosses the network in clear text. Here 1883 answers on 127.0.0.1 only and ufw has no rule for it; anything off the machine uses 8883 with TLS.

Sizing: the limits that matter more than cores

The Mosquitto documentation publishes no CPU or RAM figure; the home page calls the broker lightweight, fit for anything from low-power single-board computers to full servers. Load comes from connected clients, what each can queue while slow or offline, and the size of the persistence file. The knobs, with defaults from the current mosquitto.conf man page:

OptionDefaultWhat it bounds
max_connections-1 (no limit), per listenerClients on one listener; the OS open-file limit also applies
max_queued_messages1000QoS 1 and 2 messages queued per client beyond those in flight
max_queued_bytes0 (no maximum)Bytes queued per client; whichever limit is hit first wins
max_packet_size2,000,000 bytes since 2.1; no limit beforeLargest packet a client may send before it is disconnected

Our reading: memory follows client count times queue depth, so slow or offline QoS 1 subscribers cost far more than fast ones. Measure resident memory under real traffic before settling on a machine.

Which machine to run it on

A broker is the fixed address every device dials: it wants a stable IP, a port nobody meters, and a console for the day a ufw rule cuts off SSH. A dedicated server gives the broker the whole machine, every core and all the memory, with root and an unmetered port: no transfer allowance, no overage fee. Ready servers take about 30 minutes from payment approval to handover; builds to order take 4 to 24 hours.

  • A single-socket rack server for one broker in front of a device fleet. Every racked single-, dual- and quad-socket server has a full out-of-band console (HPE iLO, Dell iDRAC or a Supermicro BMC).
  • A Moonshot cartridge for a small broker. Of the cartridges, the m510 and m710x carry a console; the rest offer power control alone, so choose one of those two if you want a way back in.
  • Not the X-Gene ARM cartridge: it boots only Ubuntu 16.04 and 18.04, while this page assumes 24.04 or later.

Put the broker in the city nearest the devices; each of our five data centers lists a test address to ping before ordering. One IPv4 address is included. IPv6 is in no default build; it is added at checkout at no charge, and only then does the DNS name need an AAAA record.

Install, firewall and certificate

The mosquitto.org download page says Ubuntu carries Mosquitto in its own repositories, with the mosquitto-dev PPA for a newer release. The README says a binary package should start the broker; with no listener configured yet, 2.0 and later answer on loopback only. The block enables ufw first (SSH, then 8883 and 80), installs the broker, the clients and certbot, and requests a certificate in certbot's standalone mode, which binds port 80 for the challenge. Port 80 stays open for renewals; nothing else listens there.

Certbot creates its live and archive directories at mode 0700, root only, while the broker runs as mosquitto; the current mosquitto(8) page also says sensitive files whose path includes a symbolic link are refused. So the deploy hook copies chain and key into /etc/mosquitto/certs, owned by mosquitto at mode 600, and sends a HUP, on which the broker reloads its certificate. That is the method of misc/letsencrypt/mosquitto-copy.sh in the Mosquitto repository, which already reads certbot's $RENEWED_LINEAGE. Our changes: no domain check, install sets owner and mode in one step (and makes the certs folder mode 700), and the hook is registered with --deploy-hook and started by sh rather than placed in the renewal-hooks folder. Certbot saves the hook with the certificate for renewals. Re-running the block is harmless.

# As root on Ubuntu 24.04 or 26.04. Set D (a DNS name already pointing at this server) and M first.
D=mqtt.example.com; M=you@example.com
if [ "$D" = mqtt.example.com ] || [ "$M" = you@example.com ]; then echo "STOP - set D and M to your own values"
else
sed -i 's/^IPV6=.*/IPV6=yes/' /etc/default/ufw && ufw allow OpenSSH && ufw --force enable &&
apt-get update && apt-get install -y mosquitto mosquitto-clients certbot &&
ufw allow 80/tcp && ufw allow 8883/tcp &&
printf '%s\n' 'set -e' 'C=/etc/mosquitto/certs' 'install -d -o mosquitto -g mosquitto -m 700 $C' \
 'install -m 600 -o mosquitto -g mosquitto $RENEWED_LINEAGE/fullchain.pem $C/server.pem' \
 'install -m 600 -o mosquitto -g mosquitto $RENEWED_LINEAGE/privkey.pem $C/server.key' \
 'pkill -HUP -x mosquitto || true' > /usr/local/sbin/mosquitto-certs.sh &&
certbot certonly --standalone -n --agree-tos -m "$M" -d "$D" \
 --deploy-hook "sh /usr/local/sbin/mosquitto-certs.sh" &&
test -s /etc/mosquitto/certs/server.key && echo "certificate in place" || echo "STOP - read the output above"
fi

mosquitto.conf, the ACL and the first user

This block moves the package's file aside as mosquitto.conf.dist and writes ours: no anonymous clients, the password and ACL files, built-in persistence in /var/lib/mosquitto/, then the two listeners. Our choices beyond the docs: autosave_interval 300 instead of 1800, so a crash loses at most about five minutes of retained messages and session state, and persistent_client_expiration 14d, since by default sessions of clients that never return are kept for ever. There is no include_dir line, so /etc/mosquitto/conf.d is not read, and no log_dest, so logs go to stderr and systemd's journal.

In the ACL, %u is replaced by the connecting username: a device logged in as sensor1 may read and write under devices/sensor1/ only, while dashboard reads the whole tree. The last line, mosquitto_passwd -c, prompts twice for the dashboard password.

# As root, after "certificate in place". Refuses a second run. Ends by asking for the dashboard password.
F=/etc/mosquitto/mosquitto.conf
if [ -e $F.dist ]; then echo "STOP - $F.dist exists, this block has run before"
else
mv $F $F.dist &&
cat > $F <<'EOF' &&
allow_anonymous false
password_file /etc/mosquitto/passwd
acl_file /etc/mosquitto/acl
persistence true
persistence_location /var/lib/mosquitto/
autosave_interval 300
persistent_client_expiration 14d

listener 1883 127.0.0.1

listener 8883
certfile /etc/mosquitto/certs/server.pem
keyfile /etc/mosquitto/certs/server.key
tls_version tlsv1.2
EOF
cat > /etc/mosquitto/acl <<'EOF' &&
# every device reads and writes under devices/<its username>/
pattern readwrite devices/%u/#
user dashboard
topic read devices/#
EOF
chown mosquitto:mosquitto /etc/mosquitto/acl && chmod 600 /etc/mosquitto/acl &&
install -d -o mosquitto -g mosquitto -m 700 /var/lib/mosquitto &&
umask 077 && mosquitto_passwd -c /etc/mosquitto/passwd dashboard
fi

If the block stops part-way, restore the original with mv -f /etc/mosquitto/mosquitto.conf.dist /etc/mosquitto/mosquitto.conf and run it again; -c overwrites an existing password file.

Start it and test both listeners

  1. Add a device account (it prompts): mosquitto_passwd /etc/mosquitto/passwd sensor1.
  2. Hand the file to the broker user and restart: chown mosquitto:mosquitto /etc/mosquitto/passwd && chmod 600 /etc/mosquitto/passwd && systemctl restart mosquitto && systemctl is-active mosquitto. If it is not active, journalctl -u mosquitto -n 30 names what it rejected.
  3. Confirm 1883 is local: ss -ltn | grep -Eq ':1883 ' && ! ss -ltn | grep -Eq '(0\.0\.0\.0|\[::\]|\*):1883 ' && echo "1883 loopback only" || echo "STOP - check the 1883 listener".
  4. Subscribe over TLS through the public name (replace mqtt.example.com with your broker's name and the quoted password with the real one): mosquitto_sub -h mqtt.example.com -p 8883 --cafile /etc/ssl/certs/ca-certificates.crt -u dashboard -P 'dashboard-password' -t 'devices/#' -v.
  5. From a second session, publish as the device on loopback: mosquitto_pub -h 127.0.0.1 -p 1883 -u sensor1 -P 'sensor1-password' -t devices/sensor1/temp -m 21.5. The reading appears in the first; a publish by sensor1 to devices/sensor2/temp should not.

A password given with -P lands in shell history and is visible to ps while the command runs: fine for a test, not for devices. Per the authentication page, a changed password file is reloaded on SIGHUP: after each mosquitto_passwd change, repeat the chown and chmod, then pkill -HUP -x mosquitto. Password files suit a small, fairly static user list; for clients, groups and roles managed remotely, the authentication page points to the dynamic security plugin. Browser clients need a further listener with protocol websockets plus the same certfile and keyfile, on a port you choose and open in ufw.

Backups and a restore

The broker's state is /etc/mosquitto (config, password file, ACL, certificate copies) plus /var/lib/mosquitto/mosquitto.db (retained messages, subscriptions, queued messages). mosquitto(8) documents SIGUSR1 as forcing a write of the persistence database, so the job sends it, waits, archives both trees with the hook and the cron file, checks the listing for mosquitto.db, and only then prunes archives older than 14 days. Archives hold the TLS key and password hashes, hence mode 700.

# As root. Writes /etc/cron.d/mosquitto-backup: 03:20 nightly, 14 days kept, pruned only after a good archive.
install -d -m 700 /var/backups/mosquitto &&
printf '%s\n' 'SHELL=/bin/bash' 'PATH=/usr/sbin:/usr/bin:/sbin:/bin' \
 '20 3 * * * root umask 077; B=/var/backups/mosquitto; T=$(date +\%F-\%H\%M\%S); pkill -USR1 -x mosquitto; sleep 5; tar -czf $B/mqtt-$T.tgz -C / etc/mosquitto var/lib/mosquitto usr/local/sbin/mosquitto-certs.sh etc/cron.d/mosquitto-backup && tar -tzf $B/mqtt-$T.tgz | grep -q mosquitto.db && find $B -name "mqtt-*.tgz" -mtime +14 -delete || echo "$T backup FAILED, see $B" >> $B/errors.log' \
 > /etc/cron.d/mosquitto-backup && chmod 644 /etc/cron.d/mosquitto-backup && echo "backup job written" || echo "STOP - the cron file was not written"

Next morning, ls -l /var/backups/mosquitto should show one archive and no errors.log. Copy archives off the machine yourself, pulled by another host with rsync -a and no --delete. To restore onto a rebuilt server, as root:

  1. Run the install block only, copy one archive to /root, set F to its file name (for example F=/root/mqtt-2026-11-01-032000.tgz) and check it: tar -tzf "$F" | grep -q etc/mosquitto/passwd && echo ok. Stop unless it prints ok.
  2. A /root/mosquitto-before.tgz left from an earlier try blocks this step: move it elsewhere first. Then set the current state aside, verified: [ ! -e /root/mosquitto-before.tgz ] && systemctl stop mosquitto && tar -czf /root/mosquitto-before.tgz -C / etc/mosquitto var/lib/mosquitto && tar -tzf /root/mosquitto-before.tgz > /dev/null && echo saved || echo "STOP - nothing saved, do not go on".
  3. In the same shell, extract without the old certificate copies, reset owners, start: `test -s /root/mosquitto-before.tgz && tar -xzf "$F" -C / --exclude=etc/mosquitto/certs && chown -R mosquitto:mosquitto /var/lib/mosquitto && chown mosquitto:mosquitto /etc/mosquitto/passwd /etc/mosquitto/acl && systemctl start mosquitto && systemctl is-active mosquitto || echo "STOP - use step 4 to go back"`.
  4. Roll back with systemctl stop mosquitto && tar -xzf /root/mosquitto-before.tgz -C / && systemctl start mosquitto.

Upgrades and the 2.1 deprecations

The man page warns that the persistence format may change: a new broker reads old files but saves only the newest format, so copy mosquitto.db before an upgrade you may want to undo, then apt-get update && apt-get install --only-upgrade mosquitto-clients mosquitto. Before your package reaches 2.1, read its ChangeLog: max_packet_size drops to 2,000,000 bytes, an invalid ACL or password file on reload makes the broker quit, and password_file, acl_file and per_listener_settings are deprecated for the password-file and acl-file plugins, with removal planned in 3.0. 2.1 also adds mosquitto --test-config.

Licence, and when another broker fits

The repository's LICENSE.txt dual-licenses the project under the Eclipse Public License 2.0 or the Eclipse Distribution License 1.0, SPDX EPL-2.0 OR BSD-3-Clause. Our general reading, not legal advice: you may pick either. EDL 1.0 is BSD-style and permissive, so passing copies on, modified or not, means keeping the copyright notice, conditions and disclaimer, with no source owed. EPL 2.0 is weak copyleft: whoever distributes the program in any form must also make its source available under the EPL. Operating a broker that devices or customers connect to is not distribution under either.

A lone broker is a single point of failure. mosquitto.conf documents bridges, which forward chosen topics to another broker; in our reading that links brokers rather than replicating one. If the traffic is jobs between applications that need queues, routing and per-message acknowledgements rather than devices publishing on topics, read RabbitMQ on its own server.

Questions

Why are clients refused after installing Mosquitto 2?

Once the configuration defines a listener, allow_anonymous defaults to false, so a client without a username is turned away. Before 2.0, allow_anonymous defaulted to true, so clients without a username got in unless it was set to false. Configure a password file and ACL, use an authentication plugin, or set allow_anonymous true only for a real need.

Should port 1883 be open to the internet?

Not with passwords on it: plain MQTT carries usernames and passwords unencrypted. Bind 1883 to 127.0.0.1 or a private address and give remote clients 8883 with TLS. Open only the ports you serve in ufw.

Can Mosquitto read a Let's Encrypt certificate directly?

Point it at copies. The broker reads certificate files as the mosquitto user, certbot keeps its own in root-only directories, and the current mosquitto(8) page says files reached through a symbolic link are refused. A deploy hook that copies fullchain.pem and privkey.pem to /etc/mosquitto/certs, sets the owner and sends a HUP keeps renewals hands-off.

Where does Mosquitto keep retained messages?

With persistence true, in mosquitto.db under persistence_location, written on exit, every autosave_interval seconds (1800 by default) and on SIGUSR1. Persistence is off by default, and then a restart loses them. The current man page recommends plugin-based persistence, and 2.1 adds a persist-sqlite plugin.

A machine for the broker

Pick an x86-64 line, install Ubuntu 24.04 or 26.04, point a DNS name at it and run the blocks above.

See the machines

Related on Server Room

Other use cases