Skip to content

Authentication and ACL

Fabrício Bracht edited this page Jul 3, 2026 · 1 revision

Authentication and ACL

Secure your broker with authentication (identity verification), authorization (access control), role grouping, and tamper-resistant publisher identity.


Overview

┌─────────────────────────────────────────────────────────────────┐
│                     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.


Authentication Methods

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

Anonymous Access

Allow connections without credentials (development only):

mqttv5 broker --allow-anonymous

Password Authentication

Passwords are stored with Argon2id hashing, excluded from log output, and files should be mode 0600.

Create / Manage Password File

# 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 alice

Password file format: username:argon2id_hash (one per line). Lines starting with # are comments.

Start Broker with Password Auth

mqttv5 broker \
  --auth-password-file passwd.txt \
  --allow-anonymous=false

PLAIN SASL

RFC 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.txt

SCRAM-SHA-256

Challenge-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.txt

SCRAM 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.


JWT Authentication

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.

Single Issuer (HS256)

mqttv5 broker \
  --auth-method jwt \
  --jwt-algorithm hs256 \
  --jwt-key-file secret.key \
  --jwt-issuer "https://auth.example.com"

Single Issuer (RS256 / ES256)

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 60

Federated JWT

Support 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.

Identity-Only Mode

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.txt

Claim-Binding Mode

Map 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"

Trusted-Roles Mode

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"

Federated JWT Options

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

Multi-Issuer (JSON Config)

{
  "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.json

Authorization (ACL)

ACL File Format

user <username> topic <pattern> permission <type>
role <rolename> topic <pattern> permission <type>
assign <username> <rolename>

Permission Types

Permission Aliases Allows
read subscribe Subscribe only
write publish Publish only
readwrite rw, all Both subscribe and publish
deny none Explicitly deny access

Permission Evaluation Order

  1. Direct user rules (exact username match) checked first
  2. Role-based rules next (a deny in any role overrides allow across roles)
  3. Wildcard user rules (user *) checked last
  4. Default applied if nothing matches (deny unless in allow-all mode)

Example ACL File

# 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

Username Substitution (%u)

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.

CLI Management

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.txt

Start Broker with ACL

mqttv5 broker \
  --auth-password-file passwd.txt \
  --acl-file acl.txt \
  --allow-anonymous=false

Sender Identity Injection

The 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 MQTT client_id of 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.


Echo Suppression

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"
  }
}

Auth Provider Architecture

ComprehensiveAuthProvider

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.

CompositeAuthProvider

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 Security

  • Session-to-user binding: sessions store the authenticated user_id. On reconnect with clean_start=false, a mismatched user is rejected with NotAuthorized, 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.

Certificate Authentication (mTLS)

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.


Rate Limiting

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()
};

Security Best Practices

  1. Always use TLS in production to encrypt credentials in transit.
  2. Set file permissions to 0600 for password, SCRAM, and ACL files.
  3. Use SCRAM or JWT instead of plain passwords where possible.
  4. Rate limiting is on by default — keep it enabled.
  5. Scope users with %u and rely on the broker-stamped x-mqtt-sender / x-mqtt-client-id properties rather than trusting client-supplied identity.
  6. Use mTLS for IoT devices that can't store passwords.
  7. Rotate JWT keys regularly; JWKS endpoints must use HTTPS; keep token expiry short (default clock-skew tolerance is 60s).

Clone this wiki locally