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_KEYto an operator key. The examples read it.
Note: Calls except
/healthzneed an operator API key. Pass it asAuthorization: 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:
| Name | In | Required | Type | Description |
|---|---|---|---|---|
tenant | query | false | string | Tenant table to list; absent lists the default tenant |
Request body:
No request body.
Responses:
| Status | Description |
|---|---|
| 200 | List 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:
listedExample:
# 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:
| Name | In | Required | Type | Description |
|---|---|---|---|---|
tenant | query | false | string | Tenant table to write; absent writes the default tenant |
Request body:
Media type application/json:
Required: client_pattern, action, topic_pattern, allow.
| Field | Type | Description |
|---|---|---|
client_pattern | string | |
action | string | |
topic_pattern | string | |
allow | boolean |
Responses:
| Status | Description |
|---|---|
| 201 | ACL rule registered |
| 400 | Invalid ACL input |
| 500 | Registry save failed after the in-memory write |
Notes:
-
Send
client_pattern,action,topic_patternandallow. -
An empty client pattern fails with 400. The action must be
publish,subscribeorall. -
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:
createdExample:
# 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:
| Name | In | Required | Type | Description |
|---|---|---|---|---|
id | path | true | integer | Index in the tenant rule list |
tenant | query | false | string | Tenant table to write; absent writes the default tenant |
Request body:
No request body.
Responses:
| Status | Description |
|---|---|
| 204 | ACL rule deleted |
| 404 | ACL rule not found |
| 500 | Registry save failed after the in-memory write |
Notes:
-
idis 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:
deletedExample:
# 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:
| Name | In | Required | Type | Description |
|---|---|---|---|---|
tenant | query | false | string | Tenant table to list; absent lists the default tenant |
Request body:
No request body.
Responses:
| Status | Description |
|---|---|
| 200 | List 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:
listedExample:
# 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:
| Name | In | Required | Type | Description |
|---|---|---|---|---|
tenant | query | false | string | Tenant table to write; absent writes the default tenant |
Request body:
Media type application/json:
Required: username, password.
| Field | Type | Description |
|---|---|---|
username | string | |
password | string | |
quotas | object |
Responses:
| Status | Description |
|---|---|
| 201 | User created or updated successfully |
| 400 | Invalid input payload |
| 500 | Registry save failed after the in-memory write |
Notes:
-
Send
usernameandpassword. 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:
createdExample:
# 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:
| Name | In | Required | Type | Description |
|---|---|---|---|---|
username | path | true | string | User name |
tenant | query | false | string | Tenant table to write; absent writes the default tenant |
Request body:
No request body.
Responses:
| Status | Description |
|---|---|
| 204 | User deleted |
| 404 | User not found |
| 500 | Registry 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:
deletedExample:
# 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:
| Status | Description |
|---|---|
| 200 | Verification 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.
-
verificationscounts blocking password hashes.dedup_joinscounts 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:
countedExample:
# 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.
python3 - <<'PY'import json, os, urllib.request, urllib.errorport = 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, bassert "doc-user-1" in b, bs, b = call("GET", "/api/v1/auth/users")assert s == 200, bassert "doc-user-1" in b, bs, b = call("DELETE", "/api/v1/auth/users/doc-user-1")assert s in (200, 204), bs, b = call("GET", "/api/v1/auth/users")assert s == 200, bassert "doc-user-1" not in b, bprint("OK")PYOKCheck 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"python3 - <<'PY'import os, socketport = 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])PYOKCheck one documented call. GET /api/v1/auth/users answers 200 with its rows. It prints OK.
python3 - <<'PY'import os, urllib.requestport = 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, bodyassert body.strip().startswith("["), bodyprint("OK")PYOKNext steps
- API reference: the other tags.
- Configuration reference: the settings behind the rows.
- Metrics reference: the counter names.