Tutorials

MQTT über TLS in Python (CA und Client-Zertifikate)

paho-mqtt per TLS mit einem MQTT-Broker verbinden: tls_set()-Optionen, eigene CA-Dateien, Mutual TLS mit Client-Zertifikaten und Lösungen für typische SSL-Fehler.

Aktualisiert · 9 Min. Lesezeit

MQTT über TLS in Python (CA und Client-Zertifikate)

Um paho-mqtt über TLS zu verbinden, rufst du vor connect() client.tls_set() auf und nutzt den TLS-Port des Brokers, meist 8883. Ohne Argumente prüft paho 2.x das Brokerzertifikat gegen die vertrauenswürdigen CAs deines Systems und kontrolliert den Hostnamen – genau richtig für einen Broker mit öffentlichem Zertifikat. Für eine private CA übergibst du ca_certs=, für Mutual TLS zusätzlich certfile= und keyfile=.

Dieser Guide behandelt jede tls_set()-Option, ein vollständiges Mutual-TLS-Beispiel und die genauen SSL-Fehler, die paho wirft – jeweils mit Diagnose und Lösung.

Die minimale TLS-Verbindung

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()  # System-CA-Store, TLS 1.2+, Hostname-Prüfung aktiv
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()

Zwei Regeln verhindern die meisten Probleme:

  • Verbinde dich mit dem Hostnamen aus dem Zertifikat, nicht mit einer IP-Adresse. paho sendet den an connect() übergebenen Host als SNI und prüft ihn gegen das Zertifikat.
  • Port und Protokoll müssen zusammenpassen. TLS gehört auf 8883 (oder 8084 für Secure WebSockets), unverschlüsseltes TCP auf 1883. Wer das vertauscht, bekommt verwirrende Fehler, siehe unten. Die vollständige Liste steht unter MQTT-Ports.

Die Optionen von tls_set()

ArgumentStandard in paho 2.xVerwenden, wenn
ca_certsNone → Trust Store des BetriebssystemsDer Broker nutzt eine private oder selbstsignierte CA: Pfad zur CA-PEM-Datei
certfileNoneDer Broker verlangt Client-Zertifikate (Mutual TLS)
keyfileNonePrivater Schlüssel zu certfile, falls nicht in derselben Datei
keyfile_passwordNoneDer Schlüssel ist verschlüsselt; sonst fragt paho eventuell im Terminal nach
cert_reqsssl.CERT_REQUIREDFast nie ändern; ssl.CERT_NONE schaltet die Prüfung ab
tls_versionssl.PROTOCOL_TLS_CLIENT (handelt die höchste Version aus)Selten; setze lieber eine Mindestversion im eigenen Kontext
ciphersOpenSSL-StandardDu musst eine bestimmte Cipher-Policy einhalten
alpn_protocolsNoneDer Endpunkt routet per ALPN, etwa MQTT auf Port 443

Zwei verwandte Methoden vervollständigen das Bild:

  • tls_insecure_set(True) behält die Kettenprüfung bei, überspringt aber die Hostname-Prüfung. Es muss nach tls_set() aufgerufen werden, sonst wirft paho ValueError: Must configure SSL context before using tls_insecure_set.
  • tls_set_context(ctx) akzeptiert deinen eigenen ssl.SSLContext. Nutze es, wenn du Einstellungen brauchst, die tls_set() nicht anbietet. TLS lässt sich pro Client nur einmal konfigurieren: Ein zweiter Aufruf wirft ValueError: SSL/TLS has already been configured.
custom_context.py
import ssl

ctx = ssl.create_default_context(cafile="/etc/mqtt/ca.crt")  # Prüfung + Hostname-Check
ctx.minimum_version = ssl.TLSVersion.TLSv1_2
client.tls_set_context(ctx)

Private CA oder selbstsignierter Broker

Ein Mosquitto-Broker im LAN hat meist ein Zertifikat, das von deiner eigenen CA signiert ist. Gib paho das CA-Zertifikat (nicht das Brokerzertifikat und niemals den privaten Schlüssel):

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

Der Hostname, mit dem du dich verbindest, muss im Subject Alternative Name des Brokerzertifikats stehen. Verbindest du dich per IP, muss die IP als IP:-Eintrag im SAN stehen; ein DNS:-Eintrag deckt sie nicht ab. Im Zweifel erzeuge das Serverzertifikat mit dem richtigen SAN neu, statt Prüfungen abzuschalten. Die komplette Anleitung findest du unter Fehler bei selbstsignierten Zertifikaten beheben.

Mutual TLS mit Client-Zertifikaten

