ESP32 y Arduino

ESP32 MQTT SSL: MQTT seguro sobre TLS con WiFiClientSecure

Conecta un ESP32 a un broker MQTT sobre TLS en el puerto 8883 con WiFiClientSecure y PubSubClient, verifica el servidor con la CA raíz correcta, sincroniza la hora y mantén bajo control el uso del heap.

Placa ESP32 con un candado conectándose por el puerto 8883 en un tutorial de esp32 mqtt ssl
Pruébalo en tu propio broker MQTTConsigue un broker MQTT privado y gestionado con el plan Free y empieza con ejemplos de código listos para usar.
Pruébalo gratis Ejemplos de código Sin tarjeta

ESP32 MQTT SSL es el paso que convierte un sketch de aficionado en un dispositivo que puedes dejar en una red real: las credenciales y los payloads van cifrados y el ESP32 verifica que está hablando con el broker auténtico. En este tutorial conectarás un ESP32 a broker.connectmq.com en el puerto 8883 con WiFiClientSecure y PubSubClient, verificarás el servidor con el certificado raíz correcto, sincronizarás el reloj por NTP y aprenderás cuánta memoria cuesta realmente TLS. También tendrás una lista de comprobación para los errores de handshake con los que casi todo el mundo tropieza alguna vez.

Qué aporta TLS y qué no

TLS en MQTT hace tres cosas: cifra la conexión para que nadie en el Wi-Fi o en el camino hasta el broker pueda leer tu usuario, tu contraseña o tus payloads; protege los mensajes frente a manipulaciones; y, si lo configuras bien, autentica al servidor, de modo que un punto de acceso falso no pueda suplantar a tu broker.

Esa última parte es la que la mayoría de tutoriales se salta. Llamar a setInsecure() te da cifrado sin autenticación. Vale para un experimento de cinco minutos y es una debilidad real en producción, porque un atacante que pueda interceptar el DNS o la red puede presentar cualquier certificado y recoger tus credenciales.

TLS no sustituye a la autenticación. En el lado del broker, cada dispositivo debe seguir teniendo su propio usuario, contraseña y client ID. Para una visión global, consulta la guía de buenas prácticas de seguridad MQTT.

Puerto Protocolo Uso
1883 MQTT sobre TCP Pruebas en el servidor MQTT público gratuito, nunca con credenciales
8883 MQTT sobre TLS ESP32 y otros dispositivos en producción
8083 MQTT sobre WebSocket Pruebas desde el navegador
8084 MQTT sobre WebSocket seguro (/mqtt) Aplicaciones de navegador en producción

¿Qué certificado raíz necesita el ESP32?

broker.connectmq.com usa un certificado emitido por Let's Encrypt, cuya cadena llega hasta la raíz ISRG Root X1. Puedes comprobarlo tú mismo desde cualquier equipo con OpenSSL:

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

La salida muestra el certificado del servidor para broker.connectmq.com, el intermedio de Let's Encrypt que lo emitió y una cadena que termina en ISRG Root X1, seguida de Verify return code: 0 (ok) cuando tu sistema confía en esa raíz.

La regla importante: dale al ESP32 la raíz, no el certificado del servidor. Los certificados de servidor de Let's Encrypt tienen una vida corta y rotan automáticamente, y los intermedios también cambian con el tiempo. Un dispositivo que fije el certificado hoja o su huella dejará de conectarse tras la siguiente renovación. El certificado ISRG Root X1 es válido hasta 2035, así que incrustarlo significa que tu firmware sobrevive a todas las renovaciones rutinarias.

Requisitos previos

  • Una placa ESP32 con el paquete de placas esp32 de Espressif instalado en Arduino IDE 2.x.
  • PubSubClient de Nick O'Leary, desde el gestor de librerías.
  • Un broker de ConnectMQ: regístrate, elige un plan, crea un broker y copia desde el portal el usuario, la contraseña y el client ID generados. El plan gratuito Serverless es suficiente.

Si este es tu primer sketch MQTT con ESP32, el tutorial de IoT con ESP32 cubre primero la configuración de la placa y MQTT sin cifrar; TLS es más fácil de depurar cuando sabes que la versión sin TLS funciona.

