IoT 📖 11 min read

Mosquitto MQTT Broker Setup

MQTT is the language most home sensors and microcontrollers use to report readings and receive commands, and Mosquitto is the broker that most of them talk to. This guide installs it, explains why version 2 refuses outside connections out of the box, gives every device its own account and permissions instead of turning on anonymous access, and adds TLS for connections that leave your network.

Jump to: install Mosquitto, passwords, access control, TLS or troubleshooting.

Before you start

Eclipse Mosquitto is a small, widely used MQTT broker. Sensors, microcontrollers and home automation software connect to it to publish readings and receive commands. This guide installs it, turns off anonymous access, gives each device its own account and permissions, adds TLS, and tests every step with the command-line clients.

You need a Linux machine that stays on: a Raspberry Pi, a small VM or the same host that runs Home Assistant. Mosquitto uses very little memory and CPU, so it shares a machine happily. The examples cover both the Debian package and Docker; pick one.

A few MQTT terms come up throughout:

  • Broker: the server, Mosquitto. Clients never talk to each other directly; everything goes through the broker.
  • Topic: a slash-separated name a message is published to, such as home/garage/temperature. Topics don't have to be created in advance.
  • Subscribe: ask the broker for messages on a topic or pattern. + matches one level (home/+/temperature), # matches everything below (home/#).
  • QoS: delivery guarantee. 0 is at most once, 1 is at least once, 2 is exactly once.
  • Retained message: the broker keeps the last message on a topic and hands it to every new subscriber straight away.

Install Mosquitto

Debian, Ubuntu and Raspberry Pi OS
sudo apt update
sudo apt install mosquitto mosquitto-clients
mosquitto -h | head -n 1           # mosquitto version 2.x
systemctl status mosquitto --no-pager

The packaged service reads /etc/mosquitto/mosquitto.conf, which includes every .conf file in /etc/mosquitto/conf.d/. Put your settings in a file there, so package upgrades don't overwrite them.

Docker Compose
mkdir -p ~/stacks/mosquitto/config ~/stacks/mosquitto/data ~/stacks/mosquitto/log
cd ~/stacks/mosquitto
sudo chown -R 1883:1883 data log
compose.yaml
name: mosquitto

services:
  mosquitto:
    image: eclipse-mosquitto:2
    restart: unless-stopped
    ports:
      - "1883:1883"
      - "8883:8883"
    volumes:
      - ./config:/mosquitto/config
      - ./data:/mosquitto/data
      - ./log:/mosquitto/log

The image runs Mosquitto as UID 1883, which is why the data and log directories belong to that user. In the Docker setup, the configuration file is config/mosquitto.conf, and paths inside it start with /mosquitto/. Translate the Debian paths below accordingly.

Port 1883 is published on every interface here, because devices on your LAN need to reach it. Don't forward it through your router to the internet. If devices outside your home need the broker, use a VPN such as WireGuard or the TLS listener with strong passwords, never plain 1883.

Why Mosquitto 2 refuses outside connections

Since version 2.0, Mosquitto with no listener configured accepts connections only from the machine it runs on, and anonymous access is off unless you turn it on. A device on the LAN gets Connection refused, and the log says:

Starting in local only mode. Connections will only be possible from clients running on this machine.

That's deliberate. Older guides fix it with allow_anonymous true, which lets anyone on your network read every sensor and publish commands to every device. Configure a listener with authentication instead.

Passwords for every client

Create the configuration. On Debian, this is /etc/mosquitto/conf.d/local.conf:

/etc/mosquitto/conf.d/local.conf
per_listener_settings false
allow_anonymous false
password_file /etc/mosquitto/passwd
acl_file /etc/mosquitto/acl

listener 1883
protocol mqtt

persistence true
persistence_location /var/lib/mosquitto/

log_dest syslog
log_type error
log_type warning
log_type notice
connection_messages true

Give each device and each program its own account. If a sensor's flash is ever read out, you can revoke that one account without reconfiguring everything else. -c creates a new file and overwrites any existing one, so use it only for the first account:

