Sécurité MQTT

Chiffrement de bout en bout du payload MQTT avec AES-GCM

TLS protège les messages MQTT en transit, mais le broker voit toujours le texte en clair. Découvrez comment ajouter un chiffrement de bout en bout du payload MQTT avec AES-GCM en Python, et comment gérer clés, nonces et topics en toute sécurité.

Illustration du chiffrement du payload MQTT avec une enveloppe scellée traversant un broker d'un capteur jusqu'à une application
Essayez-le sur votre propre broker MQTTObtenez un broker MQTT privé et managé avec l’offre Free et partez d’exemples de code prêts à l’emploi.
Essayer gratuitement Exemples de code Aucune carte bancaire requise

Le chiffrement du payload MQTT consiste à chiffrer le corps du message lui-même, afin que seuls l'éditeur et les abonnés prévus puissent le lire, quel que soit l'opérateur du broker. Dans ce guide, vous verrez quand un chiffrement de bout en bout vaut la peine d'être ajouté par-dessus TLS, comment concevoir le format de message en toute sécurité et comment l'implémenter en Python avec AES-GCM sur une vraie connexion à un broker.

TLS ou chiffrement du payload MQTT

TLS chiffre la connexion entre chaque client et le broker. C'est indispensable et vous devez toujours l'utiliser ; consultez notre guide de configuration TLS MQTT. Mais TLS s'arrête au broker. À l'intérieur du broker, un message est en clair : il doit l'être pour que le broker puisse le router, le stocker comme retained message ou le mettre en file d'attente pour une session hors ligne.

Le chiffrement du payload ajoute une seconde couche qui voyage avec le message, de l'éditeur à l'abonné.

Propriété TLS seul TLS + chiffrement du payload
Protégé sur le réseau Oui Oui
Lisible par l'opérateur du broker Oui Non
Lisible dans les messages retenus ou en file d'attente au repos Oui Non
Noms de topics masqués Non (visibles par le broker) Non (toujours visibles par le broker)
Le broker peut inspecter ou décoder les payloads Oui Non
Gestion de clés nécessaire Certificat du broker uniquement Clés applicatives sur chaque client

Remarquez les deux lignes « Non » que l'on oublie souvent. Les topics restent lisibles, car le broker en a besoin pour le routage : ne mettez donc jamais de secrets dans les noms de topics. Et toute fonctionnalité du broker qui lit les payloads cesse de fonctionner pour les messages chiffrés. Par exemple, l'explorateur de topics en direct de ConnectMQ, les règles d'alerte à seuil et le décodage Sparkplug B ne peuvent afficher ou évaluer que les données qu'ils savent analyser ; les payloads chiffrés y apparaîtront donc comme des octets opaques.

Quand le chiffrement de bout en bout a du sens

Ajoutez le chiffrement du payload lorsque :

  • Des réglementations ou des contrats exigent que les fournisseurs d'infrastructure ne puissent pas lire les données.
  • Les messages transitent par des brokers, ponts ou passerelles partagés ou publics que vous ne maîtrisez pas entièrement.
  • Des retained messages ou des sessions en file d'attente contiennent des valeurs sensibles, comme des données personnelles ou des codes d'accès.
  • Vous voulez une défense en profondeur au cas où les identifiants d'une partie du système fuiteraient.

