Getting in is not the same as being allowed
Part 3 finished with a client that connects: it has a username, it has a password, and EMQX says yes at the door. That yes answers exactly one question — may you connect. It says nothing about which topics the client may read, which it may write to, or whether anything it sends can be read by whoever else is on the network. This part closes both gaps. Authorization rules pin each account down to the topics it genuinely needs. Listeners decide which port and which protocol a client arrives on, including the encrypted ones.
Authentication asks who you are. It never asks what you want
The authentication you set up in Part 3 controls who may connect. It does not control what a client may do to which topics once it is in. Those are two different questions, and EMQX answers them with two different mechanisms. Here is the shape of the problem in a normal house.
Authorization is what fixes that. You keep access control list rules in EMQX's built-in database, you match publish and subscribe permissions on who plus topic plus action, and you learn the order in which allow and deny get applied. By the end of this part you can tighten Home Assistant and each device down to the topics they are supposed to touch, which is what “least privilege” means in practice.
It is a hotel key card. Reception checking your booking and handing you a card is authentication — they established that you are the person on the reservation. The card then opening room 402 and the gym, but not room 403 and not the manager's office, is authorization. Two different checks, done by two different parts of the building. A hotel that only did the first one would hand every guest a master key.
The second half of this part is about the other side of the same connection: which door the client came in by. EMQX 5.8.9 lets one broker listen on several entry points at once — standard MQTT over TCP, WebSocket, secure WebSocket, and connections encrypted with SSL/TLS. If you want some devices on an encrypted port, Home Assistant on WebSocket and your own testing on 1883, you need to know what those listeners are and how certificates work.
The first rule that matches decides, and the rest never run
Authorization controls permissions on MQTT's publish and subscribe operations. You reach it in the Dashboard sidebar at Access Control → Authorization. EMQX offers four kinds of backend to hold the rules:
- ACL File — the rules live in a file.
- Built-in Database — EMQX's own storage, edited in the Dashboard. This is what the rest of this part uses.
- An external database — MySQL, PostgreSQL, MongoDB or Redis.
- HTTP Server — EMQX asks a web service of yours for the verdict.
Order is the part that surprises people. Every time a client wants to publish or subscribe, EMQX walks a chain of authorizers in order. Each authorizer holds its own set of access control list rules. EMQX starts matching at the first authorizer, and the first rule that matches that client decides — its allow or deny is applied, and nothing further is consulted. If nothing inside an authorizer matches, EMQX moves on to the next one. When nothing matches anywhere, the global default in Global Settings decides.
flowchart TD A["A client sends PUBLISH or SUBSCRIBE"] --> B["Authorizer 1: walk its ACL rules in order"] B -->|"a rule matches"| M["That rule decides"] B -->|"nothing matches"| C["Authorizer 2: walk its ACL rules in order"] C -->|"a rule matches"| M C -->|"nothing matches"| G["Global Settings decides the no-match case"] M --> AL["Allow: the publish or subscribe goes through"] M --> DE["Deny: ignore the request, or disconnect the client"] G --> AL G --> DE
Authentication and authorization are complementary layers, and the built-in database ACL is where they meet: a rule can key off a username or a client ID, so the identity that authentication established is the identity that authorization judges.
| Term | Plain English | In one line |
|---|---|---|
| Authorization | what you may do | Decides publish and subscribe permissions |
| ACL | access control list | The set of topic permission rules |
| Authorizer | authorization checker | One unit that runs a rule set against a backend |
| Allow | let it through | Lets this action and topic pair through |
| Deny | block it | Blocks this action and topic pair |
| Action | the operation being checked | publish or subscribe |
| Topic Filter | topic pattern | A rule's topic, which may contain wildcards |
Global Settings holds two decisions, and both matter. The first is the fallback: when no authorizer finds a matching rule, EMQX can always allow or always deny. The second is what a denial actually does: ignore, which drops the request silently, or disconnect, which cuts the client's connection.
Who, which topic, which action, allow or deny
A built-in database ACL rule describes a permission along three dimensions — client, topic, action — one rule per row saying who may do which action on which topic. You can pick all users to set a baseline, then stack rules for a specific client ID or username on top of it.
The topic in a rule may carry the wildcards you met in Part 2, and their rules do not bend here: + stands for exactly one level, and # stands for the rest of the topic and may only appear at the end. Collect your switch topics under a single filter and one rule covers the lot:
living-room/switch/+/state — one rule covering the state topic of every living-room switch, whatever each switch is called.device/+/state — the state topic of any single device, one level deep.device/# — everything under device/, however many levels deep it goes. Very wide. Be careful where you put it.Allow lets that action-and-topic pair through; Deny blocks it. The same topic can carry both an allow rule and a deny rule, and which one wins is settled together by the authorizer order and the global settings. That is where the classic trap lives.
Say you put a very broad deny on device/# in front, and an allow on device/+/state behind it. The over-broad deny matches first. The allow behind it never gets a turn.
flowchart TD A["Client subscribes to device/kitchen-lamp/state"] --> R1["Rule 1 in the list: Deny, subscribe, device/#"] R1 -->|"this filter matches first"| D["Denied. The chain stops here"] R1 -.->|"never consulted"| R2["Rule 2 in the list: Allow, subscribe, device/+/state"]
It is the list of instructions taped to the office fridge. Somebody reads down from the top and does the first line that applies to them. A line at the top saying “nobody touches the coffee machine” means the line three down saying “Anna refills the beans on Fridays” is never read by anyone. The beans line is not wrong. It is just underneath.
So think the narrow, precise rule through before you write it, and then order the rules by weight. On the Authorization List you can drag rows with the mouse, or reorder them from the Actions column; EMQX starts at the first one and works down until an ACL rule matches.
From an empty Authorization list to a rule you have tested
The built-in database needs no external service and no extra parameters, which makes it the right first authorizer for a home broker. Six steps, and the last one is the one that tells you whether the first five worked.
-
Step 1
Open the Authorization page
Log in to the Dashboard and go to Access Control → Authorization in the sidebar. Click Create at the top right.
-
Step 2
Create a built-in database authorizer
Choose Built-in Database as the backend. It needs no extra parameters, so there is nothing to fill in. Click Create to finish, and you land back on the Authorization List with your authorizer on it.
-
Step 3
Add your first ACL rule
On that authorizer, go to Actions → Permissions to edit the rules. Each rule takes four things: the client identity (client ID, username, or all users), the topic — which may contain
+and#— the action, publish or subscribe, and Allow or Deny. Save. -
Step 4
Put the rules in the order you meant
Narrow rules go above broad ones. On the Authorization List, drag the rows with the mouse or use the reorder controls in the Actions column. If you have written a
#rule anywhere, this is the step where you check nothing useful is sitting underneath it. -
Step 5
Set the global defaults
Back on the Authorization list, click Settings. Decide whether “no rule matched” means allow or deny, and whether a denial should ignore the request or disconnect the client. For a home broker, deny plus ignore is the usual pair — blocked, without a storm of reconnects.
-
Step 6
Prove it with a real publish and a real subscribe
Take your test account, publish to a topic that should be allowed and to one that should be denied, then subscribe to each in turn. Confirm that allow and deny behave the way the rules say. A rule list that has never been tested is a guess written down neatly.
One broker, several doors, and one of them is not MQTT at all
A listener is the server-side point at which EMQX accepts MQTT client connections. Each one has a transport protocol — TCP, SSL, WebSocket or WSS — plus a bind address and a port, and you can set how many concurrent connections it accepts with max_connections. One EMQX instance runs several listeners at the same time, so clients with different jobs can use different protocols and ports.
The official docs list four main transports: TCP on 1883, SSL on 8883, WebSocket on 8083 and Secure WebSocket on 8084. The Dashboard itself is served by the add-on on 18083 over HTTP, which is why the number people quote day to day is five rather than four. Because the add-on runs in host_network mode on the Home Assistant host, all five are the host's own ports.
| Port | Protocol | What it is for |
|---|---|---|
| 1883 | MQTT (TCP) | Standard MQTT; what Home Assistant and Zigbee use most |
| 8083 | MQTT/WS | MQTT over WebSocket. The browser test client uses this |
| 8084 | MQTT/WSS | Secure WebSocket — a WebSocket encrypted with TLS |
| 8883 | MQTTS | MQTT over SSL/TLS: standard MQTT, encrypted |
| 18083 | HTTP | The EMQX Dashboard web interface |
Per the docs, the MQTT path on a WebSocket listener defaults to /mqtt. Note that it is only 18083 that is not an MQTT listener at all — it is the web interface you have been clicking around in, and no client connects to it with an MQTT library.
flowchart LR A["Zigbee bridge and sensors"] --> P1["1883 MQTT over TCP"] B["A device holding a certificate"] --> P2["8883 MQTT over SSL/TLS"] C["Browser test client"] --> P3["8083 MQTT over WebSocket"] D["Browser, encrypted"] --> P4["8084 MQTT over WSS"] E["You, reading the Dashboard"] --> P5["18083 Dashboard over HTTP"] P1 --> X["One EMQX 5.8.9 instance"] P2 --> X P3 --> X P4 --> X P5 --> X
A shop has a front door for customers, a roller shutter at the back for deliveries, and a side door the staff use. All three go into the same shop. Which one you walk through changes nothing about the stock on the shelves — but a back door nobody has used in a year and nobody has checked is still a way in. Shops lock those. So should you.
You do not have to keep all five in play. You may have some devices on 1883 only and others on the encrypted 8883; anything running in a web browser goes to 8083, or 8084 for the secure version. Pick the one you actually need as each requirement comes up, and leave the rest closed. To see what you currently have, open Management → Listeners in the sidebar — the existing listeners are listed with their protocol and port, and Add creates a new one.
Encrypting the traffic, then checking who is at the other end
TLS, Transport Layer Security, encrypts data at the transport layer; SSL was its predecessor, which is why you see both names on the same screens. EMQX uses TLS in three places: for MQTT connections, for Data Integration reaching out to external resources, and between cluster nodes. Each of those can be one-way or mutual.
In one-way TLS the client checks the server's identity certificate. In mutual TLS, or mTLS, the server checks the client's certificate as well, which blocks a man-in-the-middle attack. SSL/TLS needs three files:
EMQX ships with a set of certificates meant only for testing, under etc/certs. That is why the 8883 listener works out of the box as a one-way test setup with no work from you. It is also why it is not ready for anything real.
A test certificate is a name badge you printed at home on the way to the conference. It has your name on it and it looks like a badge. Nobody checked it against anything, and nobody can. The badge handed to you at the registration desk says the same words, but there is a list behind the desk that it came off. A certificate from a trusted authority is the second kind, and that list is what the client is really checking.
sequenceDiagram participant C as Client participant E as EMQX SSL listener C->>E: open a connection on 8883 E->>C: send the server certificate C->>C: check it against a trusted CA Note over C,E: one-way TLS is finished here E->>C: ask for the client certificate C->>E: send the client certificate E->>E: check it, and refuse if there is none E->>C: handshake done, MQTT starts
Here is how to work on an SSL listener in the Dashboard.
-
Step 1
Open the SSL listener's edit page
Go to Management → Listeners and click the Name of the SSL listener — on a fresh install it is called
default. The edit page opens. -
Step 2
Swap in your own certificates
Replace TLS Cert with your server certificate, TLS Key with its private key, and CA Cert with the trusted CA. Click Update when you are done. Before you go live, these must be certificates issued by a CA your clients already trust, for a name your clients actually connect to — not the built-in test set.
-
Step 3
Turn the mTLS handshake on, if you want it
On the same page, the TLS Verify switch decides whether the client certificate is checked. To require mTLS, set TLS Verify to Enable and Fail If No Peer Cert to
true. A client with no certificate then fails during the handshake — every client, including the ones you forgot about. -
Step 4
Get back to a known state if you need to
The Listeners page has a Reset control that returns the listener to the built-in test certificates. When a certificate change breaks 8883 and you cannot tell which of the three files is at fault, resetting is the fastest way to prove the listener itself is fine.
One more sequencing point, because it catches people out: prepare a certificate for every device before you enable mTLS, or every device without one is locked out the moment you click Update. One-way TLS is already enough to keep the traffic from being read. Move up to mTLS when you specifically want “only devices holding a certificate I issued may get in” — and when you have actually issued them.
Two neighbors on the same port, and one setting that does not stick
Because of host_network, those five ports share their numbers with every other service on the Home Assistant host. Two collisions come up far more than the rest.
| What clashes | Port | What the sources say |
|---|---|---|
| Mosquitto | 1883 | The add-on README states plainly that it cannot run at the same time as Mosquitto — both want 1883 |
| WebRTC (AlexxIT) | 8083 | Shares 8083 with EMQX's WebSocket listener |
The add-on README gives the procedure. Stop the conflicting service — Mosquitto, say — first, then start EMQX. If you want both running, go to Management → Listeners, change the port on the listener that clashes or add a listener on a different port, then start that other service again. You can also keep only the listeners you need, 1883 and 8883 for example, and close the rest, which shrinks the surface a scanner can find.
emqx.conf, a change made in the Dashboard only holds until the next EMQX restart. To make a setting stick from startup onwards, set it in a config file or with an environment variable — the add-on's EMQX_LISTENERS__* variables. Those are covered with the rest of the configuration story in Part 7 of this series.This is worth internalizing now rather than discovering during an update. The symptom is unmistakable and deeply confusing the first time: you change a port, everything works, EMQX restarts a week later for an unrelated reason, and the old port is back with no trace of what you did.
The symptom you meet, and the thing to check first
Two tables. The first covers permissions, the second covers ports and certificates. Both are ordered roughly by how often they come up.
| Symptom | Likely cause | What to do |
|---|---|---|
| The client connects but cannot publish to a topic | An authorization rule is blocking that action or that topic | Go to Authorization → Permissions and check whether that account or client has an action-plus-topic-plus-Allow rule covering the topic you want. Then check whether the no-match value in Global Settings is deny |
| There is an Allow rule and it is still blocked | Rule order. EMQX goes by the first match | If a broader # deny sits in front and matches first, the Allow behind it never gets a turn. Simplify the rules or reorder them |
| You want to block a client and nothing happens | No deny rule reaches it before something else allows it | Change the rule aimed at it to deny, or add a topic-plus-subscribe-or-publish-plus-deny rule earlier in the order. Then confirm in Global Settings that a no-match is not an implicit allow |
| An external database authorizer shows Disconnected | Wrong address, wrong credentials, wrong query syntax, or that database is not ready | The configuration still saves, but it fails at run time. Fix the external side, go back to Overview to read its health, and confirm that what you picked really is the built-in database if that is what you meant |
| Symptom | Likely cause | What to do |
|---|---|---|
| EMQX fails to start and the log says the port is already in use | Mosquitto on 1883, or WebRTC on 8083 | Work out which port it is, stop the service that clashes, then start EMQX. If you want them to coexist long term, go to Listeners afterwards and change the port on one side |
| 8883 stops accepting connections after a certificate change | Mismatched files, wrong format, or a name that does not match | Check that certfile and keyfile are a matching pair, that the CA and the server certificate are in PEM format, and that the CN or SAN on the server certificate matches the address the client connects to — otherwise you get a Hostname/IP does not match certificate error. Use Reset in Listeners to go back to the test certificates and isolate the problem |
| Every client without a certificate drops after you enable mTLS | Nothing is broken. That is what mTLS is for | Mutual authentication insists that the client present a trusted certificate. To relax it for now, set TLS Verify back to one-way; otherwise issue that client a trusted client certificate first |
| You changed a port in the Dashboard and the restart reverted it | The listener is defined in emqx.conf |
A Dashboard change only holds until the restart. To make it permanent, override it at startup with the add-on's EMQX_LISTENERS__* environment variables |
The things that come up once the rules are in place
How do authorization and authentication divide the work?
Which is better, the built-in database ACL or an ACL file?
Does a client get disconnected after a deny?
Why does least privilege keep coming up?
Do I have to open all five ports, or can I close some?
8883 and 8084 sound alike. What is the difference?
One-way or mTLS — which should I choose?
Can I use a certificate I signed myself?
Where to go from here
Connect, prove who you are, and be allowed only what you need.
That closes the core of the guide: a client that connects, an identity the broker trusts, permissions pinned to the topics that account actually uses, and an encrypted door for the traffic that warrants one. Part 5 turns from letting messages through to doing something with them — the Rule Engine, where a SQL statement picks messages out of the stream and hands them to an action.
Open the full guidePart 4 of the WoowTech EMQX Complete Guide series on the Apporo blog.
Adapted from the WoowTech EMQX Complete Guide, produced by WoowTech and released under CC BY 4.0. This adaptation is published by Apporo under the same licence.
Light · Air · Water · Control · apporo