sudo mosquitto_passwd -c /etc/mosquitto/passwd homeassistant
sudo mosquitto_passwd /etc/mosquitto/passwd garage-sensor
sudo mosquitto_passwd /etc/mosquitto/passwd admin
sudo chown root:mosquitto /etc/mosquitto/passwd
sudo chmod 640 /etc/mosquitto/passwd

The file stores salted hashes, not the passwords. Mosquitto 2.0 and later use PBKDF2 with SHA-512 by default. With Docker, run the same command inside the container: docker compose exec mosquitto mosquitto_passwd -c /mosquitto/config/passwd homeassistant.

Access control lists

Passwords decide who can connect. The ACL file decides what each account can read and write. Without one, every authenticated account can publish to every topic, including commands for devices it has nothing to do with.

/etc/mosquitto/acl
# Home Assistant reads everything and sends commands
user homeassistant
topic readwrite homeassistant/#
topic read home/#
topic write home/+/set
topic readwrite esphome/#

# The garage sensor may only publish its own readings and read its own commands
user garage-sensor
topic write home/garage/#
topic read home/garage/set

# Admin account for debugging from the command line
user admin
topic readwrite #

# A pattern rule: every client may publish its online/offline status under its own name
pattern write status/%u
  • Rules apply to the user line above them until the next user line.
  • read allows subscribing, write allows publishing, and readwrite allows both.
  • pattern rules apply to every user. %u is replaced with the username and %c with the client ID.
  • Anything not explicitly allowed is denied. A denied publish is dropped silently, so a device seems to work while nothing arrives. Check the ACL first when messages go missing.
sudo chown root:mosquitto /etc/mosquitto/acl
sudo chmod 640 /etc/mosquitto/acl
sudo systemctl restart mosquitto
sudo journalctl -u mosquitto -n 20 --no-pager

Test from the command line

Open two terminals. In the first, subscribe as the admin account and print each topic with its message:

mosquitto_sub -h 127.0.0.1 -u admin -P 'admin-password' -t 'home/#' -v

In the second, publish as the sensor:

mosquitto_pub -h 127.0.0.1 -u garage-sensor -P 'sensor-password' -t home/garage/temperature -m 18.4

The first terminal prints home/garage/temperature 18.4. Now try something the ACL forbids:

mosquitto_pub -h 127.0.0.1 -u garage-sensor -P 'sensor-password' -t home/kitchen/light/set -m ON

Nothing arrives in the first terminal, which shows the ACL is working. Passwords typed with -P end up in your shell history. That's acceptable on a test machine; for scripts, put the settings in a file that only you can read and load them with mosquitto_sub -o, or use environment variables.

Finally, test from another machine on the LAN, using the broker's address in place of 127.0.0.1. That proves the listener and the firewall are right, not just the local socket.

Designing topics

Topic names are permanent in practice: once devices and automations use them, renaming means changing everything at once. Spend a few minutes on a scheme first.

  • Go from general to specific: home/garage/temperature, not temperature/garage. Wildcards then work naturally: home/garage/# is everything in the garage.
  • One value per topic. Publishing 18.4 on …/temperature and 61 on …/humidity is simpler to consume than a JSON object, unless the values always change together.
  • Separate state from commands: the device publishes its state on home/garage/door and listens on home/garage/door/set. The ACL can then allow the device to write state and read commands, and nothing else.
  • Avoid a leading slash (/home/...). It creates an empty first level that makes wildcards awkward.
  • Avoid spaces and # or + in names.

Retained messages and last will

A dashboard that starts up has to wait for each sensor's next reading before it knows the value. Publishing with the retain flag fixes that: the broker keeps the latest message and sends it to new subscribers immediately.

mosquitto_pub -h 127.0.0.1 -u garage-sensor -P 'sensor-password' -t home/garage/temperature -m 18.4 -r

To delete a retained message, publish an empty retained message to the same topic: -r -n.

A last will tells the broker what to publish if a client disconnects without saying goodbye, for example because it lost power. Combined with a retained "online" message on connect, it gives every device an accurate availability status:

mosquitto_sub -h 127.0.0.1 -u garage-sensor -P 'sensor-password' -t home/garage/set \
  --will-topic status/garage-sensor --will-payload offline --will-retain

