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
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()
| Argument | Valeur par défaut dans paho 2.x | À utiliser quand |
|---|---|---|
ca_certs | None → magasin de confiance du système | Le broker utilise une CA privée ou auto-signée : chemin du PEM de la CA |
certfile | None | Le broker exige des certificats client (TLS mutuel) |
keyfile | None | Clé privée de certfile, si elle n'est pas dans le même fichier |
keyfile_password | None | La clé est chiffrée ; sinon paho peut la demander dans le terminal |
cert_reqs | ssl.CERT_REQUIRED | Presque jamais à changer ; ssl.CERT_NONE désactive la vérification |
tls_version | ssl.PROTOCOL_TLS_CLIENT (négocie la version la plus haute) | Rarement ; préférez fixer une version minimale sur votre propre contexte |
ciphers | Valeurs par défaut d'OpenSSL | Vous devez respecter une politique de chiffrement précise |
alpn_protocols | None | Le 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èstls_set(), sinon paho lèveValueError: Must configure SSL context before using tls_insecure_set.tls_set_context(ctx)accepte votre propressl.SSLContext. Utilisez-la pour des réglages quetls_set()n'expose pas ; notez que TLS ne se configure qu'une fois par client : un second appel lèveValueError: SSL/TLS has already been configured.
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) :
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.
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 :
certfilepeut 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 -nokeysetopenssl 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
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.commandsur macOS, oupip install certifipuistls_set(ca_certs=certifi.where()).
self-signed certificate (in certificate chain)
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 commeca_certs, ou mieux, émettez-le depuis une petite CA.
Hostname mismatch
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
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
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 ouConnectionResetError) ; un port qui répond en HTTP ou avec d'autres octets non TLS donneWRONG_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)
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 ; ajoutezcertfile/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 libou[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, comparezopenssl x509 -noout -modulus -in device.crt | openssl md5avecopenssl 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 :
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 :
mosquitto_sub -h broker.connectmq.com -p 8883 \
--capath /etc/ssl/certs \
-u "$MQTT_USER" -P "$MQTT_PASS" \
-t 'testmqtt/tls-demo/#' -v -C 1Enfin, lancez votre script Python avec la journalisation activée pour afficher tout problème restant :
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().

