Tutoriels

MQTT sur TLS en Python (CA, certificats client)

Connecter paho-mqtt à un broker MQTT en TLS : options de tls_set(), fichiers CA privés, TLS mutuel avec certificats client et corrections des erreurs SSL courantes.

Mis à jour · 9 min de lecture

MQTT sur TLS en Python (CA, certificats client)

Pour connecter paho-mqtt en TLS, appelez client.tls_set() avant connect() et utilisez le port TLS du broker, généralement 8883. Sans argument, paho 2.x vérifie le certificat du broker avec les CA de confiance de votre système et contrôle le nom d'hôte, exactement ce qu'il faut pour un broker doté d'un certificat public. Pour une CA privée, passez ca_certs= ; pour le TLS mutuel, ajoutez certfile= et keyfile=.

Ce guide détaille chaque option de tls_set(), un exemple complet de TLS mutuel et les erreurs SSL exactes levées par paho, avec la façon de les confirmer et de les corriger.

La connexion TLS minimale

tls_minimal.py
import os
import time

import paho.mqtt.client as mqtt


def on_connect(client, userdata, flags, reason_code, properties):
    print("CONNACK:", reason_code)
    client.subscribe("testmqtt/tls-demo/#")


def on_message(client, userdata, msg):
    print(msg.topic, msg.payload.decode())


client = mqtt.Client(mqtt.CallbackAPIVersion.VERSION2, client_id="tls-demo-1")
client.username_pw_set(os.environ["MQTT_USER"], os.environ["MQTT_PASS"])
client.tls_set()  # magasin CA système, TLS 1.2+, vérification du nom d'hôte
client.on_connect = on_connect
client.on_message = on_message

client.connect("broker.connectmq.com", 8883, keepalive=60)
client.loop_start()
client.publish("testmqtt/tls-demo/hello", "encrypted hello", qos=1)
time.sleep(2)
client.loop_stop()
client.disconnect()

Deux règles évitent la plupart des problèmes :

  • Connectez-vous avec le nom d'hôte du certificat, pas avec une adresse IP. paho envoie l'hôte passé à connect() comme SNI et le compare au certificat.
  • Faites correspondre port et protocole. TLS va sur le 8883 (ou le 8084 pour les WebSockets sécurisés) ; le TCP en clair va sur le 1883. Les mélanger produit des erreurs déroutantes, détaillées plus bas. Voir ports MQTT pour la liste complète.

Les options de tls_set()

ArgumentValeur par défaut dans paho 2.xÀ utiliser quand
ca_certsNone → magasin de confiance du systèmeLe broker utilise une CA privée ou auto-signée : chemin du PEM de la CA
certfileNoneLe broker exige des certificats client (TLS mutuel)
keyfileNoneClé privée de certfile, si elle n'est pas dans le même fichier
keyfile_passwordNoneLa clé est chiffrée ; sinon paho peut la demander dans le terminal
cert_reqsssl.CERT_REQUIREDPresque jamais à changer ; ssl.CERT_NONE désactive la vérification
tls_versionssl.PROTOCOL_TLS_CLIENT (négocie la version la plus haute)Rarement ; préférez fixer une version minimale sur votre propre contexte
ciphersValeurs par défaut d'OpenSSLVous devez respecter une politique de chiffrement précise
alpn_protocolsNoneLe point d'accès route selon l'ALPN, par exemple MQTT sur le port 443

Deux méthodes associées complètent le tableau :

  • tls_insecure_set(True) conserve la vérification de la chaîne mais saute la vérification du nom d'hôte. Elle doit être appelée après tls_set(), sinon paho lève ValueError: Must configure SSL context before using tls_insecure_set.
  • tls_set_context(ctx) accepte votre propre ssl.SSLContext. Utilisez-la pour des réglages que tls_set() n'expose pas ; notez que TLS ne se configure qu'une fois par client : un second appel lève ValueError: SSL/TLS has already been configured.
custom_context.py
import ssl

ctx = ssl.create_default_context(cafile="/etc/mqtt/ca.crt")  # vérification + nom d'hôte
ctx.minimum_version = ssl.TLSVersion.TLSv1_2
client.tls_set_context(ctx)

CA privée ou broker auto-signé

Un broker Mosquitto sur votre réseau local a généralement un certificat signé par votre propre CA. Donnez à paho le certificat de la CA (pas celui du broker, et jamais la clé privée) :

private_ca.py
client.tls_set(ca_certs="/etc/mqtt/ca.crt")
client.connect("mqtt.home.internal", 8883)

Le nom d'hôte auquel vous vous connectez doit figurer dans le Subject Alternative Name du certificat du broker. Si vous vous connectez par IP, celle-ci doit apparaître dans le SAN en tant qu'entrée IP: ; une entrée DNS: ne la couvre pas. Dans le doute, régénérez le certificat serveur avec le bon SAN plutôt que de désactiver les contrôles. La procédure complète est dans corriger les erreurs de certificat auto-signé.

