Eclipse Mosquitto is a lightweight open source broker for MQTT, the publish-subscribe protocol used by sensors, smart home hubs and other IoT devices. Clients publish messages to topics such as sensors/greenhouse/temperature, and the broker delivers them to every client subscribed to that topic. In this tutorial you will install Mosquitto on Ubuntu 24.04, require usernames and passwords, limit what each device can publish or read with an access control list, and encrypt connections on port 8883 with a free Let's Encrypt certificate.

Prerequisites

To follow this tutorial you need:

  • A server running Ubuntu 24.04 LTS, for example a CubePath VPS. 512 MB of RAM is enough for thousands of idle device connections.
  • A non-root user with sudo privileges and UFW enabled.
  • A domain name with an A record pointing to your server's public IP, used for the TLS certificate. This guide uses mqtt.your_domain.
  • Port 80 reachable from the internet during certificate issuance and renewal.

Step 1 - Installing Mosquitto

Mosquitto 2.0 and its command line clients are in the Ubuntu repositories:

sudo apt update
sudo apt install -y mosquitto mosquitto-clients

The package starts the broker and enables it at boot. Check the service:

sudo systemctl status mosquitto --no-pager
● mosquitto.service - Mosquitto MQTT Broker
     Loaded: loaded (/usr/lib/systemd/system/mosquitto.service; enabled; preset: enabled)
     Active: active (running) since ...

Since version 2.0, Mosquitto with no listener configured accepts connections only from localhost, so nothing is exposed yet. Test it with a subscriber and a publisher. In one terminal:

mosquitto_sub -h localhost -t 'test/#' -v

In a second terminal:

mosquitto_pub -h localhost -t test/hello -m 'Hello MQTT'

The subscriber prints the topic and message:

test/hello Hello MQTT

Press CTRL+C to stop the subscriber.

Step 2 - Creating MQTT users

Each device or application should have its own credentials, so you can revoke one without touching the others. Create the password file with a first user, admin. The -c flag creates the file and overwrites it if it exists, so use it only this once:

sudo mosquitto_passwd -c /etc/mosquitto/passwd admin

Add a user for a sensor and one for a dashboard that only reads data, without -c:

sudo mosquitto_passwd /etc/mosquitto/passwd sensor01
sudo mosquitto_passwd /etc/mosquitto/passwd dashboard

Each command prompts for the password twice. The file stores salted hashes, not the passwords. Mosquitto warns if the file is readable by other users, so restrict it:

sudo chown mosquitto:mosquitto /etc/mosquitto/passwd
sudo chmod 600 /etc/mosquitto/passwd

Step 3 - Defining access control rules

Without an ACL, any authenticated user can read and write every topic, which means one compromised sensor can inject fake commands for all devices. Create an ACL file:

sudo nano /etc/mosquitto/acl
# Applies to every authenticated user: each device may publish
# under sensors/<its username>/ and read its own command topic
pattern write sensors/%u/#
pattern read commands/%u/#

# Administrator: everything, including the $SYS broker statistics
user admin
topic readwrite #
topic read $SYS/#

# Dashboard: read-only access to all sensor data
user dashboard
topic read sensors/#

How the rules work:

  • pattern lines apply to all users, and %u is replaced by the username of the connected client. User sensor01 can publish to sensors/sensor01/temperature but not to sensors/sensor02/temperature.
  • topic lines after a user line apply only to that user.
  • The # wildcard does not match topics that start with $, so the broker statistics under $SYS need their own rule.
  • Anything not allowed is denied.

Set the same ownership and permissions as the password file:

sudo chown mosquitto:mosquitto /etc/mosquitto/acl
sudo chmod 600 /etc/mosquitto/acl

Step 4 - Enabling authentication

On Ubuntu, /etc/mosquitto/mosquitto.conf already sets persistence and logging and includes every .conf file in /etc/mosquitto/conf.d/. Keep your settings in a separate file there so package upgrades do not conflict with them:

sudo nano /etc/mosquitto/conf.d/default.conf
# Plain MQTT only on localhost, for local tools and testing
listener 1883 localhost

# Global security settings
allow_anonymous false
password_file /etc/mosquitto/passwd
acl_file /etc/mosquitto/acl

Restart the broker and check that it came up:

sudo systemctl restart mosquitto
sudo systemctl status mosquitto --no-pager

