Skip to main content

MQTT Broker

Iotistica uses Mosquitto extended with the mosquitto-go-auth plugin. The plugin adds an HTTP authentication and ACL backend so Mosquitto delegates all credential and permission checks to the Cloud API.


Listeners

ListenerPortProtocolNotes
Plain MQTT1883TCPSuitable for internal / LAN traffic
MQTTS8883TLSTLS-terminated at Mosquitto; cert-manager or self-signed
MQTT over WSS9002WebSocket TLSBrowser-based clients

Anonymous connections are rejected. All clients must present a username and password.


Authentication Flow

Every MQTT connection triggers three HTTP callbacks from Mosquitto to the Cloud API. The API handles them without requiring a client JWT — these are internal Mosquitto plugin calls only.

MQTT client connects


Mosquitto (mosquitto-go-auth plugin)

├─ POST /mosquitto-auth/user ← credential validation
├─ POST /mosquitto-auth/superuser ← superuser flag check
└─ POST /mosquitto-auth/acl ← topic publish/subscribe check


Cloud API
└─ queries mqtt_users + mqtt_acls tables
└─ returns: { result: "allow" | "deny" }

POST /mosquitto-auth/user

Validates username and password.

Request body (JSON or query string):

{ "username": "agent-abc123", "password": "..." }

Logic:

  1. Lookup mqtt_users WHERE username = $1 AND is_active = true
  2. Verify password against stored scrypt hash via crypto.scrypt
  3. If hash format is old (bcrypt), verify and transparently upgrade to scrypt on success
  4. Return { result: "allow" } or { result: "deny" }

POST /mosquitto-auth/superuser

Checks whether the authenticated user has superuser access (can publish/subscribe to any topic without ACL checks).

{ "username": "admin" }

Returns { result: "allow" } if mqtt_users.is_superuser = true.

POST /mosquitto-auth/acl

Checks whether a user may publish or subscribe to a topic.

{ "username": "agent-abc123", "topic": "i/default/a/abc123/endpoints/modbus", "acc": 2 }

acc values: 1 = subscribe, 2 = publish, 3 = subscribe + publish, 7 = all (used for superuser ACLs).

