Pour tester des ACL MQTT, connectez-vous avec l'utilisateur à vérifier, tentez chaque publish et subscribe que l'appareil doit et ne doit pas pouvoir faire, et vérifiez le résultat depuis l'extérieur du client. Ce dernier point est essentiel : en MQTT 3.1.1, un publish refusé ressemble exactement à un publish réussi, donc la seule preuve fiable est un second client qui observe si le message est arrivé.
Ce guide explique comment chaque version du protocole signale un refus, propose une matrice de tests de permissions, des vérifications manuelles avec mosquitto et un test automatisé à lancer après chaque modification des ACL.
Comment les brokers signalent une action refusée
Les ACL (listes de contrôle d'accès) déterminent sur quels topics un utilisateur peut publier (écriture) et auxquels il peut s'abonner (lecture). À ce stade, l'authentification a déjà réussi — le CONNACK valait 0x00 —, donc tout retour arrive dans des paquets ultérieurs.
| Action | MQTT 3.1.1 | MQTT 5 |
|---|---|---|
| Publish QoS 0, refusé | Message jeté, aucun retour | Message jeté, aucun retour (le broker peut déconnecter avec 0x87) |
| Publish QoS 1, refusé | PUBACK normal et message jeté — ou connexion fermée | PUBACK avec le reason code 0x87 Not authorized |
| Publish QoS 2, refusé | PUBREC normal et message jeté — ou connexion fermée | PUBREC avec le reason code 0x87 |
| Subscribe, refusé | SUBACK avec le return code 0x80 (Failure) | SUBACK avec le reason code 0x87 (ou 0x80 Unspecified error) |
| Subscribe avec wildcard couvrant des topics refusés | Selon le broker : refusé, ou accepté et filtré message par message | Idem |
Le comportement de 3.1.1 n'est pas un bug de votre broker. La spécification indique que lorsqu'un serveur n'autorise pas un PUBLISH, il n'a aucun moyen d'en informer le client et doit soit l'acquitter selon les règles QoS habituelles, soit fermer la connexion réseau. Beaucoup de clients ne journalisent rien dans aucun des deux cas, ce qui explique pourquoi « mes messages disparaissent » est un symptôme d'ACL si courant — voir ACL MQTT refusée en silence.
Construire une matrice de tests de permissions
Listez ce qu'un utilisateur représentatif doit pouvoir faire, puis ajoutez ce qu'il ne doit pas faire. Voici un exemple pour un capteur autorisé à publier sa propre télémétrie et à lire ses propres commandes, avec l'ACL Mosquitto correspondante :
# every device: own telemetry and own commands only
pattern write testmqtt/acl/telemetry/%u/#
pattern read testmqtt/acl/commands/%u/#
# a monitoring user that may read everything under testmqtt/acl
user acl-observer
topic read testmqtt/acl/#| # | L'utilisateur sensor-01 fait | Attendu |
|---|---|---|
| 1 | Publish sur testmqtt/acl/telemetry/sensor-01/temp | Livré |
| 2 | Publish sur testmqtt/acl/telemetry/sensor-02/temp (autre appareil) | Refusé, non livré |
| 3 | Publish sur testmqtt/acl/commands/sensor-01/reboot (son propre topic de commandes) | Refusé — lecture seule |
| 4 | Subscribe à testmqtt/acl/commands/sensor-01/# | Accordé |
| 5 | Subscribe à testmqtt/acl/commands/sensor-02/# | Échec dans le SUBACK |
| 6 | Subscribe à testmqtt/acl/# | Refusé, ou accordé mais seules ses commandes sont livrées |
| 7 | Subscribe à # et $SYS/# | Refusé, ou rien hors de ses topics n'est livré |
| 8 | Recevoir un message retenu sur le topic de commandes d'un autre appareil | Jamais livré |
| 9 | Publish sur testmqtt/acl/telemetry/sensor-01 (sans niveau final) | Selon votre intention — vérifiez le pattern |
Le cas 9 est celui où les patterns surprennent : telemetry/%u/# correspond aussi à telemetry/sensor-01 lui-même, car # inclut le niveau parent. Collez vos patterns et topics dans le topic matcher pour vérifier les cas limites avant d'écrire le test. Le cas 8 compte, car les messages retenus sont livrés au moment de l'abonnement ; un broker qui oublie d'y appliquer l'ACL de lecture divulgue la dernière valeur de chaque topic. Pour d'autres pièges, voir sécurité des wildcards MQTT.
Vérifications manuelles avec mosquitto
Ouvrez d'abord un observateur. Il utilise un compte disposant d'un large accès en lecture et montre donc ce qui atteint réellement le broker :
mosquitto_sub -h broker.connectmq.com -p 1883 \
-u acl-observer -P "$OBSERVER_PASS" -t 'testmqtt/acl/#' -vPubliez maintenant en tant qu'appareil testé, en MQTT 5 et QoS 1, pour qu'un refus produise un reason code :
# cas 1 : doit apparaître chez l'observateur
mosquitto_pub -h broker.connectmq.com -p 1883 -V mqttv5 -q 1 \
-u sensor-01 -P "$MQTT_PASS" -t testmqtt/acl/telemetry/sensor-01/temp -m 21.5
# cas 2 : ne doit PAS apparaître ; MQTT 5 signale le refus
mosquitto_pub -h broker.connectmq.com -p 1883 -V mqttv5 -q 1 \
-u sensor-01 -P "$MQTT_PASS" -t testmqtt/acl/telemetry/sensor-02/temp -m 99Pour le publish refusé, le client mosquitto affiche un avertissement du type Warning: Publish 1 failed: Not authorized. Lancez la même commande avec -V mqttv311 : elle se termine proprement, sans avertissement — le silence de l'observateur est votre seule preuve.
Les abonnements sont plus faciles à vérifier, car les deux versions répondent dans le SUBACK :
mosquitto_sub -h broker.connectmq.com -p 1883 -V mqttv5 -d \
-u sensor-01 -P "$MQTT_PASS" -t 'testmqtt/acl/commands/sensor-02/#'Avec -d, vous voyez Subscribed (mid: 1): 135 pour un refus en MQTT 5 (0x87) ou Subscribed (mid: 1): 128 (0x80) en 3.1.1. Les versions récentes de mosquitto affichent ensuite All subscription requests were denied. et se terminent. Si votre broker accorde l'abonnement, publiez sur ce topic depuis un autre compte et vérifiez que rien n'est livré. Les codes sont détaillés dans SUBACK 0x80 et Not authorized 0x87.
Vous pouvez aussi le faire de manière interactive dans le client MQTT en ligne : ouvrez deux sessions avec des utilisateurs différents, abonnez-vous en tant qu'observateur dans l'une et publiez en tant qu'appareil dans l'autre.
Un test d'ACL automatisé
Ce script paho-mqtt 2.x exécute les cas de publish et de subscribe de la matrice. Il vérifie les reason codes MQTT 5 et utilise l'observateur comme référence, il fonctionne donc aussi avec les brokers qui jettent silencieusement les messages refusés :
import os, threading, time, uuid
import paho.mqtt.client as mqtt
HOST, PORT = "broker.connectmq.com", 1883
DEVICE = ("sensor-01", os.environ["MQTT_PASS"])
OBSERVER = ("acl-observer", os.environ["OBSERVER_PASS"])
BASE = "testmqtt/acl"
def client(creds):
c = mqtt.Client(mqtt.CallbackAPIVersion.VERSION2,
client_id=f"acl-{uuid.uuid4().hex[:8]}", protocol=mqtt.MQTTv5)
c.username_pw_set(*creds)
c.connect(HOST, PORT)
c.loop_start()
time.sleep(0.5)
return c
seen = set()
obs = client(OBSERVER)
obs.on_message = lambda c, u, m: seen.add(m.topic)
obs.subscribe(f"{BASE}/#", qos=1)
dev = client(DEVICE)
acks = {}
dev.on_publish = lambda c, u, mid, rc, props: acks.__setitem__(mid, str(rc))
subacks = {}
dev.on_subscribe = lambda c, u, mid, rcs, props: subacks.__setitem__(mid, [str(r) for r in rcs])
def check_publish(topic, allowed):
info = dev.publish(topic, "acl-test", qos=1)
time.sleep(1)
delivered = topic in seen
print(f"PUB {topic}: puback={acks.get(info.mid)} delivered={delivered}")
assert delivered == allowed, f"publish to {topic}: expected allowed={allowed}"
if not allowed:
assert acks.get(info.mid) == "Not authorized"
def check_subscribe(topic, allowed):
_, mid = dev.subscribe(topic, qos=1)
time.sleep(1)
granted = not subacks[mid][0].startswith(("Not authorized", "Unspecified"))
print(f"SUB {topic}: suback={subacks[mid]}")
assert granted == allowed, f"subscribe to {topic}: expected allowed={allowed}"
check_publish(f"{BASE}/telemetry/sensor-01/temp", True)
check_publish(f"{BASE}/telemetry/sensor-02/temp", False)
check_publish(f"{BASE}/commands/sensor-01/reboot", False)
check_subscribe(f"{BASE}/commands/sensor-01/#", True)
check_subscribe(f"{BASE}/commands/sensor-02/#", False)
print("all ACL checks passed")Lancez-le après chaque modification du fichier d'ACL ou du backend d'authentification, et en CI contre un broker de staging. Quand une vérification échoue, le reason code affiché et l'indicateur delivered vous disent immédiatement si le broker a rejeté le message, l'a jeté en silence ou l'a laissé passer.
Causes fréquentes d'échec d'un test d'ACL
- Tout est refusé. Le fichier d'ACL n'est pas chargé, ou le nom d'utilisateur de la règle ne correspond pas à celui du
CONNECT. Consultez le log du broker au démarrage ; avec Mosquitto,acl_filedoit être dans la même portée de listener que le fichier de mots de passe — voir authentification Mosquitto. - Tout est autorisé. Une règle large comme
topic readwrite #plus haut dans le fichier, ou un accès anonyme avec ses propres règles permissives. - Les règles
patternne correspondent pas.%uest le nom d'utilisateur et%cle client ID ; utiliser le mauvais refuse tous les appareils dont l'ID diffère du nom d'utilisateur. - Les modifications n'ont aucun effet. La plupart des brokers lisent les ACL au démarrage ou au rechargement. Envoyez
SIGHUPà Mosquitto, ou redémarrez-le, avant de relancer le test. - Le test passe mais la production échoue. Le client publie avec une barre oblique initiale (
/testmqtt/...) ou une casse différente. Les topics sont sensibles à la casse et/an'est pasa.
Observez le trafic du test d'ACL dans le client en ligne
Une ACL est un contrôle de sécurité, et un contrôle de sécurité a besoin de tests qui échouent quand il casse. Tenez une matrice des actions autorisées et refusées par rôle, vérifiez la livraison avec un observateur plutôt que de faire confiance à l'émetteur, et préférez MQTT 5 pour que les refus soient signalés au lieu d'être avalés.
Questions fréquentes
Pourquoi mon publish MQTT refusé ne renvoie-t-il aucune erreur ?
MQTT 3.1.1 n'a aucun moyen d'indiquer au client qu'un publish a été refusé. Le broker l'acquitte normalement et jette le message, ou ferme la connexion. Utilisez MQTT 5 pour recevoir un PUBACK avec le reason code 0x87, ou vérifiez avec un second abonné.
Que signifie SUBACK 0x80 ?
0x80 est le code d'échec d'un SUBACK. En MQTT 3.1.1, c'est la seule façon de signaler un abonnement refusé ; en MQTT 5, il signifie Unspecified error, et les brokers envoient généralement 0x87 Not authorized pour les refus d'ACL.
Un abonnement avec wildcard contourne-t-il les ACL de topics ?
Il ne devrait pas. Selon le broker, un abonnement à un filtre comme # est soit refusé, soit accepté avec un contrôle de chaque message par l'ACL de lecture avant livraison. Testez les deux : le code du SUBACK et les messages réellement reçus.

