ESP32 und Arduino

ESP32 MQTT SSL: Sicheres MQTT über TLS mit WiFiClientSecure

Verbinden Sie einen ESP32 mit WiFiClientSecure und PubSubClient über TLS auf Port 8883 mit einem MQTT-Broker, prüfen Sie den Server mit der richtigen Root-CA, synchronisieren Sie die Uhrzeit und behalten Sie den Heap-Verbrauch im Griff.

ESP32-Board mit Schloss-Symbol, das sich in einem ESP32-MQTT-SSL-Tutorial über Port 8883 verbindet
Probieren Sie es auf Ihrem eigenen MQTT-Broker ausHolen Sie sich im Free-Tarif einen privaten, verwalteten MQTT-Broker und starten Sie mit fertigen Codebeispielen.
Kostenlos testen Codebeispiele Keine Kreditkarte erforderlich

ESP32 MQTT SSL ist der Schritt, der aus einem Hobby-Sketch ein Gerät macht, das Sie in einem echten Netzwerk betreiben können: Zugangsdaten und Payloads werden verschlüsselt, und der ESP32 prüft, dass er tatsächlich mit dem echten Broker spricht. In diesem Tutorial verbinden Sie einen ESP32 mit WiFiClientSecure und PubSubClient über Port 8883 mit broker.connectmq.com, verifizieren den Server mit dem korrekten Root-Zertifikat, synchronisieren die Uhr per NTP und erfahren, wie viel Speicher TLS wirklich kostet. Außerdem erhalten Sie eine Checkliste für die Handshake-Fehler, über die fast jeder einmal stolpert.

Was TLS leistet und was nicht

TLS bei MQTT erfüllt drei Aufgaben: Es verschlüsselt die Verbindung, sodass niemand im WLAN oder auf dem Weg zum Broker Benutzername, Passwort oder Payloads mitlesen kann; es schützt Nachrichten vor Manipulation; und bei korrekter Konfiguration authentifiziert es den Server, sodass ein gefälschter Access Point sich nicht als Ihr Broker ausgeben kann.

Genau diesen letzten Teil überspringen die meisten Tutorials. Ein Aufruf von setInsecure() liefert Verschlüsselung ohne Authentifizierung. Für ein Fünf-Minuten-Experiment ist das in Ordnung, in der Produktion aber eine echte Schwachstelle, denn ein Angreifer, der DNS oder das Netzwerk abfangen kann, kann ein beliebiges Zertifikat vorlegen und Ihre Zugangsdaten abgreifen.

TLS ersetzt keine Authentifizierung. Auf Broker-Seite sollte jedes Gerät weiterhin eigenen Benutzernamen, eigenes Passwort und eigene Client ID haben. Das Gesamtbild beschreibt der Leitfaden zu MQTT-Sicherheit: Best Practices.

Port Protokoll Einsatz
1883 MQTT über TCP Tests auf dem öffentlichen MQTT-Broker, niemals mit Zugangsdaten
8883 MQTT über TLS ESP32 und andere Geräte in der Produktion
8083 MQTT über WebSocket Tests im Browser
8084 MQTT über Secure WebSocket (/mqtt) Browser-Apps in der Produktion

Welches Root-Zertifikat braucht der ESP32?

broker.connectmq.com verwendet ein von Let's Encrypt ausgestelltes Zertifikat, dessen Kette bis zum Root ISRG Root X1 reicht. Das können Sie von jedem Rechner mit OpenSSL selbst prüfen:

openssl s_client -connect broker.connectmq.com:8883 -servername broker.connectmq.com -showcerts </dev/null

Die Ausgabe zeigt das Serverzertifikat für broker.connectmq.com, das ausstellende Let's-Encrypt-Intermediate und eine Kette, die bei ISRG Root X1 endet, gefolgt von Verify return code: 0 (ok), wenn Ihr System diesem Root vertraut.

