MQTT Broker Hardening: TLS, Authentication and ACLs on Mosquitto
A step-by-step guide to securing a Mosquitto MQTT broker — disabling anonymous access, issuing TLS certificates, per-client authentication, and topic-level ACLs that actually restrict what devices can publish and subscribe to.
The Default Install Is Wide Open
A stock Mosquitto install on most distributions starts with a configuration that, if exposed, lets anyone publish and subscribe to anything. No username, no encryption, no topic restrictions.
That is a reasonable default for a five-minute lab test. It is a serious problem the moment the broker becomes reachable from anywhere untrusted — and MQTT brokers have a habit of quietly becoming reachable. Shodan consistently indexes tens of thousands of open MQTT brokers on port 1883, many of them publishing live industrial and building telemetry to whoever asks.
The failure is rarely "we chose not to secure it." It is that MQTT security has four distinct layers, and stopping after the first one feels like being done:
- Anonymous access — off
- Transport encryption — TLS
- Authentication — who is this client
- Authorisation — what may this client touch
Layer 4 is the one that gets skipped, and it is the one that limits blast radius when a device is compromised.
Related: IoT Real-Time Data Transfer Systems — the architecture this broker sits inside.
Layer 1: Stop Anonymous Access
By default, and with no listener configured, Mosquitto 2.x binds to localhost only — a deliberate improvement over 1.x. The moment you add a listener to expose it, you must also decide about anonymous clients.
# /etc/mosquitto/mosquitto.conf
listener 1883
allow_anonymous false
password_file /etc/mosquitto/passwdCreate the password file:
# -c creates a new file; omit -c to add users to an existing one
sudo mosquitto_passwd -c /etc/mosquitto/passwd sensor-gateway-01
sudo mosquitto_passwd /etc/mosquitto/passwd dashboard-backend
sudo chown mosquitto:mosquitto /etc/mosquitto/passwd
sudo chmod 600 /etc/mosquitto/passwdTwo habits worth adopting now rather than later:
- One credential per device, not one shared "iot" account. A shared credential cannot be revoked without a fleet-wide outage, which in practice means it never gets revoked.
- Name credentials after the thing, so an ACL file and a log line are both readable six months later.
At this point credentials cross the network in cleartext. Fixing that is layer 2.
Layer 2: TLS
Generating certificates
For an internal deployment, your own CA is fine and avoids public-CA constraints on device certificates.
# 1. CA key and self-signed CA certificate
openssl genrsa -out ca.key 4096
openssl req -new -x509 -days 3650 -key ca.key -out ca.crt \
-subj "/C=IN/O=TechSystem Lab/CN=TSL Internal MQTT CA"
# 2. Broker key and CSR
openssl genrsa -out server.key 2048
openssl req -new -key server.key -out server.csr \
-subj "/C=IN/O=TechSystem Lab/CN=mqtt.example.com"
# 3. Sign the broker certificate
openssl x509 -req -in server.csr -CA ca.crt -CAkey ca.key \
-CAcreateserial -out server.crt -days 825 -sha256The CN on the broker certificate must match the hostname clients connect to. A mismatch is the single most common cause of "TLS just won't work" — clients that verify properly will reject it, and the temptation is then to disable verification, which throws away most of the benefit.
sudo mkdir -p /etc/mosquitto/certs
sudo cp ca.crt server.crt server.key /etc/mosquitto/certs/
sudo chown -R mosquitto:mosquitto /etc/mosquitto/certs
sudo chmod 600 /etc/mosquitto/certs/server.keyBroker configuration
# Plain listener — bind to loopback only, for local tooling
listener 1883 127.0.0.1
# TLS listener — the only one exposed
listener 8883
cafile /etc/mosquitto/certs/ca.crt
certfile /etc/mosquitto/certs/server.crt
keyfile /etc/mosquitto/certs/server.key
tls_version tlsv1.2
allow_anonymous false
password_file /etc/mosquitto/passwdNote the plain listener is pinned to 127.0.0.1. Local scripts and health checks keep working without exposing an unencrypted port. If you have no such need, delete the line entirely.
Verify from a client:
mosquitto_sub -h mqtt.example.com -p 8883 \
--cafile ca.crt \
-u sensor-gateway-01 -P 'the-password' \
-t 'test/#' -vIf that connects and a mosquitto_pub to test/hello arrives, transport and authentication are working.
Client certificates
For fixed industrial fleets, mutual TLS is stronger than passwords — a stolen password is portable, a device key is not.
listener 8883
cafile /etc/mosquitto/certs/ca.crt
certfile /etc/mosquitto/certs/server.crt
keyfile /etc/mosquitto/certs/server.key
require_certificate true
use_identity_as_username trueuse_identity_as_username true makes the certificate CN the username, so the same CN flows straight into your ACL rules and you maintain one identity list instead of two.
Layer 3 and 4: ACLs, Where the Real Work Is
Authentication proves identity. It does not stop an authenticated temperature sensor from subscribing to # and reading your entire plant, or publishing to a command topic that actuates a valve. That requires an ACL.
acl_file /etc/mosquitto/aclA worked example:
# /etc/mosquitto/acl
# --- backend service: full access to the telemetry tree ---
user dashboard-backend
topic read telemetry/#
topic write commands/#
# --- a field gateway: only its own subtree ---
user sensor-gateway-01
topic write telemetry/site-a/gw-01/#
topic read commands/site-a/gw-01/#
user sensor-gateway-02
topic write telemetry/site-a/gw-02/#
topic read commands/site-a/gw-02/#
# --- read-only monitoring account ---
user grafana-reader
topic read telemetry/#
# no anonymous section == anonymous deniedRead the direction keywords from the client's point of view: write means the client may publish; read means it may subscribe. Getting these backwards is the usual reason a correct-looking ACL blocks everything.
Use pattern rules for fleets
Enumerating a thousand gateways by hand is unmaintainable. pattern substitutes %u (username) and %c (client id):
# Each device may only touch its own username-scoped subtree
pattern write telemetry/%u/#
pattern read commands/%u/#Now sensor-gateway-01 can publish to telemetry/sensor-gateway-01/temp and nothing else — one rule covering the whole fleet, with per-device isolation.
Verify the denials, not the permissions
The mistake here is testing only the happy path. What matters is that the wrong things fail:
# Should SUCCEED — own subtree
mosquitto_pub -h mqtt.example.com -p 8883 --cafile ca.crt \
-u sensor-gateway-01 -P 'pw' \
-t 'telemetry/sensor-gateway-01/temp' -m '23.4'
# Should FAIL — another device's subtree
mosquitto_pub -h mqtt.example.com -p 8883 --cafile ca.crt \
-u sensor-gateway-01 -P 'pw' \
-t 'telemetry/sensor-gateway-02/temp' -m '99.9'
# Should FAIL — wildcard subscribe across everything
mosquitto_sub -h mqtt.example.com -p 8883 --cafile ca.crt \
-u sensor-gateway-01 -P 'pw' -t '#'One caveat that costs people hours: a publish denied by ACL does not error at the client on MQTT 3.1.1 with QoS 0. The broker silently drops it. Check the broker log, or test with QoS 1, or use MQTT 5 which returns proper reason codes.
sudo tail -f /var/log/mosquitto/mosquitto.log
# Denied PUBLISH from sensor-gateway-01 to telemetry/sensor-gateway-02/tempOperational Hardening
# Reject oversized payloads (default is unlimited)
message_size_limit 262144
# Cap per-client queue depth so one dead subscriber
# cannot consume broker memory indefinitely
max_queued_messages 1000
# Bound total connections
max_connections 5000
# Log what you will need for an incident
log_dest file /var/log/mosquitto/mosquitto.log
log_type error
log_type warning
log_type notice
connection_messages trueThen close the network path. The broker should not be internet-reachable unless it genuinely must be:
# Allow TLS MQTT only from the site subnet
sudo ufw allow from 10.20.0.0/16 to any port 8883 proto tcp
# Make sure 1883 is not open to the world
sudo ufw deny 1883If remote devices need access across the internet, put them on a VPN and keep 8883 private rather than exposing the broker directly.
Related: Zero Trust for OT & Industrial Networks — segmentation principles that apply directly here.
Verification Checklist
[ ] allow_anonymous false, and confirmed anonymous connect is refused
[ ] Plain 1883 bound to loopback or firewalled off
[ ] TLS on 8883; broker cert CN matches the client's hostname
[ ] One credential per device, named after the device
[ ] ACL file present and acl_file set
[ ] Cross-device publish DENIED (verified, in broker log)
[ ] Wildcard '#' subscribe DENIED for device accounts (verified)
[ ] message_size_limit and max_queued_messages set
[ ] Broker not reachable from the public internet
[ ] Credential revocation tested — remove a user, confirm it cannot reconnect
The last item is the one nobody rehearses. Revoking a compromised device credential under time pressure, without taking down the fleet, is exactly the operation a per-device credential scheme exists to make possible — so prove it works before you need it.
Frequently Asked Questions
Is TLS enough to secure an MQTT broker?
No. TLS encrypts traffic in transit and lets the client verify the broker, but it says nothing about what an authenticated client is allowed to do. Without an ACL, any valid client can typically subscribe to # and read every topic on the broker. TLS plus per-device credentials plus topic ACLs is the minimum useful set.
What is the difference between MQTT ports 1883 and 8883?
1883 is the conventional plaintext MQTT port; 8883 is the conventional TLS port. Nothing enforces this — the security comes from your listener configuration, not the number. A TLS listener on 1883 is secure; a plaintext listener on 8883 is not.
Why is my MQTT publish silently failing?
Most often an ACL denial. On MQTT 3.1.1 with QoS 0 the broker drops a denied publish without telling the client, so the client sees success. Check the broker log for Denied PUBLISH, retry with QoS 1, or use MQTT 5, which returns explicit reason codes.
Should I use passwords or client certificates?
Client certificates for fixed device fleets — a certificate cannot be copied out of a config file and reused as easily as a password, and use_identity_as_username keeps identity management in one place. Passwords are reasonable for human-operated tools and short-lived integrations where certificate lifecycle management is more burden than benefit.
How do I stop one device reading another device's data?
Use pattern ACL rules scoped by %u, so each client is confined to a subtree named after its own username: pattern write telemetry/%u/#. Then verify the denial explicitly by trying to publish to another device's topic and confirming it is refused in the broker log.