Bei Mutual TLS prüft auch der Broker den Client. Jedes Gerät bekommt ein eigenes Zertifikat, signiert von einer CA, der der Broker vertraut, und viele Broker können den CN des Zertifikats auf den MQTT-Benutzernamen abbilden.

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, die das Brokerzertifikat signiert hat
    certfile="/etc/mqtt/device-0042.crt",  # Zertifikat dieses Clients (PEM)
    keyfile="/etc/mqtt/device-0042.key",   # zugehöriger privater Schlüssel (PEM)
    keyfile_password=None,                 # oder aus einem Secret Store lesen
    cert_reqs=ssl.CERT_REQUIRED,
)
client.connect("mqtt.example.internal", 8883)
client.loop_forever()

Typische Stolpersteine:

  • certfile darf das Client-Zertifikat gefolgt von Intermediate-CAs enthalten – nötig, wenn der Broker nur der Root vertraut.
  • Schlüssel müssen PEM sein. Ein PKCS#12-Bundle konvertierst du mit openssl pkcs12 -in device.p12 -out device.crt -clcerts -nokeys und openssl pkcs12 -in device.p12 -out device.key -nocerts -nodes.
  • Schütze die Schlüsseldatei (chmod 600) und committe sie niemals.

Ob dein Broker Client-Zertifikate unterstützt und wie er sie Identitäten zuordnet, ist brokerspezifisch. Konzepte und brokerseitige Einrichtung erklärt MQTT-Client-Zertifikate und mTLS.

Typische SSL-Fehler in paho und ihre Lösung

Mit connect() werden TLS-Fehler direkt als Exceptions geworfen und sind leicht zu lesen. Mit connect_async() und loop_start() versucht der Netzwerk-Thread es stattdessen immer wieder – ruf dann client.enable_logger() auf, um den Fehler zu sehen. Das Suffix (_ssl.c:NNNN) hängt von der Python-Version ab; such nach dem Text davor.

certificate verify failed: unable to get local issuer certificate

Fehler
ssl.SSLCertVerificationError: [SSL: CERTIFICATE_VERIFY_FAILED] certificate verify failed: unable to get local issuer certificate (_ssl.c:1006)
  • Ursache: Python findet die CA nicht, die das Brokerzertifikat ausgestellt hat. Entweder nutzt der Broker eine private CA, oder dein Python hat kein CA-Bundle (typisch für python.org-Builds auf macOS).
  • Prüfen: python3 -c "import ssl; print(ssl.get_default_verify_paths())" zeigt, wo Python sucht; openssl s_client (siehe unten) zeigt, wer das Zertifikat ausgestellt hat.
  • Lösung: Bei einer privaten CA ca_certs="ca.crt" übergeben. Fehlt das Bundle, unter macOS /Applications/Python 3.x/Install Certificates.command ausführen oder pip install certifi und tls_set(ca_certs=certifi.where()) verwenden.

self-signed certificate (in certificate chain)

Fehler
ssl.SSLCertVerificationError: [SSL: CERTIFICATE_VERIFY_FAILED] certificate verify failed: self-signed certificate in certificate chain (_ssl.c:1006)
  • Ursache: Die Kette endet in einer Root, der dein Client nicht vertraut. Ältere OpenSSL-Builds schreiben self signed certificate.
  • Lösung: Root-CA mit ca_certs= übergeben. Ist das Brokerzertifikat selbst selbstsigniert (ohne CA), übergib genau diese Datei als ca_certs – oder besser, stelle es über eine kleine CA aus.

Hostname mismatch

Fehler
ssl.SSLCertVerificationError: [SSL: CERTIFICATE_VERIFY_FAILED] certificate verify failed: Hostname mismatch, certificate is not valid for 'localhost'. (_ssl.c:1006)
  • Ursache: Der Name oder die IP aus connect() steht nicht in der SAN-Liste des Zertifikats.
  • Prüfen: openssl s_client -connect host:8883 -servername host </dev/null 2>/dev/null | openssl x509 -noout -ext subjectAltName.
  • Lösung: Mit einem Namen aus dem SAN verbinden oder das Zertifikat neu ausstellen. tls_insecure_set(True) versteckt das Problem nur und hat in Produktion nichts verloren. Mehr unter MQTT-Hostname-Mismatch.

certificate has expired

Fehler
ssl.SSLCertVerificationError: [SSL: CERTIFICATE_VERIFY_FAILED] certificate verify failed: certificate has expired (_ssl.c:1006)
  • Ursache: Das Brokerzertifikat (oder ein Intermediate) hat sein notAfter-Datum überschritten, oder die Geräteuhr geht falsch. Geräte ohne RTC, die im Jahr 1970 booten, treffen den umgekehrten Fall: certificate is not yet valid.
  • Lösung: Zertifikat erneuern; auf Geräten vor dem Verbinden die Zeit synchronisieren (NTP).