Die wichtige Regel: Geben Sie dem ESP32 das Root-Zertifikat, nicht das Serverzertifikat. Serverzertifikate von Let's Encrypt sind kurzlebig und werden automatisch erneuert, und auch Intermediates ändern sich im Lauf der Zeit. Ein Gerät, das das Leaf-Zertifikat oder dessen Fingerprint pinnt, verbindet sich nach der nächsten Erneuerung nicht mehr. Das Zertifikat ISRG Root X1 ist bis 2035 gültig, sodass Ihre Firmware jede routinemäßige Erneuerung übersteht, wenn Sie es einbetten.

Voraussetzungen

  • Ein ESP32-Board mit installiertem esp32-Boardpaket von Espressif in der Arduino IDE 2.x.
  • PubSubClient von Nick O'Leary aus dem Bibliotheksverwalter.
  • Ein ConnectMQ-Broker: Registrieren Sie sich, wählen Sie einen Tarif, erstellen Sie einen Broker und kopieren Sie den generierten Benutzernamen, das Passwort und die Client ID aus dem Portal. Der kostenlose Serverless-Tarif genügt.

Falls dies Ihr erster ESP32-MQTT-Sketch ist, behandelt das ESP32-IoT-Tutorial zuerst die Board-Einrichtung und unverschlüsseltes MQTT; TLS lässt sich leichter debuggen, wenn Sie wissen, dass die Version ohne TLS funktioniert.

Der vollständige ESP32-MQTT-SSL-Sketch

Der folgende Sketch verbindet das WLAN, synchronisiert die Zeit, lädt das Root-Zertifikat, verbindet sich mit Zugangsdaten über Port 8883, abonniert ein Befehls-Topic und veröffentlicht alle zehn Sekunden Telemetrie.

#include <WiFi.h>
#include <WiFiClientSecure.h>
#include <PubSubClient.h>
#include <time.h>

const char* WIFI_SSID     = "YOUR_WIFI";
const char* WIFI_PASSWORD = "YOUR_WIFI_PASSWORD";

const char* MQTT_HOST     = "broker.connectmq.com";
const uint16_t MQTT_PORT  = 8883;
const char* MQTT_CLIENT_ID = "YOUR_CLIENT_ID";
const char* MQTT_USERNAME  = "YOUR_USERNAME";
const char* MQTT_PASSWORD  = "YOUR_PASSWORD";

const char* TOPIC_TELEMETRY = "devices/esp32-01/telemetry";
const char* TOPIC_COMMAND   = "devices/esp32-01/cmd";

