Tests de brokers

Comment tester les ACL MQTT et les permissions de topics

Vérifiez vos ACL MQTT topic par topic : pourquoi un publish refusé est silencieux en MQTT 3.1.1, le code 0x87 en MQTT 5, SUBACK 0x80 et un test automatisé.

Mis à jour · 9 min de lecture

Comment tester les ACL MQTT et les permissions de topics

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.

ActionMQTT 3.1.1MQTT 5
Publish QoS 0, refuséMessage jeté, aucun retourMessage jeté, aucun retour (le broker peut déconnecter avec 0x87)
Publish QoS 1, refuséPUBACK normal et message jeté — ou connexion ferméePUBACK avec le reason code 0x87 Not authorized
Publish QoS 2, refuséPUBREC normal et message jeté — ou connexion ferméePUBREC 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ésSelon le broker : refusé, ou accepté et filtré message par messageIdem

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 :

acl_file (Mosquitto)
# 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 faitAttendu
1Publish sur testmqtt/acl/telemetry/sensor-01/tempLivré
2Publish sur testmqtt/acl/telemetry/sensor-02/temp (autre appareil)Refusé, non livré
3Publish sur testmqtt/acl/commands/sensor-01/reboot (son propre topic de commandes)Refusé — lecture seule
4Subscribe à testmqtt/acl/commands/sensor-01/#Accordé
5Subscribe à testmqtt/acl/commands/sensor-02/#Échec dans le SUBACK
6Subscribe à testmqtt/acl/#Refusé, ou accordé mais seules ses commandes sont livrées
7Subscribe à # et $SYS/#Refusé, ou rien hors de ses topics n'est livré
8Recevoir un message retenu sur le topic de commandes d'un autre appareilJamais livré
9Publish 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 :

Terminal 1 — observateur
mosquitto_sub -h broker.connectmq.com -p 1883 \
  -u acl-observer -P "$OBSERVER_PASS" -t 'testmqtt/acl/#' -v

Publiez maintenant en tant qu'appareil testé, en MQTT 5 et QoS 1, pour qu'un refus produise un reason code :

Terminal 2 — publish autorisé et refusé
# 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 99

Pour 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 :

Abonnement refusé
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 :

test_acl.py
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_file doit ê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 pattern ne correspondent pas. %u est le nom d'utilisateur et %c le 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 /a n'est pas a.

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.