An anonymous client is now refused:

mosquitto_pub -h localhost -t test/hello -m 'anonymous'
Connection error: Connection Refused: not authorised.
Error: The connection was refused.

Subscribe as the dashboard user, replacing dashboard_password with the password you set in step 2:

mosquitto_sub -h localhost -u dashboard -P 'dashboard_password' -t 'sensors/#' -v

In another terminal, publish as sensor01 to its own topic:

mosquitto_pub -h localhost -u sensor01 -P 'sensor01_password' -t sensors/sensor01/temperature -m '21.4'
sensors/sensor01/temperature 21.4

Now try to publish to another sensor's topic:

mosquitto_pub -h localhost -u sensor01 -P 'sensor01_password' -t sensors/sensor02/temperature -m '99'

The command exits normally, because with MQTT 3.1.1 the broker cannot tell the client that a publish was rejected, but the dashboard receives nothing. The broker log records the denial:

sudo grep Denied /var/log/mosquitto/mosquitto.log | tail -n 1
... Denied PUBLISH from auto-XXXX (d0, q0, r0, m0, 'sensors/sensor02/temperature', ... (2 bytes))

Step 5 - Getting a TLS certificate

MQTT on port 1883 sends usernames, passwords and data in plain text. Devices on the internet must connect over TLS on port 8883. Install Certbot from the Ubuntu repositories:

sudo apt install -y certbot

Open port 80 for the HTTP challenge and request a certificate with Certbot's standalone web server. Replace mqtt.your_domain with your domain:

sudo ufw allow 80/tcp
sudo certbot certonly --standalone -d mqtt.your_domain
Successfully received certificate.
Certificate is saved at: /etc/letsencrypt/live/mqtt.your_domain/fullchain.pem
Key is saved at:         /etc/letsencrypt/live/mqtt.your_domain/privkey.pem

The files in /etc/letsencrypt/live are readable only by root, while the broker runs as the mosquitto user. Rather than loosening those permissions, copy the certificate into a directory owned by mosquitto every time Certbot renews it. Create a deploy hook, which Certbot runs after each successful renewal:

sudo nano /etc/letsencrypt/renewal-hooks/deploy/mosquitto.sh
#!/usr/bin/env bash
set -euo pipefail

domain="mqtt.your_domain"
dest="/etc/mosquitto/certs"

# Only act on renewals of the MQTT certificate
if [[ "${RENEWED_LINEAGE:-}" != "/etc/letsencrypt/live/${domain}" ]]; then
    exit 0
fi

install -d -m 0750 -o mosquitto -g mosquitto "$dest"
install -m 0644 -o mosquitto -g mosquitto "${RENEWED_LINEAGE}/fullchain.pem" "${dest}/fullchain.pem"
install -m 0600 -o mosquitto -g mosquitto "${RENEWED_LINEAGE}/privkey.pem" "${dest}/privkey.pem"

systemctl restart mosquitto

Make it executable and run it once by hand to copy the certificate you just obtained:

sudo chmod 755 /etc/letsencrypt/renewal-hooks/deploy/mosquitto.sh
sudo RENEWED_LINEAGE=/etc/letsencrypt/live/mqtt.your_domain /etc/letsencrypt/renewal-hooks/deploy/mosquitto.sh
ls -l /etc/mosquitto/certs
-rw-r--r-- 1 mosquitto mosquitto 2848 ... fullchain.pem
-rw------- 1 mosquitto mosquitto  241 ... privkey.pem

Certbot's systemd timer (certbot.timer) renews the certificate automatically. Confirm that renewal, including the hook, works:

sudo certbot renew --dry-run

Step 6 - Enabling the TLS listener

Add a second listener to the same configuration file:

sudo nano /etc/mosquitto/conf.d/default.conf
# Plain MQTT only on localhost, for local tools and testing
listener 1883 localhost

# MQTT over TLS for devices
listener 8883
certfile /etc/mosquitto/certs/fullchain.pem
keyfile /etc/mosquitto/certs/privkey.pem
tls_version tlsv1.2

# Global security settings
allow_anonymous false
password_file /etc/mosquitto/passwd
acl_file /etc/mosquitto/acl

tls_version tlsv1.2 sets the minimum version: TLS 1.2 and 1.3 clients are accepted. The authentication and ACL settings are global, so they apply to both listeners. Restart the broker and open port 8883:

sudo systemctl restart mosquitto
sudo ufw allow 8883/tcp

Confirm Mosquitto is listening on both ports:

sudo ss -tlnp | grep mosquitto
LISTEN 0  100  127.0.0.1:1883  0.0.0.0:*  users:(("mosquitto",pid=...,fd=5))
LISTEN 0  100    0.0.0.0:8883  0.0.0.0:*  users:(("mosquitto",pid=...,fd=6))
LISTEN 0  100       [::]:8883     [::]:*  users:(("mosquitto",pid=...,fd=7))

Test a TLS connection using the domain name, as a remote device would. --capath /etc/ssl/certs tells the client to trust the system CA store, which includes Let's Encrypt:

mosquitto_sub -h mqtt.your_domain -p 8883 --capath /etc/ssl/certs \
  -u dashboard -P 'dashboard_password' -t 'sensors/#' -v
mosquitto_pub -h mqtt.your_domain -p 8883 --capath /etc/ssl/certs \
  -u sensor01 -P 'sensor01_password' -t sensors/sensor01/humidity -m '48'
sensors/sensor01/humidity 48

Step 7 - Connecting a device from Python

Most MQTT libraries work the same way: connect with TLS, authenticate, then publish or subscribe. This example uses the Eclipse Paho client. Install it in a virtual environment:

sudo apt install -y python3-venv
python3 -m venv ~/mqtt-demo
~/mqtt-demo/bin/pip install paho-mqtt

Create a script that simulates sensor01 sending a temperature reading every 10 seconds:

nano ~/sensor.py
import json
import random
import time

import paho.mqtt.client as mqtt

BROKER = "mqtt.your_domain"
USERNAME = "sensor01"
PASSWORD = "sensor01_password"
TOPIC = f"sensors/{USERNAME}/temperature"


def on_connect(client, userdata, flags, reason_code, properties):
    print(f"Connected: {reason_code}")


client = mqtt.Client(mqtt.CallbackAPIVersion.VERSION2, client_id=USERNAME)
client.username_pw_set(USERNAME, PASSWORD)
client.tls_set()  # verify the broker with the system CA store
client.on_connect = on_connect
client.connect(BROKER, 8883, keepalive=60)
client.loop_start()

try:
    while True:
        reading = {"celsius": round(random.uniform(19, 23), 1), "ts": int(time.time())}
        client.publish(TOPIC, json.dumps(reading), qos=1)
        time.sleep(10)
except KeyboardInterrupt:
    pass
finally:
    client.loop_stop()
    client.disconnect()

Run it while your dashboard subscriber from step 6 is still open:

~/mqtt-demo/bin/python ~/sensor.py
Connected: Success

The subscriber shows a new JSON reading every 10 seconds. qos=1 asks the broker to acknowledge each message, and the client resends it if the acknowledgment does not arrive.

Step 8 - Monitoring the broker

Mosquitto publishes its own statistics under the $SYS/ topic tree, which the admin user can read:

mosquitto_sub -h localhost -u admin -P 'admin_password' -t '$SYS/broker/clients/connected' -t '$SYS/broker/messages/received' -v -C 2
$SYS/broker/clients/connected 2
$SYS/broker/messages/received 57

-C 2 exits after two messages. The broker log is at /var/log/mosquitto/mosquitto.log; it records every connection, disconnection and denied operation.

Troubleshooting

The service fails to start after a configuration change. Run sudo journalctl -u mosquitto -n 30 to see the error. A common cause is a typo in a file path or a certificate the mosquitto user cannot read.

Connection Refused: not authorised with the right password. The password file was recreated with -c, or the user was added to a different file. List users with sudo cut -d: -f1 /etc/mosquitto/passwd, then restart Mosquitto after adding users so it reloads the file.

TLS clients fail with certificate verify failed. The client is connecting by IP address or to a name not on the certificate. Always use the exact domain the certificate was issued for, and make sure the device has an up-to-date CA bundle.

Conclusion

Your Mosquitto broker now accepts only authenticated clients, restricts each device to its own topics, and encrypts traffic from the internet with an automatically renewed Let's Encrypt certificate. From here you can add more devices with mosquitto_passwd, connect your broker to a time-series database such as InfluxDB through Telegraf's MQTT consumer, or add a WebSockets listener so browser dashboards can subscribe directly.