// ISRG Root X1 (Let's Encrypt), gültig bis 2035-06-04
static const char ROOT_CA[] PROGMEM = R"EOF(
-----BEGIN CERTIFICATE-----
MIIFazCCA1OgAwIBAgIRAIIQz7DSQONZRGPgu2OCiwAwDQYJKoZIhvcNAQELBQAw
TzELMAkGA1UEBhMCVVMxKTAnBgNVBAoTIEludGVybmV0IFNlY3VyaXR5IFJlc2Vh
cmNoIEdyb3VwMRUwEwYDVQQDEwxJU1JHIFJvb3QgWDEwHhcNMTUwNjA0MTEwNDM4
WhcNMzUwNjA0MTEwNDM4WjBPMQswCQYDVQQGEwJVUzEpMCcGA1UEChMgSW50ZXJu
ZXQgU2VjdXJpdHkgUmVzZWFyY2ggR3JvdXAxFTATBgNVBAMTDElTUkcgUm9vdCBY
MTCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBAK3oJHP0FDfzm54rVygc
h77ct984kIxuPOZXoHj3dcKi/vVqbvYATyjb3miGbESTtrFj/RQSa78f0uoxmyF+
0TM8ukj13Xnfs7j/EvEhmkvBioZxaUpmZmyPfjxwv60pIgbz5MDmgK7iS4+3mX6U
A5/TR5d8mUgjU+g4rk8Kb4Mu0UlXjIB0ttov0DiNewNwIRt18jA8+o+u3dpjq+sW
T8KOEUt+zwvo/7V3LvSye0rgTBIlDHCNAymg4VMk7BPZ7hm/ELNKjD+Jo2FR3qyH
B5T0Y3HsLuJvW5iB4YlcNHlsdu87kGJ55tukmi8mxdAQ4Q7e2RCOFvu396j3x+UC
B5iPNgiV5+I3lg02dZ77DnKxHZu8A/lJBdiB3QW0KtZB6awBdpUKD9jf1b0SHzUv
KBds0pjBqAlkd25HN7rOrFleaJ1/ctaJxQZBKT5ZPt0m9STJEadao0xAH0ahmbWn
OlFuhjuefXKnEgV4We0+UXgVCwOPjdAvBbI+e0ocS3MFEvzG6uBQE3xDk3SzynTn
jh8BCNAw1FtxNrQHusEwMFxIt4I7mKZ9YIqioymCzLq9gwQbooMDQaHWBfEbwrbw
qHyGO0aoSCqI3Haadr8faqU9GY/rOPNk3sgrDQoo//fb4hVC1CLQJ13hef4Y53CI
rU7m2Ys6xt0nUW7/vGT1M0NPAgMBAAGjQjBAMA4GA1UdDwEB/wQEAwIBBjAPBgNV
HRMBAf8EBTADAQH/MB0GA1UdDgQWBBR5tFnme7bl5AFzgAiIyBpY9umbbjANBgkq
hkiG9w0BAQsFAAOCAgEAVR9YqbyyqFDQDLHYGmkgJykIrGF1XIpu+ILlaS/V9lZL
ubhzEFnTIZd+50xx+7LSYK05qAvqFyFWhfFQDlnrzuBZ6brJFe+GnY+EgPbk6ZGQ
3BebYhtF8GaV0nxvwuo77x/Py9auJ/GpsMiu/X1+mvoiBOv/2X/qkSsisRcOj/KK
NFtY2PwByVS5uCbMiogziUwthDyC3+6WVwW6LLv3xLfHTjuCvjHIInNzktHCgKQ5
ORAzI4JMPJ+GslWYHb4phowim57iaztXOoJwTdwJx4nLCgdNbOhdjsnvzqvHu7Ur
TkXWStAmzOVyyghqpZXjFaH3pO3JLF+l+/+sKAIuvtd7u+Nxe5AW0wdeRlN8NwdC
jNPElpzVmbUq4JUagEiuTDkHzsxHpFKVK7q4+63SM1N95R1NbdWhscdCb+ZAJzVc
oyi3B43njTOQ5yOf+1CceWxG1bQVs5ZufpsMljq4Ui0/1lvh+wjChP4kqKOJ2qxq
4RgqsahDYVvTH9w7jXbyLeiNdd8XM2w9U/t7y0Ff/9yi0GE44Za4rF2LN9d11TPA
mRGunUHBcnWEvgJBQl9nJEiU0Zsnvgc/ubhPgXRR4Xq37Z0j4r7g1SgEEzwxA57d
emyPxgcYxn/eR44/KJ4EBs+lVDR3veyJm+kXQ99b21/+jh5Xos1AnX5iItreGCc=
-----END CERTIFICATE-----
)EOF";

WiFiClientSecure net;
PubSubClient mqtt(net);
unsigned long lastAttempt = 0, lastPublish = 0;

void syncTime() {
  configTime(0, 0, "pool.ntp.org", "time.google.com");
  Serial.print("Waiting for NTP time");
  time_t now = time(nullptr);
  unsigned long start = millis();
  while (now < 1700000000 && millis() - start < 20000) {   // jedes Datum nach 2023
    delay(250);
    Serial.print(".");
    now = time(nullptr);
  }
  Serial.printf("\nUTC time: %s", ctime(&now));
}

void onMessage(char* topic, byte* payload, unsigned int length) {
  Serial.printf("Command on %s: %.*s\n", topic, (int)length, (const char*)payload);
}

