Skip to content

Authentication & ACL

Authentication & ACL

The complete MQTT platform in one small binary: broker, SQL rules, data connectors and multi-tenancy in a single process.

Manage users and ACL rules. Use it to add a user or a rule. Reads come from the user store.

flowchart LR
Op["operator"] -- "POST /api/v1/auth/users" --> API["management API"]
API -- "write user row" --> U["users"]

Before you start

  • Start the broker from First start.
  • Set INDRA_API_KEY to an operator key. The examples read it.

Note: Calls except /healthz need an operator API key. Pass it as Authorization: Bearer $INDRA_API_KEY.

User credential management and topic ACL rules

GET /api/v1/auth/acls

List ordered ACL rules

Operation id: listAcls.

GET /api/v1/auth/acls lists the ordered ACL rules of a tenant.

The rules keep evaluation order. The first match wins. An absent tenant reads the default tenant. A tenant with no rules returns an empty list.

Parameters:

NameInRequiredTypeDescription
tenantqueryfalsestringTenant table to list; absent lists the default tenant

Request body:

No request body.

Responses:

StatusDescription
200List of ACL rules evaluated in order

Response 200 body:

Media type application/json:

{
"type": "array",
"items": {
"type": "object",
"properties": {
"client_pattern": {
"type": "string"
},
"action": {
"type": "string",
"enum": [
"publish",
"subscribe",
"all"
]
},
"topic_pattern": {
"type": "string"
},
"allow": {
"type": "boolean"
}
}
}
}

Notes:

  • The rules keep evaluation order. The first match wins.

  • An absent tenant reads the default tenant.

Verified: the runner ran this call through the management API. See ../../_verified/api/listAcls.json.

Verified example:

Request: GET /api/v1/auth/acls after the flow creates two ordered rules.

Captured output:

listed

Example:

# Illustrative request. Values come from the schema above.
# The example flow below runs verified calls on a live broker.
curl -s -X GET 'http://127.0.0.1:18083/api/v1/auth/acls' \
-H "Authorization: Bearer $INDRA_API_KEY"

POST /api/v1/auth/acls

Add new ACL rule

Operation id: createAcl.

POST /api/v1/auth/acls appends one ordered ACL rule to a tenant.

A tenant holds at most 10000 rules. An empty client pattern fails with 400. The action must be publish, subscribe or all. The topic pattern must be a valid MQTT filter. A successful write returns 201 with the stored client pattern, topic pattern and allow value. The write persists and clears cached authorization decisions.

Parameters:

NameInRequiredTypeDescription
tenantqueryfalsestringTenant table to write; absent writes the default tenant

Request body:

Media type application/json:

Required: client_pattern, action, topic_pattern, allow.

FieldTypeDescription
client_patternstring
actionstring
topic_patternstring
allowboolean

Responses:

StatusDescription
201ACL rule registered
400Invalid ACL input
500Registry save failed after the in-memory write

Notes:

  • Send client_pattern, action, topic_pattern and allow.

  • An empty client pattern fails with 400. The action must be publish, subscribe or all.

  • The topic pattern must be a valid MQTT filter.

  • A tenant holds at most 10000 rules.

  • The write persists and clears cached authorization decisions.

Verified: the runner ran this call through the management API. See ../../_verified/api/createAcl.json.

Verified example:

Request: POST /api/v1/auth/acls with client_pattern doc-acl-create, action publish, topic_pattern sensors/#, allow true.

Captured output:

created

Example:

# Illustrative request. Values come from the schema above.
# The example flow below runs verified calls on a live broker.
curl -s -X POST 'http://127.0.0.1:18083/api/v1/auth/acls' \
-H "Authorization: Bearer $INDRA_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"client_pattern": "doc-client",
"action": "publish",
"topic_pattern": "sensors/#",
"allow": true
}'

DELETE /api/v1/auth/acls/{id}

Delete one ACL rule by index

Operation id: deleteAcl.

DELETE /api/v1/auth/acls/{id} removes one ACL rule at an index in a tenant.

id is the zero-based index in the tenant rule list. A successful delete returns 204 with no body. An index past the end returns 404. The delete persists and clears cached authorization decisions.

Parameters:

NameInRequiredTypeDescription
idpathtrueintegerIndex in the tenant rule list
tenantqueryfalsestringTenant table to write; absent writes the default tenant

Request body:

No request body.

Responses:

StatusDescription
204ACL rule deleted
404ACL rule not found
500Registry save failed after the in-memory write

Notes:

  • id is the zero-based index in the tenant rule list.

  • Success answers 204 with no body.

  • An index past the end fails with 404.

  • The write persists and clears cached authorization decisions.

Verified: the runner ran this call through the management API. See ../../_verified/api/deleteAcl.json.

Verified example:

Request: DELETE /api/v1/auth/acls/{id} for the index of the new doc-acl-delete rule.

Captured output:

deleted

Example:

# Illustrative request. Values come from the schema above.
# The example flow below runs verified calls on a live broker.
curl -s -X DELETE 'http://127.0.0.1:18083/api/v1/auth/acls/1' \
-H "Authorization: Bearer $INDRA_API_KEY"

GET /api/v1/auth/users

List all configured users and quotas

Operation id: listUsers.

GET /api/v1/auth/users lists the users and quota objects of a tenant.

The names are sorted. A password never appears in the answer. An absent tenant reads the default tenant.

Parameters:

NameInRequiredTypeDescription
tenantqueryfalsestringTenant table to list; absent lists the default tenant

Request body:

No request body.

Responses:

StatusDescription
200List of users with quota allocations

Response 200 body:

Media type application/json:

{
"type": "array",
"items": {
"type": "object",
"properties": {
"username": {
"type": "string"
},
"quotas": {
"type": "object",
"properties": {
"max_connections": {
"type": "integer"
},
"max_publish_rate": {
"type": "integer"
},
"max_publish_burst": {
"type": "integer"
}
}
}
}
}
}

Notes:

  • Names are sorted. A password never appears in the answer.

  • An absent tenant reads the default tenant.

Verified: the runner ran this call through the management API. See ../../_verified/api/listUsers.json.

Verified example:

Request: GET /api/v1/auth/users after the flow creates two users.

Captured output:

listed

Example:

# Illustrative request. Values come from the schema above.
# The example flow below runs verified calls on a live broker.
curl -s -X GET 'http://127.0.0.1:18083/api/v1/auth/users' \
-H "Authorization: Bearer $INDRA_API_KEY"

POST /api/v1/auth/users

Create or update user credentials and quotas

Operation id: createUser.

POST /api/v1/auth/users stores one username and password in a tenant.

An empty username or password fails with 400. A create for an existing username replaces the stored credential. The password is write-only and never appears in a read. A password longer than 8192 bytes is refused. A successful write returns 201 with the username and quota object. The write persists and clears cached authorization decisions.

Parameters:

NameInRequiredTypeDescription
tenantqueryfalsestringTenant table to write; absent writes the default tenant

Request body:

Media type application/json:

Required: username, password.

FieldTypeDescription
usernamestring
passwordstring
quotasobject

Responses:

StatusDescription
201User created or updated successfully
400Invalid input payload
500Registry save failed after the in-memory write

Notes:

  • Send username and password. An empty value fails with 400.

  • Success answers 201 with the stored row. The password is write-only.

  • A create for an existing username replaces the stored credential.

  • A password longer than 8192 bytes is refused.

  • The write persists and clears cached authorization decisions.

Verified: the runner ran this call through the management API. See ../../_verified/api/createUser.json.

Verified example:

Request: POST /api/v1/auth/users with username doc-user-create, a password and quota values.

Captured output:

created

Example:

# Illustrative request. Values come from the schema above.
# The example flow below runs verified calls on a live broker.
curl -s -X POST 'http://127.0.0.1:18083/api/v1/auth/users' \
-H "Authorization: Bearer $INDRA_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"username": "doc-user-1",
"password": "Secret123!",
"quotas": {}
}'

DELETE /api/v1/auth/users/{username}

Delete one user

Operation id: deleteUser.

DELETE /api/v1/auth/users/{username} removes one user from a tenant.

A successful delete returns 204 with no body. An unknown username returns 404. The delete persists.

Parameters:

NameInRequiredTypeDescription
usernamepathtruestringUser name
tenantqueryfalsestringTenant table to write; absent writes the default tenant

Request body:

No request body.

Responses:

StatusDescription
204User deleted
404User not found
500Registry save failed after the in-memory write

Notes:

  • Success answers 204 with no body.

  • An unknown username fails with 404.

Verified: the runner ran this call through the management API. See ../../_verified/api/deleteUser.json.

Verified example:

Request: DELETE /api/v1/auth/users/doc-user-delete for a user that the flow creates.

Captured output:

deleted

Example:

# Illustrative request. Values come from the schema above.
# The example flow below runs verified calls on a live broker.
curl -s -X DELETE 'http://127.0.0.1:18083/api/v1/auth/users/doc-user-1' \
-H "Authorization: Bearer $INDRA_API_KEY"

GET /api/v1/auth/verify_stats

Show password verification counters

Operation id: getVerifyStats.

GET /api/v1/auth/verify_stats reads the password verification counters of the process.

verifications counts blocking password hashes. dedup_joins counts CONNECTs that shared one leader check. The counters live in memory and restart at zero.

Parameters:

No parameters.

Request body:

No request body.

Responses:

StatusDescription
200Verification and deduplication counters

Response 200 body:

Media type application/json:

{
"type": "object",
"properties": {
"verifications": {
"type": "integer"
},
"dedup_joins": {
"type": "integer"
}
}
}

Notes:

  • The counters live in memory and restart at zero.

  • verifications counts blocking password hashes. dedup_joins counts CONNECTs that shared one leader check.

Verified: the runner ran this call through the management API. See ../../_verified/api/getVerifyStats.json.

Verified example:

Request: GET /api/v1/auth/verify_stats after the flow creates a user and connects it.

Captured output:

counted

Example:

# Illustrative request. Values come from the schema above.
# The example flow below runs verified calls on a live broker.
curl -s -X GET 'http://127.0.0.1:18083/api/v1/auth/verify_stats' \
-H "Authorization: Bearer $INDRA_API_KEY"

Example flow

Create a real object through the management API, read it back, then delete it. The docs harness runs this flow against a started broker.

Terminal window
python3 - <<'PY'
import json, os, urllib.request, urllib.error
port = os.environ.get("API_PORT", "18083")
key = os.environ.get("API_KEY", "")
base = f"http://127.0.0.1:{port}"
def call(method, path, body=None):
data = json.dumps(body).encode() if body is not None else None
req = urllib.request.Request(base + path, data=data, method=method)
req.add_header("Authorization", f"Bearer {key}")
if body is not None:
req.add_header("Content-Type", "application/json")
try:
with urllib.request.urlopen(req, timeout=5) as r:
return r.status, r.read().decode()
except urllib.error.HTTPError as e:
return e.code, e.read().decode()
s, b = call("POST", "/api/v1/auth/users", {"username": "doc-user-1", "password": "Secret123!"})
assert s == 201, b
assert "doc-user-1" in b, b
s, b = call("GET", "/api/v1/auth/users")
assert s == 200, b
assert "doc-user-1" in b, b
s, b = call("DELETE", "/api/v1/auth/users/doc-user-1")
assert s in (200, 204), b
s, b = call("GET", "/api/v1/auth/users")
assert s == 200, b
assert "doc-user-1" not in b, b
print("OK")
PY
OK

Check that it works

Start the broker with this file. The health endpoint returns OK.

[node]
id = "doc-ref"
[listeners.tcp]
bind = "0.0.0.0:1883"
[listeners.tls]
bind = "0.0.0.0:8883"
[listeners.ws]
bind = "0.0.0.0:8083"
[listeners.wss]
bind = "0.0.0.0:8084"
[listeners.api]
bind = "0.0.0.0:18083"
[logging]
level = "warn"
Terminal window
python3 - <<'PY'
import os, socket
port = int(os.environ.get("API_PORT", "18083"))
sock = socket.create_connection(("127.0.0.1", port), timeout=5)
sock.sendall(b"GET /healthz HTTP/1.0\r\nHost: docs\r\n\r\n")
print(sock.makefile().read().strip().splitlines()[-1])
PY
OK

Check one documented call. GET /api/v1/auth/users answers 200 with its rows. It prints OK.

Terminal window
python3 - <<'PY'
import os, urllib.request
port = os.environ.get("API_PORT", "18083")
key = os.environ.get("API_KEY", "")
base = f"http://127.0.0.1:{port}"
req = urllib.request.Request(base + "/api/v1/auth/users", method="GET")
req.add_header("Authorization", f"Bearer {key}")
with urllib.request.urlopen(req, timeout=5) as r:
body = r.read().decode()
assert r.status == 200, body
assert body.strip().startswith("["), body
print("OK")
PY
OK

Next steps