Skip to content

Authentication

Authentication

Authentication runs at CONNECT. The broker checks the username and the password. It denies unknown names and bad passwords. It logs each denial. Use the built-in database for fleets the broker manages alone. Use an external back end when passwords already live elsewhere.

flowchart TD
Conn["CONNECT"] -- "JWT-shaped password" --> Jwt["jwks check"]
Conn -- "other password" --> Chain["built-in, ldap, dbauth, webhook, kerberos"]
Jwt -- "valid" --> In["accept"]
Jwt -- "invalid" --> Out["deny + log"]
Chain -- "match" --> In
Chain -- "no match" --> Out

Before you start

Back ends

Back endSetting that enables itUse it when
Built-in databasealways onThe broker manages the fleet alone.
JWTjwks.urlDevices already hold issuer tokens.
HTTP webhookwebhook.urlA custom service decides each connect.
LDAPldap.server_urlStaff clients live in a directory.
SQL and NoSQLdbauth.postgres_url, dbauth.mysql_url, dbauth.redis_url, dbauth.mongodb_urlPasswords live in Postgres, MySQL, Redis or MongoDB.
Kerberoskerberos.keytab_pathThe site already runs Kerberos.

Note: JWT checks see only JWT-shaped passwords. Three base64url parts with dots between them enter JWT checks. Other passwords keep the existing credential paths.

Note: Dashboard operators log in with SCRAM through POST /api/v5/login/challenge and POST /api/v5/login/verify. MQTT clients cannot use SCRAM. See Management users.

Minimal example

This file keeps the defaults and closes anonymous access.

[node]
id = "docs-auth"
[listeners.tcp]
bind = "0.0.0.0:1883"
[listeners.ws]
bind = "0.0.0.0:8083"
path = "/mqtt"
[listeners.api]
bind = "0.0.0.0:18083"
[logging]
level = "warn"
[auth]
enabled = true
allow_anonymous = false

Validate the directory before you start. The command prints ok.

Terminal window
"${INDRAMQTT_BIN:-indramqtt}" --config-dir "${DOCS_CFG_DIR:-./docs-start}" --check-config
ok

Check that it works

Create the user auth-01 through the management API. The call answers 201.

Terminal window
python3 - <<'PY'
import json, os, urllib.request
port = os.environ.get("API_PORT", "18083")
key = os.environ["API_KEY"]
def call(method, path, body=None):
req = urllib.request.Request(
f"http://127.0.0.1:{port}{path}",
data=None if body is None else json.dumps(body).encode(),
method=method,
headers={"Authorization": f"Bearer {key}", "Content-Type": "application/json"},
)
# Password hashing and the config save can take longer on a loaded host.
with urllib.request.urlopen(req, timeout=30) as res:
return res.status
status = call("POST", "/api/v1/auth/users",
{"username": "auth-01", "password": "Auth-docs-pw-1"})
assert status == 201
print(status)
PY
201

List the authenticator chain through the management API.

Terminal window
# Needs a running broker with an operator key. The docs test never starts one.
curl -sf http://127.0.0.1:18083/api/v5/authentication -H "Authorization: Bearer $INDRA_API_KEY"

Limits

  • With zero users the broker is open. The first user closes anonymous access.
  • auth.enabled defaults to true. auth.allow_anonymous defaults to false.
  • A known local name with a wrong password denies at once. The denial stops the chain. It never falls through to a later backend.
  • An unknown local name ignores to the next enabled backend.
  • A stored non-empty chain decides in its own order. Disabled entries never run.
  • A chain change or a user change evicts the node authentication cache. The next CONNECT checks the new credential.
  • A non-default tenant cannot use a global external backend. The broker denies the CONNECT and logs it.
  • A namespace config with an external backend key is refused with 400. The refused write stores nothing.
  • Usernames hold at most 256 bytes. Passwords hold at most 8192 bytes. Longer values fail closed.
  • The broker holds at most 100000 users. One import batch holds at most 10000 entries. Large fleets import in repeated batches.
  • An unreachable back end denies access. It never grants it.
  • Unknown INDRA_* variables refuse startup. A typo can never boot an open broker.

Next steps