bool connectMqtt() {
  Serial.printf("Free heap before TLS: %u bytes\n", ESP.getFreeHeap());
  if (!mqtt.connect(MQTT_CLIENT_ID, MQTT_USERNAME, MQTT_PASSWORD)) {
    char err[128];
    int code = net.lastError(err, sizeof(err));
    Serial.printf("MQTT state=%d, TLS error %d: %s\n", mqtt.state(), code, err);
    return false;
  }
  Serial.printf("Connected over TLS. Free heap: %u bytes\n", ESP.getFreeHeap());
  mqtt.subscribe(TOPIC_COMMAND, 1);
  return true;
}

void setup() {
  Serial.begin(115200);
  WiFi.mode(WIFI_STA);
  WiFi.begin(WIFI_SSID, WIFI_PASSWORD);
  while (WiFi.status() != WL_CONNECTED) { delay(250); Serial.print("."); }
  Serial.printf("\nWi-Fi connected, IP %s\n", WiFi.localIP().toString().c_str());

  syncTime();
  net.setCACert(ROOT_CA);
  net.setHandshakeTimeout(15);              // Sekunden

  mqtt.setServer(MQTT_HOST, MQTT_PORT);
  mqtt.setCallback(onMessage);
  mqtt.setKeepAlive(60);
  mqtt.setSocketTimeout(10);
  mqtt.setBufferSize(512);
}

void loop() {
  if (!mqtt.connected()) {
    if (millis() - lastAttempt > 10000) {
      lastAttempt = millis();
      connectMqtt();
    }
    return;
  }
  mqtt.loop();

  if (millis() - lastPublish > 10000) {
    lastPublish = millis();
    char json[96];
    snprintf(json, sizeof(json), "{\"uptime\":%lu,\"heap\":%u,\"rssi\":%d}",
             millis() / 1000, ESP.getFreeHeap(), WiFi.RSSI());
    mqtt.publish(TOPIC_TELEMETRY, json);
  }
}

Ersetzen Sie die drei Platzhalter für die Zugangsdaten durch die Werte aus dem Portal. Da ConnectMQ jede Client ID an die Zugangsdaten bindet, mit denen sie ausgegeben wurde, verwenden Sie die generierte Client ID, statt sich eine eigene auszudenken, und betreiben Sie nie zwei Geräte mit demselben Satz. Um die eingehenden Nachrichten zu beobachten, öffnen Sie den Live-Topic-Explorer des Portals oder dessen MQTT-Client im Browser und abonnieren devices/#.

Warum Zeitsynchronisation für ESP32 MQTT SSL wichtig ist

Ein Zertifikat ist nur zwischen seinem „not before"- und „not after"-Datum gültig. Der ESP32 hat keine batteriegepufferte Uhr und glaubt daher nach jedem Start, es sei der 1. Januar 1970. Je nachdem, wie mbedTLS in Ihrer Arduino-Core-Version konfiguriert ist, werden Zertifikatsdaten entweder gegen diese Uhr geprüft, sodass jedes Zertifikat als „noch nicht gültig" erscheint und der Handshake scheitert, oder gar nicht geprüft, sodass ein abgelaufenes Zertifikat stillschweigend akzeptiert würde. Beides wollen Sie nicht.

Eine NTP-Synchronisation vor der ersten TLS-Verbindung löst beide Fälle. Sie kostet beim Start ein bis zwei Sekunden und erfordert ausgehenden UDP-Port 123. In Netzwerken, die NTP blockieren, richten Sie configTime() auf den lokalen NTP-Server, den Ihre Firewall erlaubt. Eine genaue Uhrzeit ist auch für sich genommen nützlich: Sie können echte Zeitstempel in Ihre Telemetrie schreiben, wie in ESP32-NTP-Zeitstempel für MQTT-Nachrichten beschrieben.

Speicher: Was TLS auf einem ESP32 kostet

TLS ist das Speicherhungrigste, was ein typischer ESP32-Sketch tut. Während des Handshakes reserviert mbedTLS Puffer für ein- und ausgehende Records, für die zu parsende Zertifikatskette und für den Schlüsselaustausch. Der Sketch gibt den freien Heap vor und nach dem Verbinden aus, damit Sie den Effekt auf Ihrem eigenen Board und Ihrer Core-Version sehen.