Libraries on microcontrollers, and firmware such as ESPHome, set the last will automatically. The ESPHome guide shows it in action.

TLS on port 8883

On a trusted home LAN, plain MQTT with passwords is a reasonable trade-off: many small devices handle TLS poorly. For anything crossing a network you don't control, add a TLS listener. With a certificate for a real domain, for example from Let's Encrypt with certbot, add:

/etc/mosquitto/conf.d/tls.conf
listener 8883
protocol mqtt
certfile /etc/mosquitto/certs/fullchain.pem
keyfile /etc/mosquitto/certs/privkey.pem
tls_version tlsv1.2

Mosquitto runs as the mosquitto user and can't read certbot's files under /etc/letsencrypt directly. Copy them into /etc/mosquitto/certs from a certbot deploy hook, with ownership root:mosquitto and mode 640, and restart Mosquitto in the same hook so it loads the renewed certificate. Then test with the real hostname, so the certificate is actually checked:

mosquitto_sub -h mqtt.example.net -p 8883 --cafile /etc/ssl/certs/ca-certificates.crt \
  -u admin -P 'admin-password' -t 'home/#' -v

Don't switch off the client's certificate check to get past a certificate error. It then accepts any certificate, which removes the protection TLS was added for. Fix the hostname or the certificate chain instead.

WebSockets for browser clients

Browser-based MQTT tools connect over WebSockets rather than raw TCP. Add a listener if you use one:

listener 9001
protocol websockets

The same users and ACLs apply. Put it behind a reverse proxy with HTTPS if it's reachable beyond the LAN.

Choosing a QoS level

The QoS level is set separately by the publisher and by each subscriber, and the broker delivers at the lower of the two. Higher levels add messages back and forth, so they cost battery and airtime on small devices.

QoSGuaranteeGood for
0At most once. Sent and forgotten.Frequent sensor readings, where the next one arrives soon anyway
1At least once. Resent until acknowledged, so duplicates are possible.Commands and state changes, provided the receiver handles a repeat safely
2Exactly once, using a four-step handshake.Rarely needed at home; counters or billing-style events where a duplicate would be wrong

QoS 1 only helps across a disconnection if the client uses a persistent session: it connects with a fixed client ID and the clean-session flag off. The broker then queues QoS 1 and 2 messages for it while it's away, and keeps them across restarts because persistence true is set. The mosquitto_sub options for this are -i client-id -c -q 1. Queued messages use memory and disk, so max_queued_messages in the configuration caps how many the broker keeps for each offline client.

Watching the broker

Mosquitto publishes its own statistics on topics under $SYS/: connected clients, message rates, memory use. Read them with an account the ACL allows to see them:

mosquitto_sub -h 127.0.0.1 -u admin -P 'admin-password' -t '$SYS/broker/clients/connected' -t '$SYS/broker/messages/received' -v

The single quotes stop the shell treating $SYS as a variable. For alerting, a TCP probe on port 1883 from Uptime Kuma or the Blackbox Exporter tells you when the broker stops accepting connections.

Troubleshooting

Connection refused from another machine

Either there's no listener 1883 line, so Mosquitto is in local-only mode, or a firewall blocks the port. sudo ss -ltnp | grep 1883 should show Mosquitto listening on 0.0.0.0, not 127.0.0.1.

Connection Refused: not authorised

Wrong username or password, or the password file wasn't reloaded. Check the log with sudo journalctl -u mosquitto, which records each failed login with the client's address.

A client connects, but its messages never arrive

Almost always the ACL. Mosquitto drops publishes the account isn't allowed to make, without telling the client. Subscribe as the admin account to # to confirm nothing arrives, then check the rule for that user and topic. Remember that ACL changes need a restart.

Clients keep disconnecting and reconnecting

Two devices are using the same client ID. The broker allows one connection per ID and disconnects the older one each time the other connects. Give every device a unique client ID.

Mosquitto won't start after an edit

Run it in the foreground to see the error: sudo mosquitto -c /etc/mosquitto/mosquitto.conf -v. A setting placed before its listener line, or a file it can't read, is the usual cause.

Next steps