-
-
Notifications
You must be signed in to change notification settings - Fork 6
Authentication and ACL
Secure your broker with authentication (identity verification), authorization (access control), role grouping, and tamper-resistant publisher identity.
┌─────────────────────────────────────────────────────────────────┐
│ Security Layers │
├─────────────────────────────────────────────────────────────────┤
│ 1. Transport Security │ TLS encryption, mTLS │
│ 2. Authentication │ Who is connecting? │
│ 3. Authorization (ACL) │ What can they access? │
│ 4. RBAC │ Group permissions into roles │
│ 5. Identity Injection │ Broker-stamped sender identity │
└─────────────────────────────────────────────────────────────────┘
These layers compose freely — enable any combination.
| Method | Description | Use Case |
|---|---|---|
| Anonymous | No credentials required | Development, internal networks |
| Password | Username/password with Argon2id | Internal users |
| PLAIN SASL | RFC 4616 PLAIN over MQTT v5 enhanced auth | Internal users |
| SCRAM-SHA-256 | Challenge-response, no password sent | High security |
| JWT | Stateless token verification (HS256/RS256/ES256) | Single IdP |
| Federated JWT | Multi-issuer with JWKS | Google, Keycloak, Azure AD |
| mTLS | Client certificate fingerprint | IoT devices |
Allow connections without credentials (development only):
mqttv5 broker --allow-anonymousPasswords are stored with Argon2id hashing, excluded from log output, and files should be mode 0600.
# Interactive (prompts for password)
mqttv5 passwd alice passwd.txt
# Create a new file (overwrites)
mqttv5 passwd -c alice passwd.txt
# Batch mode (password passed on the command line)
mqttv5 passwd bob passwd.txt -b mypassword
# Delete a user
mqttv5 passwd -D alice passwd.txt
# Output a hash to stdout instead of writing a file
mqttv5 passwd -n alicePassword file format: username:argon2id_hash (one per line). Lines starting with
# are comments.
mqttv5 broker \
--auth-password-file passwd.txt \
--allow-anonymous=falseRFC 4616 PLAIN over the MQTT v5 enhanced-authentication flow. Credentials are sent
as three NUL-separated fields [authzid]\0username\0password (authzid usually
empty). Uses the same password file as password auth. Clients set
auth_method: "PLAIN".
mqttv5 broker --auth-password-file passwd.txtChallenge-response authentication — the password is never transmitted. Client-side
passwords are zeroized on drop and credentials are compared in constant time.
Channel binding is not supported (clients requesting y,, or requiring p= are
rejected).
# Create/update SCRAM credentials (username then optional file)
mqttv5 scram alice scram.txt
# Options: -c create, -b <pass> batch, -D delete, -n stdout, -i <iterations>
mqttv5 scram bob scram.txt -b mypassword -i 310000
# Start broker
mqttv5 broker --auth-method scram --scram-file scram.txtSCRAM credentials file format:
username:salt_b64:iterations:stored_key_b64:server_key_b64 (5 colon-separated
fields). Default iteration count: 310,000. Per-client handshake state expires after
60 seconds; at most 1,000 concurrent SCRAM handshakes; concurrent authentication
for the same client ID is rejected.
Stateless token verification with HS256, RS256, or ES256. Tokens must
include exp and sub claims. Verifier selection uses the kid header (not
alg) to prevent algorithm-confusion attacks.
mqttv5 broker \
--auth-method jwt \
--jwt-algorithm hs256 \
--jwt-key-file secret.key \
--jwt-issuer "https://auth.example.com"mqttv5 broker \
--auth-method jwt \
--jwt-algorithm rs256 \
--jwt-key-file public.pem \
--jwt-issuer "https://auth.example.com" \
--jwt-audience "my-audience" \
--jwt-clock-skew 60Support multiple identity providers with automatic JWKS key refresh. The JWKS cache has a circuit breaker (3 consecutive failures → 60s open period) and a configurable static fallback key.
Federated user IDs are constructed as issuer_domain:sub (e.g.
accounts.google.com:12345); a custom prefix can be set with --jwt-issuer-prefix.
IdP verifies identity, broker handles authorization via ACL:
mqttv5 broker \
--auth-method jwt-federated \
--jwt-issuer "https://accounts.google.com" \
--jwt-jwks-uri "https://www.googleapis.com/oauth2/v3/certs" \
--jwt-fallback-key fallback.pem \
--jwt-audience "YOUR_CLIENT_ID.apps.googleusercontent.com" \
--jwt-auth-mode identity-only \
--acl-file acl.txtMap JWT claims to broker roles. Claim patterns support Equals, Contains, EndsWith,
StartsWith, Regex, and Any (in JSON: {"EndsWith": "@company.com"} or "Any"):
mqttv5 broker \
--auth-method jwt-federated \
--jwt-issuer "https://auth.example.com" \
--jwt-jwks-uri "https://auth.example.com/.well-known/jwks" \
--jwt-auth-mode claim-binding \
--jwt-role-claim "email" \
--jwt-role-map "@company.com:employee" \
--jwt-default-roles "guest"Trust role claims directly from the IdP (Keycloak, Azure AD). With no trusted role
claims configured, the broker checks roles, groups, and realm_access.roles:
mqttv5 broker \
--auth-method jwt-federated \
--jwt-issuer "https://keycloak.example.com/realms/mqtt" \
--jwt-jwks-uri "https://keycloak.example.com/realms/mqtt/protocol/openid-connect/certs" \
--jwt-fallback-key fallback.pem \
--jwt-auth-mode trusted-roles \
--jwt-trusted-role-claim "realm_access.roles"| Option | Description |
|---|---|
--jwt-jwks-uri |
JWKS endpoint URL |
--jwt-jwks-refresh |
JWKS refresh interval seconds (default 3600) |
--jwt-fallback-key |
Fallback key file when JWKS is unavailable |
--jwt-auth-mode |
identity-only, claim-binding, trusted-roles
|
--jwt-role-claim |
Claim path for role extraction |
--jwt-role-map |
claim_value:role mapping (repeatable) |
--jwt-default-roles |
Default roles (comma-separated) |
--jwt-trusted-role-claim |
Trusted role claim paths (repeatable) |
--jwt-session-scoped-roles |
Clear derived roles on disconnect |
--jwt-issuer-prefix |
Custom prefix for user-ID namespacing |
--jwt-config-file |
JSON config for multi-issuer setups |
{
"issuers": [
{
"name": "corporate",
"issuer": "https://login.corp.example.com",
"key_source": {
"Jwks": {
"uri": "https://login.corp.example.com/.well-known/jwks",
"fallback_key_file": "corp-fallback.pem"
}
},
"auth_mode": "TrustedRoles",
"trusted_role_claims": ["groups"]
},
{
"name": "public",
"issuer": "https://accounts.google.com",
"key_source": {
"Jwks": {
"uri": "https://www.googleapis.com/oauth2/v3/certs",
"fallback_key_file": "google-fallback.pem"
}
},
"audience": "YOUR_CLIENT_ID",
"auth_mode": "IdentityOnly",
"default_roles": ["public-user"]
}
]
}mqttv5 broker --jwt-config-file jwt-config.jsonuser <username> topic <pattern> permission <type>
role <rolename> topic <pattern> permission <type>
assign <username> <rolename>
| Permission | Aliases | Allows |
|---|---|---|
read |
subscribe |
Subscribe only |
write |
publish |
Publish only |
readwrite |
rw, all
|
Both subscribe and publish |
deny |
none |
Explicitly deny access |
- Direct user rules (exact username match) checked first
- Role-based rules next (a
denyin any role overridesallowacross roles) - Wildcard user rules (
user *) checked last - Default applied if nothing matches (deny unless in allow-all mode)
# User-specific rules
user alice topic sensors/# permission readwrite
user bob topic sensors/temperature permission read
# Wildcard user (all users)
user * topic public/# permission read
# Role definitions
role admin topic # permission readwrite
role sensors topic sensors/# permission readwrite
# Role assignments
assign alice admin
assign bob sensors
ACL topic patterns support %u as a placeholder for the authenticated username.
The broker expands %u before matching, so one rule can scope every user to their
own namespace:
user * topic $DB/u/%u/# permission readwrite
When alice@gmail.com publishes to $DB/u/alice@gmail.com/nodes, the pattern
expands to $DB/u/alice@gmail.com/# and matches; publishing to
$DB/u/bob@gmail.com/nodes does not. %u works in both user and role rules:
role db-user topic $DB/u/%u/# permission readwrite
assign alice db-user
Anonymous clients never match %u patterns. Usernames containing MQTT special
characters (+, #, /) are also excluded from expansion to prevent injection.
%u can be combined with MQTT wildcards, e.g.
user * topic devices/%u/+/telemetry/# permission read.
mqttv5 acl add alice "sensors/#" readwrite -f acl.txt
mqttv5 acl remove alice "sensors/#" -f acl.txt
mqttv5 acl list -f acl.txt
mqttv5 acl check alice "sensors/temp" write -f acl.txt
mqttv5 acl role-add admin "#" readwrite -f acl.txt
mqttv5 acl role-remove admin -f acl.txt
mqttv5 acl role-list -f acl.txt
mqttv5 acl assign alice admin -f acl.txt
mqttv5 acl unassign alice admin -f acl.txt
mqttv5 acl user-roles alice -f acl.txtmqttv5 broker \
--auth-password-file passwd.txt \
--acl-file acl.txt \
--allow-anonymous=falseThe broker stamps two MQTT v5 user properties on every PUBLISH before routing, stripping any client-supplied values first (anti-spoofing):
-
x-mqtt-sender— the authenticated username (user_id) of the publisher. -
x-mqtt-client-id— the MQTTclient_idof the immediate publisher.
Anonymous and internal/bridge messages carry no x-mqtt-sender.
These are distinct from x-origin-client-id, an application-layer property set
by intermediaries (e.g. event republishers) to track the original causation client
across republish hops.
When enabled, the broker skips delivering a PUBLISH to a subscriber whose
client_id matches a configurable user-property value on the message — preventing
clients from receiving their own messages when routed through an intermediary.
The default property key is x-origin-client-id (not x-mqtt-client-id,
because an intermediary republishing on behalf of the originator would set
x-mqtt-client-id to its own ID). The key is hot-reloadable via SIGHUP.
{
"echo_suppression_config": {
"enabled": true,
"property_key": "x-origin-client-id"
}
}The primary broker auth provider wraps PasswordAuthProvider + AclManager into a
single provider handling both authentication and authorization. Factory methods
include from_files(), with_password_file_and_allow_all_acl(), and
with_providers(). Both password and ACL files support live reloading.
Chains a primary and a fallback provider. If the primary returns
BadAuthenticationMethod, the fallback is tried; other rejections (e.g.
NotAuthorized) are final. Obtain the broker's built provider via
broker.auth_provider() and install a composite with broker.with_auth_provider():
use mqtt5::broker::auth::{AuthorizationMode, CompositeAuthProvider, PasswordAuthProvider};
use std::sync::Arc;
let primary = broker.auth_provider();
let fallback = Arc::new(PasswordAuthProvider::new());
let composite = CompositeAuthProvider::new(primary, fallback)
.with_authorization_mode(AuthorizationMode::Or);
let broker = broker.with_auth_provider(Arc::new(composite));AuthorizationMode controls how publish/subscribe checks combine:
| Mode | Behavior |
|---|---|
PrimaryOnly (default) |
Only the primary provider's authorization is checked |
Or |
Allowed if either provider authorizes |
And |
Allowed only if both providers authorize |
-
Session-to-user binding: sessions store the authenticated
user_id. On reconnect withclean_start=false, a mismatched user is rejected withNotAuthorized, preventing session hijacking. - ACL re-check on restore: restored subscriptions are re-authorized against the current ACL; ones that no longer pass are silently pruned.
CertificateAuthProvider validates TLS peer certificate fingerprints (64-char hex
SHA-256, case-insensitive). Client IDs starting with cert: are matched against
registered fingerprints from the actual TLS connection. The broker rejects cert:
client IDs at the transport layer unless the connection presents a verified TLS
client certificate, preventing spoofing over plain TCP, WebSocket, or QUIC.
Authentication rate limiting is enabled by default and tracks failed attempts by both IP address and username independently. A successful authentication clears the counter.
| Setting | Default | Description |
|---|---|---|
max_attempts |
5 | Failed attempts before lockout |
window_secs |
60 | Time window for counting |
lockout_secs |
300 | Lockout duration (5 minutes) |
use mqtt5::broker::config::{AuthConfig, RateLimitConfig};
let auth = AuthConfig {
rate_limit: RateLimitConfig {
enabled: true,
max_attempts: 5,
window_secs: 60,
lockout_secs: 300,
},
..Default::default()
};- Always use TLS in production to encrypt credentials in transit.
- Set file permissions to 0600 for password, SCRAM, and ACL files.
- Use SCRAM or JWT instead of plain passwords where possible.
- Rate limiting is on by default — keep it enabled.
-
Scope users with
%uand rely on the broker-stampedx-mqtt-sender/x-mqtt-client-idproperties rather than trusting client-supplied identity. - Use mTLS for IoT devices that can't store passwords.
- Rotate JWT keys regularly; JWKS endpoints must use HTTPS; keep token expiry short (default clock-skew tolerance is 60s).
Getting Started
Broker Guide
Client Guide
Platform Guides
CLI Reference
Development