El sketch completo de ESP32 MQTT SSL

El sketch de abajo conecta el Wi-Fi, sincroniza la hora, carga el certificado raíz, se conecta al puerto 8883 con credenciales, se suscribe a un topic de comandos y publica telemetría cada diez segundos.

#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), válido hasta el 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) {   // cualquier fecha posterior a 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);              // segundos

  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);
  }
}

Sustituye los tres marcadores de credenciales por los valores del portal. Como ConnectMQ vincula cada client ID a las credenciales con las que se emitió, usa el client ID generado en lugar de inventarte uno, y no ejecutes dos dispositivos con el mismo juego de credenciales. Para ver llegar los mensajes, abre el explorador de topics en vivo del portal o su cliente MQTT en el navegador y suscríbete a devices/#.

Por qué importa la sincronización horaria en ESP32 MQTT SSL

Un certificado solo es válido entre sus fechas "not before" y "not after". El ESP32 no tiene un reloj con batería, así que después de cada arranque cree que es el 1 de enero de 1970. Según cómo esté configurado mbedTLS en tu versión del core de Arduino, las fechas del certificado se comprueban contra ese reloj, y entonces todos los certificados parecen "todavía no válidos" y el handshake falla, o no se comprueban en absoluto, y entonces se aceptaría en silencio un certificado caducado. Ninguna de las dos cosas es lo que quieres.

Sincronizar por NTP antes de la primera conexión TLS resuelve ambos casos. Cuesta uno o dos segundos en el arranque y requiere el puerto UDP 123 de salida. En redes que bloquean NTP, apunta configTime() al servidor NTP local que permita tu firewall. Tener la hora exacta también es útil por sí mismo: puedes poner marcas de tiempo reales en tu telemetría, como se describe en marcas de tiempo NTP en mensajes MQTT con ESP32.

Memoria: lo que cuesta TLS en un ESP32

TLS es lo que más memoria consume en un sketch típico de ESP32. Durante el handshake, mbedTLS reserva búferes para los registros entrantes y salientes, la cadena de certificados que se está analizando y el intercambio de claves. El sketch imprime el heap libre antes y después de conectar para que veas el efecto en tu propia placa y versión del core.

Factor Efecto Qué hacer
Búferes de registros TLS Decenas de kilobytes retenidos durante toda la conexión Tenlo en cuenta en el presupuesto; evita un segundo cliente TLS simultáneo (por ejemplo, OTA por HTTPS mientras MQTT está conectado) en placas justas
Análisis de la cadena de certificados Pico temporal durante el handshake Incrusta solo la raíz que necesitas en lugar de un bundle grande cuando la RAM escasea
Fragmentación del heap Un uptime largo con muchas asignaciones de String puede dejar sin ningún bloque contiguo grande Prefiere búferes char fijos, reconecta con menos frecuencia
Placas con PSRAM (WROVER, muchas S3) Más margen El asignador del core puede colocar búferes grandes en PSRAM
Búfer de PubSubClient setBufferSize(512) cuesta 512 bytes Dimensiónalo según tu mensaje más grande, consulta tamaño del búfer de PubSubClient

Una señal de alarma práctica: si las conexiones empiezan a fallar tras horas o días con errores de asignación de memoria en lastError() y un reinicio lo arregla, estás ante fragmentación. Registrar ESP.getMaxAllocHeap() junto al heap libre te indica el bloque más grande todavía disponible.

Lista de comprobación para resolver problemas

  1. X509 - Certificate verification failed. Certificado raíz incorrecto, una errata en el PEM o el reloj no está ajustado. Vuelve a copiar el PEM incluidas las líneas BEGIN y END y confirma la hora NTP que aparece en el monitor serie.
  2. El handshake agota el tiempo. La red bloquea el puerto 8883, algo habitual en Wi-Fi corporativas y de invitados. Prueba el comando OpenSSL de arriba desde un portátil en la misma red.
  3. state=4 o state=5 tras un handshake correcto. TLS está bien; las credenciales o el client ID son incorrectos. Busca espacios al final copiados del portal.
  4. Fallos de asignación de memoria en lastError(). No hay suficiente heap contiguo. Libera memoria antes de conectar, evita sesiones TLS concurrentes y reduce el trasiego de String.
  5. Funciona en 1883 pero no en 8883. Sigues usando WiFiClient en lugar de WiFiClientSecure, o setServer() apunta al puerto 1883.
  6. Conecta y luego se cae más o menos cada minuto. Normalmente es un problema de keep-alive o de client ID duplicado más que de TLS; los patrones de reconexión de lógica de reconexión MQTT en ESP32 ayudan aquí.