Logic:

  1. Load all ACL rules for the username from mqtt_acls
  2. Match topic against each rule using MQTT wildcard expansion (+ = single level, # = multi-level)
  3. Check that the requested acc is covered by the matched rule's access bitmask

Database Tables

mqtt_users

Stores broker credentials for agents and the admin user.

ColumnTypeDescription
idserialPrimary key
usernametextUnique MQTT username
password_hashtextscrypt hash (format: scrypt$...)
is_superuserbooleanSuperuser bypasses ACL checks
is_activebooleanSoft-disable without deleting
created_attimestamptz
updated_attimestamptz

Password hashing uses Node.js native crypto.scrypt with parameters N=16384, r=8, p=1. The hash format is self-describing so the verifier can detect and upgrade bcrypt hashes transparently.

mqtt_acls

Per-user topic permission rules.

ColumnTypeDescription
idserialPrimary key
usernametextFK to mqtt_users.username
topictextMQTT topic pattern (supports + and # wildcards)
accessintegerPermission bitmask (see below)
priorityintegerEvaluation order — rules are checked highest priority first

ACL Permissions

Access bitmask

The access column is a bitfield. Mosquitto sends the requested access level as a number; the ACL check computes (rule.access & requested) === requested to decide whether a rule covers the request.

ValueBinaryMeaning
1001Subscribe — read messages from matching topics
2010Publish — write messages to matching topics
3011Subscribe + Publish — full read/write (1 | 2)
4100Subscribe (retained) — receive retained messages on subscribe
7111All — subscribe, publish, and retained (1 | 2 | 4)

Examples:

  • A rule with access = 3 allows subscribe (3 & 1 = 1 ✓) and publish (3 & 2 = 2 ✓) but not retained (3 & 4 = 0 ✗).
  • A rule with access = 7 allows everything — subscribe, publish, and retained.
  • A rule with access = 1 allows subscribe only — a publish attempt is denied (1 & 2 = 0 ✗).

Agent users and the admin user are provisioned with access = 7 on their topic scope.


Topic wildcards

MQTT topic patterns support two wildcard characters. The ACL engine compiles each pattern to a regex at cache-warm time so matching is O(1) per rule per check.

WildcardMatchesExample patternMatchesDoes not match
+Any single topic level (no /)i/+/a/+/endpoints/modbusi/default/a/abc123/endpoints/modbusi/default/a/abc123/sub/endpoints/modbus
#Any number of levels including the current one (must be last)i/default/a/abc123/#i/default/a/abc123/state/desiredi/other/a/abc123/state

Wildcard compilation (from auth-cache.ts):

+ → [^/]+ (any chars except slash)
# → .* (any chars including slash)
/ → \/ (escaped for regex)

So the pattern i/+/a/+/# compiles to: ^i\/[^/]+\/a\/[^/]+\/.*$

Exact matches skip the regex path entirely for a fast-path equality check before regex evaluation.


Evaluation order

For each ACL check the engine follows this sequence:

1. Is the user a superuser? ──YES──▶ allow (skip ACL rules entirely)

NO

2. Load all ACL rules for username (from cache or DB)

3. For each rule (in DB order, highest priority first):

├─ Does the topic match the rule pattern?
│ NO → next rule
│ YES → (rule.access & requested) === requested ?
│ YES → allow
│ NO → next rule (insufficient permission level)

4. No rule matched → deny

Key behaviours:

  • Superusers bypass all ACL rulesis_superuser = true means no topic check is ever made.
  • First matching rule that grants sufficient access wins — lower-priority rules are not evaluated once a grant is found.
  • A matching rule with insufficient access does not deny immediately — it falls through to the next rule, allowing a more permissive rule later in the list to grant access.
  • No rules at all → deny — a user with zero ACL rows can authenticate but cannot publish or subscribe to any topic.

Default ACL assignments

User typeTopic patternAccess
Admin / API service#7 (all)
Agenti/{tenantId}/a/{agentUuid}/#7 (all)
Node-RED instance#3 (pub + sub)
Custom usersconfigured per rowany

Agents are scoped to their own namespace — i/{tenant}/a/{uuid}/# — so they can neither read other agents' state topics nor publish to topics outside their prefix. The {tenantId} and {agentUuid} values in the pattern are UUID-encoded at provisioning time.


Auth Cache

Auth decisions are cached in Redis to avoid a database round-trip on every MQTT packet. The cache is read-through: a miss triggers a DB lookup and populates the cache.

VariableDefaultDescription
MQTT_AUTH_CACHE_TTL_SECONDS30Base cache TTL
MQTT_AUTH_USER_CACHE_TTL_SECONDS300TTL for user allow decisions (longer — credentials rarely change)
MQTT_AUTH_DENY_CACHE_TTL_SECONDS5TTL for deny decisions (short — failed auth shouldn't block re-auth)
MQTT_AUTH_CACHE_MAX_ENTRIES5000Max in-memory cache entries
MQTT_AUTH_LOADER_TIMEOUT_MS100Max time to wait for a DB lookup before denying
MQTT_AUTH_USER_LOADER_TIMEOUT_MS500Loader timeout for user auth specifically
MQTT_AUTH_WARMUP_USER_LIMIT100Users to pre-warm into cache on API startup

Cache keys are namespaced by username and decision type. Mutations to mqtt_users or mqtt_acls via the management API call clearMqttAuthCaches() to immediately invalidate affected entries.


Admin and Agent Users

Admin user — seeded automatically by the API on every startup via initializeMqttAdmin(). The username and password come from MQTT_USERNAME / MQTT_PASSWORD env vars. The admin user has is_superuser = true and a wildcard ACL (#, access 7). If the password env var changes and the API restarts, the stored hash is updated idempotently.

Agent users — created during agent provisioning (POST /provisioning/register). Each agent gets a unique MQTT username (agent-{uuid}) with a randomly generated password and a scoped ACL limited to its own topic namespace.


Managing Users via API

All endpoints require Authorization: Bearer <admin-token> and admin role.

MethodPathDescription
GET/api/v1/mqtt-usersList all users with their ACLs
POST/api/v1/mqtt-usersCreate a new MQTT user
PUT/api/v1/mqtt-users/:idUpdate password, superuser flag, or active status
DELETE/api/v1/mqtt-users/:idDelete user and all associated ACLs
POST/api/v1/mqtt-users/:id/aclsAdd an ACL rule to a user
PUT/api/v1/mqtt-acls/:idUpdate an ACL rule
DELETE/api/v1/mqtt-acls/:idDelete an ACL rule

Password updates are hashed server-side before storage. The raw password is never logged or persisted in plaintext.


Agent Topic Namespace

Agents publish and subscribe to topics under the i/ prefix:

i/{tenantId}/a/{agentUuid}/endpoints/{protocol} ← telemetry publish (agent → broker)
i/{tenantId}/a/{agentUuid}/state/desired ← desired state (cloud → agent)
i/{tenantId}/a/{agentUuid}/state/reported ← reported state (agent → cloud)
i/{tenantId}/a/{agentUuid}/jobs ← job dispatch
i/{tenantId}/a/{agentUuid}/events ← audit events

The Cloud API subscribes to all i/# topics via a persistent MQTT connection and routes incoming messages into Redis Streams and TimescaleDB.