TLS mutuel avec certificats client

Avec le TLS mutuel, le broker vérifie aussi le client. Chaque appareil reçoit son propre certificat signé par une CA de confiance pour le broker, et de nombreux brokers peuvent associer le CN du certificat au nom d'utilisateur MQTT.

mtls_client.py
import ssl

import paho.mqtt.client as mqtt

client = mqtt.Client(mqtt.CallbackAPIVersion.VERSION2, client_id="device-0042")
client.tls_set(
    ca_certs="/etc/mqtt/ca.crt",          # CA qui a signé le certificat du broker
    certfile="/etc/mqtt/device-0042.crt",  # certificat de ce client (PEM)
    keyfile="/etc/mqtt/device-0042.key",   # sa clé privée (PEM)
    keyfile_password=None,                 # ou lue depuis un coffre de secrets
    cert_reqs=ssl.CERT_REQUIRED,
)
client.connect("mqtt.example.internal", 8883)
client.loop_forever()

Les pièges classiques :

  • certfile peut contenir le certificat client suivi des CA intermédiaires, ce qui est nécessaire quand le broker ne fait confiance qu'à la racine.
  • Les clés doivent être au format PEM. Convertissez un bundle PKCS#12 avec openssl pkcs12 -in device.p12 -out device.crt -clcerts -nokeys et openssl pkcs12 -in device.p12 -out device.key -nocerts -nodes.
  • Protégez le fichier de clé (chmod 600) et ne le committez jamais.

La prise en charge des certificats client par votre broker, et la façon dont il les associe à des identités, dépend du broker. Les concepts et la configuration côté broker sont expliqués dans certificats client MQTT et mTLS.

Erreurs SSL courantes dans paho et leurs corrections

Avec connect(), les erreurs TLS sont levées directement sous forme d'exceptions, faciles à lire. Avec connect_async() et loop_start(), le thread réseau réessaie en boucle : appelez client.enable_logger() pour voir l'erreur. Le suffixe (_ssl.c:NNNN) varie selon la version de Python ; recherchez le texte qui le précède.

certificate verify failed: unable to get local issuer certificate

Erreur
ssl.SSLCertVerificationError: [SSL: CERTIFICATE_VERIFY_FAILED] certificate verify failed: unable to get local issuer certificate (_ssl.c:1006)
  • Cause : Python ne trouve pas la CA qui a émis le certificat du broker. Soit le broker utilise une CA privée, soit votre Python n'a pas de bundle CA (typique des builds python.org sur macOS).
  • Confirmer : python3 -c "import ssl; print(ssl.get_default_verify_paths())" indique où Python cherche ; openssl s_client (voir plus bas) indique qui a émis le certificat.
  • Correction : pour une CA privée, passez ca_certs="ca.crt". Si le bundle manque, exécutez /Applications/Python 3.x/Install Certificates.command sur macOS, ou pip install certifi puis tls_set(ca_certs=certifi.where()).

self-signed certificate (in certificate chain)

Erreur
ssl.SSLCertVerificationError: [SSL: CERTIFICATE_VERIFY_FAILED] certificate verify failed: self-signed certificate in certificate chain (_ssl.c:1006)
  • Cause : la chaîne se termine par une racine à laquelle votre client ne fait pas confiance. Les anciennes versions d'OpenSSL écrivent self signed certificate.
  • Correction : passez la CA racine avec ca_certs=. Si le certificat du broker est lui-même auto-signé (sans CA), passez ce fichier comme ca_certs, ou mieux, émettez-le depuis une petite CA.

Hostname mismatch

Erreur
ssl.SSLCertVerificationError: [SSL: CERTIFICATE_VERIFY_FAILED] certificate verify failed: Hostname mismatch, certificate is not valid for 'localhost'. (_ssl.c:1006)
  • Cause : le nom ou l'IP passé à connect() ne figure pas dans la liste SAN du certificat.
  • Confirmer : openssl s_client -connect host:8883 -servername host </dev/null 2>/dev/null | openssl x509 -noout -ext subjectAltName.
  • Correction : connectez-vous avec un nom présent dans le SAN ou réémettez le certificat. tls_insecure_set(True) masque le problème et n'a pas sa place en production. Plus de détails dans hostname mismatch MQTT.

certificate has expired

Erreur
ssl.SSLCertVerificationError: [SSL: CERTIFICATE_VERIFY_FAILED] certificate verify failed: certificate has expired (_ssl.c:1006)
  • Cause : le certificat du broker (ou un intermédiaire) a dépassé sa date notAfter, ou l'horloge de l'appareil est fausse. Les appareils sans RTC qui démarrent en 1970 rencontrent le cas inverse : certificate is not yet valid.
  • Correction : renouvelez le certificat ; sur les appareils, synchronisez l'heure (NTP) avant de vous connecter.

Mauvais port : EOF ou wrong version number