Passez-vous-en lorsque vous avez besoin de visibilité côté broker (tableaux de bord dans le portail du broker, règles d'alerte sur des valeurs) et que vous faites confiance à l'opérateur, ou lorsque des appareils contraints ne peuvent pas se permettre le code et la gestion de clés supplémentaires. Beaucoup d'équipes ne chiffrent que quelques topics sensibles, comme .../commands/unlock, et laissent la télémétrie en clair via TLS.

Choisir un algorithme

Utilisez un algorithme de chiffrement authentifié. Il chiffre le payload et ajoute une étiquette (tag) qui détecte toute modification. Deux bons choix :

  • AES-256-GCM : largement disponible, accéléré matériellement sur de nombreux processeurs et sur l'ESP32 (via mbedTLS). Exige un nonce unique de 96 bits par message sous une même clé.
  • XChaCha20-Poly1305 (libsodium crypto_aead_xchacha20poly1305_ietf) : rapide en logiciel, et son nonce de 192 bits peut être généré aléatoirement sans risque réaliste de collision.

N'utilisez pas AES-CBC sans MAC, le mode ECB sous quelque forme que ce soit, ni un « chiffrement » par XOR ou Base64. Ils donnent un faux sentiment de sécurité.

Concevoir le format du message chiffré

Un format simple et robuste pour AES-GCM :

| version (1 byte) | nonce (12 bytes) | ciphertext + 16-byte tag |

Trois règles de conception comptent davantage que l'algorithme :

  1. Ne jamais réutiliser un nonce avec la même clé. Avec AES-GCM, la réutilisation peut exposer le texte en clair et permettre des falsifications. Un nonce aléatoire de 12 octets est acceptable pour des volumes de messages modérés par clé ; renouvelez périodiquement les clés des éditeurs à fort volume.
  2. Lier le texte chiffré à son topic. Passez le topic comme données authentifiées additionnelles (AAD). Sinon, un attaquant ayant accès au broker pourrait rejouer un message « unlock » chiffré sur le topic d'un autre appareil.
  3. Inclure un horodatage ou un compteur dans le texte en clair pour que les abonnés puissent rejeter les anciens messages rejoués plus tard.

Exemple Python : publication et abonnement chiffrés

L'exemple ci-dessous utilise paho-mqtt 2.x et le paquet cryptography. Installez-les d'abord :

pip install "paho-mqtt>=2.0" cryptography

Générez une clé une seule fois et partagez-la avec l'éditeur et l'abonné via votre coffre de secrets ou votre environnement, pas via MQTT :

python -c "import os,base64;print(base64.b64encode(os.urandom(32)).decode())"

Enregistrez le code suivant sous secure_mqtt.py :

import base64
import json
import os
import sys
import time

import paho.mqtt.client as mqtt
from cryptography.hazmat.primitives.ciphers.aead import AESGCM
from cryptography.exceptions import InvalidTag

BROKER = "broker.connectmq.com"
PORT = 8883
TOPIC = "connectmq/demo/e2e-91c4/door/command"
VERSION = b"\x01"
MAX_AGE_SECONDS = 30

KEY = base64.b64decode(os.environ["MQTT_PAYLOAD_KEY"])
aead = AESGCM(KEY)


def encrypt(topic: str, data: dict) -> bytes:
    data["ts"] = int(time.time())
    plaintext = json.dumps(data).encode()
    nonce = os.urandom(12)
    ciphertext = aead.encrypt(nonce, plaintext, topic.encode())
    return VERSION + nonce + ciphertext


def decrypt(topic: str, blob: bytes) -> dict:
    if len(blob) < 1 + 12 + 16 or blob[:1] != VERSION:
        raise ValueError("unknown message format")
    nonce, ciphertext = blob[1:13], blob[13:]
    plaintext = aead.decrypt(nonce, ciphertext, topic.encode())
    data = json.loads(plaintext)
    if abs(time.time() - data["ts"]) > MAX_AGE_SECONDS:
        raise ValueError("stale or replayed message")
    return data


def on_connect(client, userdata, flags, reason_code, properties):
    print("Connected:", reason_code)
    if userdata == "sub":
        client.subscribe(TOPIC, qos=1)


def on_message(client, userdata, msg):
    try:
        print("Decrypted:", decrypt(msg.topic, msg.payload))
    except (InvalidTag, ValueError) as exc:
        print("Rejected message:", exc)


def make_client(role: str) -> mqtt.Client:
    client = mqtt.Client(
        mqtt.CallbackAPIVersion.VERSION2,
        client_id=f"e2e-{role}-91c4",
        userdata=role,
    )
    client.tls_set()  # TLS protège toujours les identifiants et les métadonnées
    # Broker ConnectMQ privé :
    # client.username_pw_set("YOUR_USERNAME", "YOUR_PASSWORD")
    client.on_connect = on_connect
    client.on_message = on_message
    client.connect(BROKER, PORT, keepalive=60)
    return client


if __name__ == "__main__":
    role = sys.argv[1] if len(sys.argv) > 1 else "sub"
    client = make_client(role)
    if role == "pub":
        client.loop_start()
        time.sleep(1)
        payload = encrypt(TOPIC, {"action": "unlock", "door": "lab-2"})
        client.publish(TOPIC, payload, qos=1).wait_for_publish()
        print("Published", len(payload), "encrypted bytes")
        client.loop_stop()
        client.disconnect()
    else:
        client.loop_forever()

Lancez l'abonné et l'éditeur dans deux terminaux avec la même clé :

export MQTT_PAYLOAD_KEY="paste-the-base64-key-here"
python secure_mqtt.py sub      # terminal 1
python secure_mqtt.py pub      # terminal 2

Si vous vous abonnez au même topic avec mosquitto_sub, vous ne verrez que du bruit binaire, et c'est tout l'intérêt. Modifiez un octet d'un payload, publiez-le sur un autre topic ou attendez plus de 30 secondes avant de le déchiffrer, et l'abonné le rejette. Comme l'exemple utilise le serveur MQTT public gratuit, réservé aux tests, n'importe qui peut voir le texte chiffré, mais personne sans la clé ne peut le lire ni le falsifier.

La gestion des clés : la partie difficile

Le code de chiffrement est court ; c'est la gestion des clés qui fait réussir ou échouer les projets.

  1. Restreignez la portée des clés. Utilisez une clé par appareil ou par groupe d'appareils, pas une seule clé pour toute la flotte. Une clé divulguée doit exposer le moins possible.
  2. Stockez les clés de façon sécurisée sur les appareils. Utilisez une flash chiffrée ou un élément sécurisé sur les microcontrôleurs, et un magasin de clés du système ou un gestionnaire de secrets sur les serveurs. Notre guide sur le stockage des identifiants MQTT sur les appareils s'applique tout autant aux clés de chiffrement.
  3. Incluez un identifiant de clé. Étendez l'en-tête avec un identifiant de clé pour que les abonnés choisissent la bonne clé et que vous puissiez effectuer une rotation sans interruption.
  4. Renouvelez les clés. Introduisez une nouvelle clé, laissez les abonnés accepter les deux, basculez les éditeurs, puis retirez l'ancienne clé.
  5. Envisagez des schémas asymétriques pour les flux plusieurs-vers-un. Avec les sealed boxes de libsodium, des milliers d'appareils peuvent chiffrer vers la clé publique d'un backend, seul ce dernier détenant la clé privée.

Appareils embarqués

Sur un ESP32, mbedTLS (déjà présent pour TLS) fournit mbedtls_gcm_crypt_and_tag et mbedtls_gcm_auth_decrypt, si bien que le chiffrement du payload ajoute peu de code. Utilisez le générateur de nombres aléatoires matériel (esp_fill_random) pour les nonces, et assurez-vous que le Wi-Fi ou le Bluetooth fonctionne, ou qu'une source d'entropie adéquate est activée, pour que la sortie du RNG soit réellement aléatoire.

Erreurs d'implémentation courantes

En relisant du code de chiffrement de payload dans des projets IoT, on retrouve sans cesse les mêmes erreurs :

  • Un nonce fixe ou un compteur qui se réinitialise au redémarrage. Un appareil qui repart de zéro après chaque coupure d'alimentation réutilisera des nonces. Utilisez des nonces aléatoires, ou persistez le compteur en flash.
  • Ignorer les échecs de déchiffrement. Un abonné qui journalise une erreur InvalidTag puis traite quand même le message n'a aucune protection. Traitez tout échec comme un message rejeté.
  • Compresser après avoir chiffré. Le texte chiffré ne se compresse pas : compressez d'abord le JSON si la taille compte.
  • Mélanger les formats sans octet de version. Quand vous changez d'algorithme ou ajoutez un identifiant de clé, les anciens et les nouveaux appareils doivent pouvoir être distingués. L'octet de version de l'exemple existe précisément pour cela.
  • Mettre la clé dans le même dépôt que le code. Les clés ont leur place dans un coffre de secrets, des variables d'environnement ou un stockage sécurisé sur l'appareil, jamais dans Git.

Testez délibérément les cas négatifs : publiez un message tronqué, un message avec un bit inversé et un ancien message rejoué, et vérifiez que chacun est rejeté.

Le chiffrement ne remplace pas la sécurité du broker

Le chiffrement du payload protège la confidentialité et l'intégrité des données, mais il n'empêche personne de se connecter, d'inonder des topics ou de s'abonner aux métadonnées. Vous avez toujours besoin de TLS, d'identifiants robustes par appareil et de protections côté broker. Un broker MQTT managé comme ConnectMQ fournit TLS, des identifiants liés aux client IDs, des quotas par offre, une protection contre l'inondation et un blocage d'IP après des échecs de connexion répétés, tandis que le chiffrement de bout en bout garde les données elles-mêmes privées. Ensemble, ils couvrent la checklist de nos bonnes pratiques de sécurité MQTT.

Démarrer avec ConnectMQ

Créez un broker gratuit sur app.connectmq.com avec l'offre Serverless, sans carte bancaire, et lancez l'exemple ci-dessus avec vos propres identifiants sur le port 8883. ConnectMQ gère TLS, l'authentification et la montée en charge, tandis que votre application garde les clés du payload.

Questions fréquentes

MQTT intègre-t-il un chiffrement du payload ?

Non. MQTT 3.1.1 et 5.0 ne définissent aucun chiffrement du payload ; les payloads sont des octets opaques. Le chiffrement est assuré par TLS sur la connexion et, en option, par votre application sur le payload.

Peut-on aussi chiffrer le nom du topic ?

Pas sans casser le routage, car le broker associe les abonnements aux noms de topics. Gardez les topics exempts d'informations sensibles et utilisez des identifiants d'appareils opaques plutôt que des noms ou des adresses.

Le chiffrement du payload augmente-t-il la taille des messages ?

Légèrement. Le format AES-GCM ci-dessus ajoute 29 octets par message (version, nonce et tag), plus le surcoût d'encodage si vous convertissez le résultat en Base64.

Faut-il encore utiliser TLS si les payloads sont chiffrés ?

Oui. TLS protège vos identifiants MQTT, vos client IDs, vos noms de topics et les métadonnées du protocole, et authentifie le broker, ce que le chiffrement du payload ne couvre pas.

Articles liés

Rotation des identifiants MQTT sans interruption de service

Une stratégie à double identifiant, étape par étape, pour la rotation des noms d'utilisateur et mots de passe MQTT sur un parc : préparer le nouvel identifiant, basculer avec repli, vérifier, puis seulement révoquer l'ancien.

8 min de lecture

Tous les articles →

Votre broker MQTT privé, à quelques clics

Créez un compte, récupérez les identifiants de votre broker et connectez votre premier appareil — aucun serveur à installer, mettre à jour ou dimensionner.