background-shape
Self-Hosting Mosquitto with TLS and Auth
August 8, 2022 · 4 min read · by Muhammad Amal programming

TL;DR — Compose stack: Mosquitto 2.0 + persistent volume + mounted TLS certs + password file + ACL file. Anonymous off by default in 2.0. Public TLS via Let’s Encrypt or internal CA. The whole setup is one folder of config files.

After picking Mosquitto, the production setup. Mosquitto 2.0 (released late 2020) made some breaking changes: anonymous connections are off by default, listeners require explicit declaration. The setup below works for 2.0+.

The Compose file

name: mqtt-stack

services:
  mosquitto:
    image: eclipse-mosquitto:2.0.15
    restart: unless-stopped
    ports:
      - "1883:1883"    # plain MQTT (internal only)
      - "8883:8883"    # MQTT over TLS
      - "9001:9001"    # WebSocket (optional)
    volumes:
      - ./config/mosquitto.conf:/mosquitto/config/mosquitto.conf:ro
      - ./config/acl.conf:/mosquitto/config/acl.conf:ro
      - ./config/passwords:/mosquitto/config/passwords:ro
      - ./certs:/mosquitto/certs:ro
      - mosquitto-data:/mosquitto/data
      - mosquitto-log:/mosquitto/log

volumes:
  mosquitto-data:
  mosquitto-log:

The config

# config/mosquitto.conf
persistence true
persistence_location /mosquitto/data/
log_dest file /mosquitto/log/mosquitto.log
log_type error
log_type warning
log_type notice

# Plain MQTT — internal only
listener 1883
allow_anonymous false
password_file /mosquitto/config/passwords
acl_file /mosquitto/config/acl.conf

# MQTT over TLS
listener 8883
allow_anonymous false
password_file /mosquitto/config/passwords
acl_file /mosquitto/config/acl.conf
cafile /mosquitto/certs/ca.crt
certfile /mosquitto/certs/server.crt
keyfile /mosquitto/certs/server.key
require_certificate false  # set to true for mTLS

# WebSockets (browser clients) — optional
listener 9001
protocol websockets
allow_anonymous false
password_file /mosquitto/config/passwords

Three listeners. Plain on 1883 (only for trusted networks). TLS on 8883 (public). WebSocket on 9001 (browser dashboards).

Passwords

# Create the password file
docker run --rm -v $(pwd)/config:/config eclipse-mosquitto:2.0.15 \
  mosquitto_passwd -c -b /config/passwords device1 password1

# Add more users
docker run --rm -v $(pwd)/config:/config eclipse-mosquitto:2.0.15 \
  mosquitto_passwd -b /config/passwords device2 password2

The password file is hashed (PBKDF2). Don’t manually edit; use mosquitto_passwd.

ACL

# config/acl.conf

# Device pattern: each device publishes to its own subtree only
pattern readwrite factory/%u/#

# Dashboard user reads everything
user dashboard
topic read factory/#

# Anomaly service reads everything, publishes alerts
user anomaly-detector
topic read factory/#
topic write alerts/#

# Admin
user admin
topic readwrite #

%u substitutes the connecting username. Each device (device1, device2) can only access factory/device1/# etc. ACL enforced server-side.

TLS certificates

Three options:

Public CA (Let’s Encrypt) for public-facing broker:

certbot certonly --standalone -d mqtt.example.com
# Output: /etc/letsencrypt/live/mqtt.example.com/

Copy fullchain.pemserver.crt, privkey.pemserver.key. Mosquitto doesn’t need the CA cert for client verification when using Let’s Encrypt (trusted public CA chain). For mTLS or internal CAs, provide cafile.

Internal CA (self-signed) for private network:

# CA
openssl genrsa -out ca.key 4096
openssl req -new -x509 -days 3650 -key ca.key -out ca.crt -subj "/CN=My IoT CA"

# Server cert
openssl genrsa -out server.key 4096
openssl req -new -key server.key -out server.csr -subj "/CN=mqtt.internal"
openssl x509 -req -in server.csr -CA ca.crt -CAkey ca.key -CAcreateserial -out server.crt -days 825

Devices need ca.crt to trust the server. Distribute via your config management.

No TLS (internal network only):

Skip the 8883 listener. Trust the network. Acceptable for fully air-gapped factory networks. Always document the assumption.

Testing the connection

From your laptop:

# Plain (only if you exposed 1883)
docker run --rm eclipse-mosquitto:2.0.15 mosquitto_sub \
  -h <host> -p 1883 -u device1 -P password1 -t "factory/device1/test" -v

# TLS
docker run --rm -v $(pwd)/certs:/certs eclipse-mosquitto:2.0.15 mosquitto_sub \
  -h <host> -p 8883 --cafile /certs/ca.crt \
  -u device1 -P password1 -t "factory/device1/test" -v

In another terminal, publish:

docker run --rm eclipse-mosquitto:2.0.15 mosquitto_pub \
  -h <host> -p 1883 -u device1 -P password1 \
  -t "factory/device1/test" -m "hello"

Subscriber should print the message.

High availability

For HA, run two Mosquitto instances behind a TCP load balancer (HAProxy, Nginx stream module, AWS NLB). Each broker has its own state — clients reconnecting to the other broker miss in-flight messages.

Mosquitto’s “bridge” mode can forward selected topics between brokers. Configure each to bridge to the other for the topics that matter. Eventual consistency, not strict HA.

For real HA: switch to EMQX or HiveMQ with clustering.

Logs and metrics

Mosquitto logs to file (/mosquitto/log/mosquitto.log). For a Compose stack, tail:

docker compose exec mosquitto tail -f /mosquitto/log/mosquitto.log

For metrics:

  • Mosquitto exposes $SYS/# topics with internal stats
  • Subscribe to them with a Prometheus exporter (e.g., sapcc/mosquitto-exporter)
  • Scrape from your Prometheus

For our project with 120 devices, basic file logs + a daily mosquitto_sub health-check script is sufficient. At scale, the Prometheus exporter is worth the setup.

Common Pitfalls

Forgetting allow_anonymous false. Mosquitto 2.0 defaults safe; ensure config doesn’t override.

No ACL. Every device can publish/subscribe to everything. One compromised device leaks (or pollutes) the whole bus.

Same username for many devices. Compromise one, all are at risk. Per-device credentials.

TLS cert with bad CN. Clients reject. Verify cert matches the hostname being used.

Manual edit of password file. Breaks hashes. Always use mosquitto_passwd.

Forgetting to restart on config change. docker compose restart mosquitto after editing.

Wrapping Up

Compose + mosquitto.conf + passwords + ACL + certs = production-ready broker. Wednesday: sensor data schemas — what to actually put in those payloads.