Erreurs
ssl.SSLEOFError: [SSL: UNEXPECTED_EOF_WHILE_READING] EOF occurred in violation of protocol (_ssl.c:1006)
ssl.SSLError: [SSL: WRONG_VERSION_NUMBER] wrong version number (_ssl.c:1006)
  • Cause : vous avez appelé tls_set() mais vous vous êtes connecté à un port en clair. Un listener MQTT ferme généralement la connexion (EOF ou ConnectionResetError) ; un port qui répond en HTTP ou avec d'autres octets non TLS donne WRONG_VERSION_NUMBER.
  • Correction : utilisez le port TLS (8883). L'erreur inverse, sans tls_set() vers le 8883, ne produit rien d'utile : le broker coupe la connexion et vous ne recevez jamais de CONNACK.

Certificat client refusé (TLS mutuel)

Erreurs
ssl.SSLError: [SSL: TLSV13_ALERT_CERTIFICATE_REQUIRED] tlsv13 alert certificate required (_ssl.c:2580)
ssl.SSLError: [SSL: TLSV1_ALERT_UNKNOWN_CA] tlsv1 alert unknown ca (_ssl.c:1006)
ssl.SSLError: [SSL] PEM lib (_ssl.c:3900)
  • certificate required : le broker exige un certificat client et vous n'en avez envoyé aucun ; ajoutez certfile/keyfile. En TLS 1.3, cette alerte peut arriver juste après le handshake et apparaître comme une déconnexion immédiate plutôt qu'une exception.
  • unknown ca : le broker ne fait pas confiance à la CA qui a signé votre certificat client, ou vous n'avez pas envoyé l'intermédiaire. Corrigez le fichier CA du broker ou ajoutez l'intermédiaire à certfile.
  • PEM lib ou [X509: KEY_VALUES_MISMATCH] key values mismatch : le certificat ou la clé est illisible, chiffré avec un autre mot de passe, pas au format PEM, ou la clé ne correspond pas au certificat. Pour les clés RSA, comparez openssl x509 -noout -modulus -in device.crt | openssl md5 avec openssl rsa -noout -modulus -in device.key | openssl md5.

Pour d'autres erreurs paho, consultez erreurs paho-mqtt en Python.

Tester la correction

Vérifiez d'abord le côté broker, indépendamment de Python :

Inspecter le certificat du broker
openssl s_client -connect broker.connectmq.com:8883 \
  -servername broker.connectmq.com -showcerts </dev/null
# cherchez : Verify return code: 0 (ok)

Confirmez ensuite qu'un client éprouvé fonctionne avec les mêmes fichiers :

Même vérification avec mosquitto_sub
mosquitto_sub -h broker.connectmq.com -p 8883 \
  --capath /etc/ssl/certs \
  -u "$MQTT_USER" -P "$MQTT_PASS" \
  -t 'testmqtt/tls-demo/#' -v -C 1

Enfin, lancez votre script Python avec la journalisation activée pour afficher tout problème restant :

debug_tls.py
import logging

logging.basicConfig(level=logging.DEBUG)
client.enable_logger()

Si openssl réussit et que Python échoue, la différence est presque toujours le bundle CA utilisé par Python. Si les deux échouent, le problème vient du broker ou du réseau. Une checklist pas à pas de tout le parcours se trouve dans tester une connexion MQTT TLS, et vous pouvez contre-vérifier le broker en WebSockets sécurisés avec le client MQTT en ligne.

Le TLS dans paho se résume à un appel bien fait : tls_set() avec la bonne CA, le bon port et un nom d'hôte qui correspond au certificat. Gardez la vérification active, corrigez les certificats au lieu de désactiver les contrôles, et vous obtiendrez chiffrement et authentification du broker sans code supplémentaire.

Questions fréquentes

Que fait tls_set() sans argument ?

Dans paho-mqtt 2.x, il crée un contexte TLS client avec la version de TLS la plus élevée prise en charge par Python, charge les certificats CA de confiance du système, exige un certificat serveur valide et vérifie que le nom d'hôte correspond. C'est le bon réglage pour les brokers dotés de certificats publiquement reconnus.

Quelle différence entre tls_insecure_set(True) et cert_reqs=ssl.CERT_NONE ?

tls_insecure_set(True) vérifie toujours la chaîne de certificats mais saute la vérification du nom d'hôte. cert_reqs=ssl.CERT_NONE désactive entièrement la vérification et accepte n'importe quel certificat. Les deux rendent possibles les attaques man-in-the-middle et ne servent qu'à de brefs tests locaux.

Pourquoi paho affiche-t-il certificate verify failed: unable to get local issuer certificate ?

Python n'a pas trouvé la CA qui a signé le certificat du broker. Soit le broker utilise une CA privée (passez son fichier avec ca_certs=), soit votre Python n'a pas de bundle CA, fréquent avec les installations python.org sur macOS. Exécutez Install Certificates.command ou passez ca_certs=certifi.where().