Falscher Port: EOF oder wrong version number

Fehler
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)
  • Ursache: Du hast tls_set() aufgerufen, dich aber mit einem unverschlüsselten Port verbunden. Ein MQTT-Listener schließt meist einfach die Verbindung (EOF oder ConnectionResetError); ein Port, der mit HTTP oder anderen Nicht-TLS-Bytes antwortet, liefert WRONG_VERSION_NUMBER.
  • Lösung: Den TLS-Port (8883) verwenden. Der umgekehrte Fehler, kein tls_set() gegen 8883, liefert nichts Hilfreiches: Der Broker trennt die Verbindung, und du bekommst nie ein CONNACK.

Client-Zertifikat abgelehnt (Mutual TLS)

Fehler
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: Der Broker verlangt ein Client-Zertifikat, du hast keins gesendet – certfile/keyfile ergänzen. Mit TLS 1.3 kann dieser Alert erst kurz nach dem Handshake eintreffen und sich deshalb als sofortiger Disconnect statt als Exception zeigen.
  • unknown ca: Der Broker vertraut der CA nicht, die dein Client-Zertifikat signiert hat, oder du hast das Intermediate nicht mitgeschickt. Korrigiere die CA-Datei des Brokers oder hänge das Intermediate an certfile an.
  • PEM lib oder [X509: KEY_VALUES_MISMATCH] key values mismatch: Zertifikat oder Schlüssel sind nicht lesbar, mit einem anderen Passwort verschlüsselt, kein PEM, oder der Schlüssel gehört nicht zum Zertifikat. Bei RSA-Schlüsseln vergleichst du openssl x509 -noout -modulus -in device.crt | openssl md5 mit openssl rsa -noout -modulus -in device.key | openssl md5.

Weitere paho-Fehler findest du unter paho-mqtt-Fehler in Python.

So testest du die Lösung

Prüfe zuerst die Brokerseite, unabhängig von Python:

Brokerzertifikat inspizieren
openssl s_client -connect broker.connectmq.com:8883 \
  -servername broker.connectmq.com -showcerts </dev/null
# achte auf: Verify return code: 0 (ok)

Bestätige dann, dass ein bekannt funktionierender Client mit denselben Dateien klappt:

Dieselbe Prüfung mit 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

Führe schließlich dein Python-Skript mit aktiviertem Logging aus, damit jedes verbleibende Problem ausgegeben wird:

debug_tls.py
import logging

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

Klappt openssl und Python scheitert, liegt der Unterschied fast immer am CA-Bundle, das Python nutzt. Scheitern beide, liegt das Problem beim Broker oder im Netzwerk. Eine Schritt-für-Schritt-Checkliste für den ganzen Weg gibt es unter eine MQTT-TLS-Verbindung testen, und mit dem Online-MQTT-Client kannst du den Broker per Secure WebSockets gegenprüfen.

TLS in paho läuft auf einen korrekt ausgeführten Aufruf hinaus: tls_set() mit der richtigen CA, dem richtigen Port und einem Hostnamen, der zum Zertifikat passt. Lass die Prüfung an, repariere Zertifikate statt Checks abzuschalten, und du bekommst Verschlüsselung und Broker-Authentifizierung ohne zusätzlichen Code.

Häufig gestellte Fragen

Was macht tls_set() ohne Argumente?

In paho-mqtt 2.x erstellt es einen TLS-Client-Kontext mit der höchsten TLS-Version, die Python unterstützt, lädt die vertrauenswürdigen CA-Zertifikate des Betriebssystems, verlangt ein gültiges Serverzertifikat und prüft, ob der Hostname passt. Das ist die richtige Einstellung für Broker mit öffentlich vertrauenswürdigen Zertifikaten.

Was ist der Unterschied zwischen tls_insecure_set(True) und cert_reqs=ssl.CERT_NONE?

tls_insecure_set(True) prüft weiterhin die Zertifikatskette, überspringt aber die Hostname-Prüfung. cert_reqs=ssl.CERT_NONE schaltet die Prüfung komplett ab, jedes Zertifikat wird akzeptiert. Beides ermöglicht Man-in-the-Middle-Angriffe und gehört nur in kurze lokale Tests.

Warum meldet paho certificate verify failed: unable to get local issuer certificate?

Python hat die CA nicht gefunden, die das Brokerzertifikat signiert hat. Entweder nutzt der Broker eine private CA (übergib ihre Datei mit ca_certs=) oder dein Python hat kein CA-Bundle, was bei python.org-Installationen auf macOS häufig ist. Abhilfe: Install Certificates.command ausführen oder ca_certs=certifi.where() übergeben.