Los inicios de sesión fallidos repetidos pueden provocar el bloqueo de la IP en el broker, así que corrige los errores de credenciales antes de dejar un dispositivo en un bucle de reintentos rápido.

Alternativas a incrustar una sola raíz

Incrustar ISRG Root X1 es la opción robusta más sencilla para un único broker. Existen dos alternativas. El core de Arduino admite un bundle de CA mediante setCACertBundle(), que permite que un mismo firmware confíe en muchas raíces públicas a costa de flash y algo de configuración; es útil cuando el mismo dispositivo habla con varios servicios HTTPS y MQTTS. Como alternativa, puedes guardar el PEM en una partición del sistema de archivos para poder actualizarlo sin reflashear el código. Evita fijar la huella del certificado con cualquier broker que use certificados renovados automáticamente.

Notas para producción

El cifrado protege el enlace; operar el broker de forma segura es la otra mitad. Un broker MQTT gestionado se encarga por ti de la renovación de certificados, el aislamiento multi-tenant y la protección frente a fuerza bruta, de modo que el firmware solo necesita el certificado raíz y sus credenciales. ConnectMQ sirve TLS en el puerto 8883 y WebSocket seguro en el 8084 con certificados de Let's Encrypt, y las mismas credenciales funcionan desde tu ESP32, un backend en Python o un dashboard en el navegador. Si estás comparando el autoalojamiento con un broker MQTT en la nube, la gestión de certificados es una de las mayores diferencias del día a día, y la guía sobre qué hace un broker MQTT explica el resto de las piezas.

Empieza con ConnectMQ

Crea un broker Serverless gratuito en app.connectmq.com, sin tarjeta de crédito. Copia las credenciales generadas en el sketch de arriba y tu ESP32 estará hablando TLS en el puerto 8883 en cuestión de minutos.

Preguntas frecuentes

¿Es aceptable setInsecure() para MQTT sobre TLS en el ESP32?

Solo para pruebas rápidas. Cifra el tráfico pero se salta la verificación del servidor, así que un atacante capaz de redirigir tu tráfico puede capturar las credenciales. Usa setCACert() con el certificado raíz del broker en cualquier cosa que despliegues.

¿Qué certificado uso para broker.connectmq.com?

Usa el certificado raíz ISRG Root X1, porque el certificado del broker lo emite Let's Encrypt. No incrustes el certificado del servidor en sí; se renueva automáticamente y tu dispositivo dejaría de conectarse.

¿Necesito NTP para usar TLS en el ESP32?

Deberías sincronizar la hora antes de conectar. Sin ella, las fechas del certificado o bien se comparan con un reloj de 1970 y fallan, o bien no se comprueban en absoluto, según la configuración del core.

¿Cuánta RAM necesita TLS en un ESP32?

Cuenta con decenas de kilobytes de heap mientras haya una conexión TLS abierta, con un pico temporal mayor durante el handshake. Mídelo en tu placa imprimiendo ESP.getFreeHeap() antes y después de conectar, como hace el sketch.

Artículos relacionados

Ejemplo de ESP-IDF MQTT: crea un cliente esp-mqtt desde cero

Construye un proyecto ESP-IDF desde una carpeta vacía con el componente esp-mqtt: configuración de Wi-Fi en modo estación, el manejador de eventos MQTT, publicación y suscripción, propiedades de MQTT 5 y TLS con el bundle de certificados.

9 min de lectura

Todos los artículos →

Tu broker MQTT privado, a unos pocos clics

Crea una cuenta, obtén las credenciales de tu broker y conecta tu primer dispositivo, sin servidores que instalar, parchear ni escalar.