Faktor Auswirkung Was zu tun ist
TLS-Record-Puffer Zig Kilobyte, die für die gesamte Verbindungsdauer belegt sind Einplanen; auf knappen Boards keinen zweiten gleichzeitigen TLS-Client betreiben (etwa HTTPS-OTA bei bestehender MQTT-Verbindung)
Parsen der Zertifikatskette Kurzzeitige Spitze während des Handshakes Bei knappem RAM nur das benötigte Root einbetten statt eines großen Bundles
Heap-Fragmentierung Lange Laufzeit und viele String-Allokationen können keinen großen zusammenhängenden Block übrig lassen Feste char-Puffer bevorzugen, seltener neu verbinden
Boards mit PSRAM (WROVER, viele S3) Mehr Spielraum Große Puffer können vom Allokator des Cores im PSRAM abgelegt werden
PubSubClient-Puffer setBufferSize(512) kostet 512 Byte An der größten Nachricht ausrichten, siehe PubSubClient-Puffergröße

Ein praktisches Warnsignal: Wenn Verbindungsversuche nach Stunden oder Tagen mit Speicherallokationsfehlern in lastError() scheitern und ein Neustart das behebt, haben Sie es mit Fragmentierung zu tun. Wenn Sie ESP.getMaxAllocHeap() zusammen mit dem freien Heap protokollieren, sehen Sie den größten noch verfügbaren Block.

Checkliste zur Fehlerbehebung

  1. X509 - Certificate verification failed. Falsches Root-Zertifikat, ein Tippfehler im PEM oder die Uhr ist nicht gestellt. Kopieren Sie das PEM einschließlich der Zeilen BEGIN und END erneut und prüfen Sie die im seriellen Monitor ausgegebene NTP-Zeit.
  2. Handshake läuft in einen Timeout. Das Netzwerk blockiert Port 8883, was in Firmen- und Gast-WLANs häufig vorkommt. Testen Sie mit dem obigen OpenSSL-Befehl von einem Laptop im selben Netzwerk.
  3. state=4 oder state=5 nach erfolgreichem Handshake. TLS funktioniert; die Zugangsdaten oder die Client ID sind falsch. Achten Sie auf Leerzeichen am Ende, die aus dem Portal mitkopiert wurden.
  4. Speicherallokationsfehler in lastError(). Nicht genug zusammenhängender Heap. Geben Sie vor dem Verbinden Speicher frei, vermeiden Sie parallele TLS-Sessions und reduzieren Sie String-Umwälzungen.
  5. Funktioniert auf 1883, aber nicht auf 8883. Sie verwenden noch WiFiClient statt WiFiClientSecure, oder setServer() zeigt auf Port 1883.
  6. Verbindet sich und bricht dann etwa jede Minute ab. Meist ein Keep-Alive-Problem oder eine doppelte Client ID statt TLS; hier helfen die Muster aus ESP32-MQTT-Reconnect-Logik.

Wiederholte fehlgeschlagene Logins können auf dem Broker eine IP-Sperre auslösen. Beheben Sie Zugangsdatenfehler daher, bevor Sie ein Gerät in einer schnellen Wiederholungsschleife laufen lassen.

Alternativen zum Einbetten eines einzelnen Roots

Das Einbetten von ISRG Root X1 ist die einfachste robuste Lösung für einen einzelnen Broker. Es gibt zwei Alternativen. Der Arduino-Core unterstützt über setCACertBundle() ein CA-Bundle, mit dem eine Firmware vielen öffentlichen Roots vertrauen kann, auf Kosten von Flash-Speicher und etwas Einrichtungsaufwand; das ist nützlich, wenn dasselbe Gerät mit mehreren HTTPS- und MQTTS-Diensten spricht. Alternativ legen Sie das PEM in einer Dateisystempartition ab, sodass es sich ohne erneutes Flashen des Codes aktualisieren lässt. Vermeiden Sie Fingerprint-Pinning bei jedem Broker, der automatisch erneuerte Zertifikate verwendet.

Hinweise für die Produktion

Verschlüsselung schützt die Verbindung; der sichere Betrieb des Brokers ist die andere Hälfte. Ein verwalteter MQTT-Broker übernimmt Zertifikatserneuerung, Mandantentrennung und Brute-Force-Schutz für Sie, sodass die Firmware nur das Root-Zertifikat und ihre Zugangsdaten braucht. ConnectMQ bietet TLS auf Port 8883 und Secure WebSocket auf 8084 mit Let's-Encrypt-Zertifikaten, und dieselben Zugangsdaten funktionieren auf Ihrem ESP32, in einem Python-Backend oder in einem Browser-Dashboard. Wenn Sie einen selbst betriebenen Broker gegen einen Cloud-MQTT-Broker abwägen, ist der Umgang mit Zertifikaten einer der größten Unterschiede im laufenden Betrieb, und der Leitfaden dazu, was ein MQTT-Broker macht, erklärt die übrigen Bausteine. In Werksnetzen, deren Zonen und Übergänge sich an IEC 62443 orientieren, hat das Muster noch einen Vorteil: Der ESP32 baut nur eine ausgehende TLS-Verbindung auf Port 8883 auf, sodass die OT-Firewall keine eingehenden Ports zum Gerät öffnen muss.

Starten Sie mit ConnectMQ

Erstellen Sie einen kostenlosen Serverless Broker unter app.connectmq.com, ohne Kreditkarte. Kopieren Sie die generierten Zugangsdaten in den obigen Sketch, und Ihr ESP32 spricht innerhalb von Minuten TLS auf Port 8883.

Häufig gestellte Fragen

Ist setInsecure() für ESP32 MQTT über TLS akzeptabel?

Nur für schnelle Tests. Es verschlüsselt den Datenverkehr, überspringt aber die Serverprüfung, sodass ein Angreifer, der Ihren Datenverkehr umleiten kann, Zugangsdaten abfangen kann. Verwenden Sie in allem, was Sie ausrollen, setCACert() mit dem Root-Zertifikat des Brokers.

Welches Zertifikat verwende ich für broker.connectmq.com?

Verwenden Sie das Root-Zertifikat ISRG Root X1, da das Zertifikat des Brokers von Let's Encrypt ausgestellt wird. Betten Sie nicht das Serverzertifikat selbst ein; es wird automatisch erneuert, und Ihr Gerät würde sich danach nicht mehr verbinden.

Brauche ich NTP für TLS auf dem ESP32?

Sie sollten die Zeit vor dem Verbinden synchronisieren. Ohne Synchronisation werden Zertifikatsdaten je nach Core-Konfiguration entweder mit einer Uhr aus dem Jahr 1970 verglichen und scheitern, oder sie werden gar nicht geprüft.

Wie viel RAM braucht TLS auf einem ESP32?

Rechnen Sie mit einigen Dutzend Kilobyte Heap, solange eine TLS-Verbindung offen ist, mit einer höheren kurzzeitigen Spitze während des Handshakes. Messen Sie es auf Ihrem Board, indem Sie wie im Sketch ESP.getFreeHeap() vor und nach dem Verbinden ausgeben.

Verwandte Artikel

ESP32 NTP MQTT Zeitstempel: genaue Uhrzeit in jeder Nachricht

Synchronisieren Sie die ESP32-Uhr per NTP, formatieren Sie Zeitstempel in ISO 8601 und Epoch-Millisekunden und hängen Sie sie an MQTT-Payloads, damit Messwerte ihren echten Erfassungszeitpunkt behalten, auch wenn sie verspätet veröffentlicht werden.

8 Min. Lesezeit

Alle Artikel →

Ihr privater MQTT-Broker ist nur wenige Klicks entfernt

Konto anlegen, Broker-Zugangsdaten erhalten und das erste Gerät verbinden — ohne Server, die installiert, gepatcht oder skaliert werden müssen.