ESP32 e Arduino

ESP32 MQTT SSL: MQTT Seguro sobre TLS com WiFiClientSecure

Conecte um ESP32 a um broker MQTT via TLS na porta 8883 com WiFiClientSecure e PubSubClient, valide o servidor com o certificado raiz correto, sincronize o relógio e mantenha o uso de heap sob controle.

Placa ESP32 com um cadeado conectando na porta 8883 em um tutorial de ESP32 MQTT SSL
Rode isso no seu próprio broker MQTTTenha um broker MQTT privado e gerenciado no plano Free e comece com exemplos de código prontos.
Teste grátis Exemplos de código Sem cartão de crédito

ESP32 MQTT SSL é o passo que transforma um sketch de hobby em um dispositivo que você pode deixar em uma rede real: credenciais e payloads são criptografados, e o ESP32 verifica que está falando com o broker legítimo. Neste tutorial você vai conectar um ESP32 ao broker.connectmq.com na porta 8883 com WiFiClientSecure e PubSubClient, validar o servidor com o certificado raiz correto, sincronizar o relógio via NTP e descobrir quanto de memória o TLS realmente custa. Você também vai receber um checklist para os erros de handshake que quase todo mundo encontra pelo menos uma vez.

O que o TLS acrescenta e o que ele não faz

O TLS no MQTT faz três coisas: criptografa a conexão para que ninguém no Wi-Fi ou no caminho até o broker consiga ler seu usuário, senha ou payloads; protege as mensagens contra adulteração; e, se configurado corretamente, autentica o servidor, de modo que um ponto de acesso falso não consiga se passar pelo seu broker.

Essa última parte é a que a maioria dos tutoriais pula. Chamar setInsecure() dá criptografia sem autenticação. Serve para um experimento de cinco minutos e é uma fraqueza real em produção, porque um atacante capaz de interceptar o DNS ou a rede pode apresentar qualquer certificado e coletar suas credenciais.

O TLS não substitui a autenticação. Do lado do broker, cada dispositivo ainda deve ter seu próprio usuário, senha e client ID. Para a visão geral, consulte o guia de boas práticas de segurança MQTT.

Porta Protocolo Uso
1883 MQTT sobre TCP Testes no servidor MQTT público (gratuito), nunca com credenciais nem dados de produção
8883 MQTT sobre TLS ESP32 e outros dispositivos em produção
8083 MQTT sobre WebSocket Testes no navegador
8084 MQTT sobre WebSocket seguro (/mqtt) Aplicações web em produção

Qual certificado raiz o ESP32 precisa?

O broker.connectmq.com usa um certificado emitido pela Let's Encrypt, cuja cadeia termina na raiz ISRG Root X1. Você mesmo pode conferir isso em qualquer computador com OpenSSL:

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

A saída lista o certificado do servidor para broker.connectmq.com, o intermediário da Let's Encrypt que o emitiu e uma cadeia que termina em ISRG Root X1, seguida de Verify return code: 0 (ok) quando o seu sistema confia nessa raiz.

A regra importante: entregue ao ESP32 a raiz, não o certificado do servidor. Os certificados de servidor da Let's Encrypt têm vida curta e são renovados automaticamente, e os intermediários também mudam com o tempo. Um dispositivo que fixa o certificado folha ou sua impressão digital vai parar de conectar após a próxima renovação. O certificado ISRG Root X1 é válido até 2035, então embuti-lo significa que seu firmware sobrevive a todas as renovações de rotina.

Pré-requisitos

  • Uma placa ESP32 com o pacote de placas esp32 da Espressif instalado na Arduino IDE 2.x.
  • PubSubClient, de Nick O'Leary, pelo Library Manager.
  • Um broker ConnectMQ: cadastre-se, escolha um plano, crie um broker e copie do portal o usuário, a senha e o client ID gerados. O plano Serverless gratuito é suficiente.

Se este é o seu primeiro sketch MQTT com ESP32, o tutorial de IoT com ESP32 cobre primeiro a configuração da placa e o MQTT sem criptografia; o TLS é mais fácil de depurar quando você sabe que a versão sem TLS funciona.

O sketch completo de ESP32 MQTT SSL

O sketch abaixo conecta o Wi-Fi, sincroniza a hora, carrega o certificado raiz, conecta na porta 8883 com credenciais, se inscreve em um tópico de comando e publica telemetria a cada dez 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), valid until 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) {   // any date after 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);              // seconds

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

Substitua os três placeholders de credenciais pelos valores do portal. Como o ConnectMQ vincula cada client ID às credenciais com as quais ele foi emitido, use o client ID gerado em vez de inventar um, e não rode dois dispositivos com o mesmo conjunto. Para ver as mensagens chegando, abra o explorador de tópicos ao vivo do portal ou o cliente MQTT no navegador e se inscreva em devices/#.

Por que a sincronização de hora importa no ESP32 MQTT SSL

Um certificado só é válido entre as datas "not before" e "not after". O ESP32 não tem relógio com bateria, então após cada boot ele acredita que a data é 1º de janeiro de 1970. Dependendo de como o mbedTLS está configurado na sua versão do core Arduino, as datas do certificado ou são verificadas contra esse relógio, caso em que todo certificado parece "ainda não válido" e o handshake falha, ou simplesmente não são verificadas, caso em que um certificado expirado seria aceito silenciosamente. Nenhuma das duas opções é o que você quer.

Sincronizar com NTP antes da primeira conexão TLS resolve os dois casos. Custa um ou dois segundos no boot e exige a porta UDP 123 de saída. Em redes que bloqueiam NTP, aponte configTime() para o servidor NTP local que o seu firewall permite. Ter a hora correta também é útil por si só: você pode colocar timestamps reais na telemetria, como descrito em timestamps NTP do ESP32 em mensagens MQTT.

Memória: quanto o TLS custa em um ESP32

O TLS é a tarefa que mais consome memória em um sketch típico de ESP32. Durante o handshake, o mbedTLS aloca buffers para registros de entrada e saída, para a cadeia de certificados sendo analisada e para a troca de chaves. O sketch imprime o heap livre antes e depois de conectar para que você veja o efeito na sua placa e na sua versão do core.

Fator Efeito O que fazer
Buffers de registros TLS Dezenas de kilobytes ocupados durante toda a conexão Reserve espaço para isso; evite um segundo cliente TLS simultâneo (por exemplo, OTA via HTTPS com o MQTT conectado) em placas com pouca memória
Análise da cadeia de certificados Pico temporário durante o handshake Embuta apenas a raiz necessária em vez de um bundle grande quando a RAM estiver apertada
Fragmentação do heap Muito tempo ligado mais muitas alocações de String podem não deixar nenhum bloco contíguo grande Prefira buffers char fixos e reconecte com menos frequência
Placas com PSRAM (WROVER, muitas S3) Mais folga Buffers grandes podem ser colocados na PSRAM pelo alocador do core
Buffer do PubSubClient setBufferSize(512) custa 512 bytes Dimensione para a sua maior mensagem, veja tamanho do buffer do PubSubClient

Um sinal de alerta prático: se as conexões começam a falhar após horas ou dias com erros de alocação de memória em lastError(), e um reboot resolve, você está diante de fragmentação. Registrar ESP.getMaxAllocHeap() junto com o heap livre mostra o maior bloco ainda disponível.

Checklist de solução de problemas

  1. X509 - Certificate verification failed. Certificado raiz errado, erro de digitação no PEM ou relógio não ajustado. Copie de novo o PEM incluindo as linhas BEGIN e END e confirme a hora NTP impressa no monitor serial.
  2. O handshake estoura o tempo. A rede bloqueia a porta 8883, algo comum em Wi-Fi corporativo e de visitantes. Teste com o comando OpenSSL acima a partir de um notebook na mesma rede.
  3. state=4 ou state=5 após um handshake bem-sucedido. O TLS está ok; as credenciais ou o client ID estão errados. Verifique espaços extras copiados do portal.
  4. Falhas de alocação de memória em lastError(). Não há heap contíguo suficiente. Libere memória antes de conectar, evite sessões TLS simultâneas e reduza a criação de String.
  5. Funciona na 1883, mas não na 8883. Você ainda está usando WiFiClient em vez de WiFiClientSecure, ou o setServer() aponta para a porta 1883.
  6. Conecta e cai mais ou menos a cada minuto. Normalmente é um problema de keep-alive ou de client ID duplicado, e não de TLS; os padrões de lógica de reconexão MQTT no ESP32 ajudam aqui.

Falhas repetidas de login podem acionar o bloqueio de IP no broker, então corrija erros de credencial antes de deixar um dispositivo em um loop rápido de novas tentativas.

Alternativas a embutir uma única raiz

Embutir a ISRG Root X1 é a opção robusta mais simples para um único broker. Existem duas alternativas. O core Arduino suporta um bundle de CAs via setCACertBundle(), que permite a um único firmware confiar em muitas raízes públicas ao custo de flash e de alguma configuração; isso é útil quando o mesmo dispositivo conversa com vários serviços HTTPS e MQTTS. Outra opção é guardar o PEM em uma partição de sistema de arquivos, para que ele possa ser atualizado sem regravar o código. Evite fixar a impressão digital do certificado em qualquer broker que use certificados renovados automaticamente.

Notas para produção

A criptografia protege o link; operar o broker com segurança é a outra metade. Um broker MQTT gerenciado cuida da renovação de certificados, do isolamento multi-tenant e da proteção contra força bruta para você, de modo que o firmware só precisa do certificado raiz e das suas credenciais. O ConnectMQ oferece TLS na porta 8883 e WebSocket seguro na 8084 com certificados Let's Encrypt, e as mesmas credenciais funcionam no seu ESP32, em um backend Python ou em um dashboard no navegador. Se você está pesando a hospedagem própria contra um broker MQTT na nuvem, o gerenciamento de certificados é uma das maiores diferenças no dia a dia da operação, e o guia sobre o que um broker MQTT faz explica as demais peças.

Comece com o ConnectMQ

Crie um broker Serverless gratuito em app.connectmq.com, sem cartão de crédito. Copie as credenciais geradas para o sketch acima e seu ESP32 estará usando TLS na porta 8883 em poucos minutos.

Perguntas frequentes

setInsecure() é aceitável para ESP32 MQTT sobre TLS?

Só para testes rápidos. Ele criptografa o tráfego, mas pula a verificação do servidor, então um atacante capaz de redirecionar seu tráfego pode capturar as credenciais. Use setCACert() com o certificado raiz do broker em tudo o que for para campo.

Qual certificado devo usar para broker.connectmq.com?

Use o certificado raiz ISRG Root X1, porque o certificado do broker é emitido pela Let's Encrypt. Não embuta o certificado do servidor em si; ele é renovado automaticamente e seu dispositivo deixaria de conectar.

Preciso de NTP para usar TLS no ESP32?

Você deve sincronizar a hora antes de conectar. Sem isso, as datas do certificado ou são comparadas com um relógio de 1970 e falham, ou não são verificadas, dependendo da configuração do core.

Quanta RAM o TLS precisa em um ESP32?

Conte com dezenas de kilobytes de heap enquanto uma conexão TLS está aberta, com um pico temporário maior durante o handshake. Meça na sua placa imprimindo ESP.getFreeHeap() antes e depois de conectar, como o sketch faz.

Artigos relacionados

Exemplo de MQTT com ESP-IDF: Monte um Cliente esp-mqtt do Zero

Monte um projeto ESP-IDF a partir de uma pasta vazia com o componente esp-mqtt: configuração do Wi-Fi em modo station, o handler de eventos MQTT, publicação e inscrição, propriedades do MQTT 5 e TLS com o bundle de certificados.

8 min de leitura

Usando os Recursos do MQTT 5 do esp-mqtt no ESP32 com ESP-IDF

Um projeto ESP-IDF prático que ativa o MQTT 5.0 no esp-mqtt e usa user properties, expiração de mensagens, topic aliases, expiração de sessão e response topics com um broker MQTT 5 real.

8 min de leitura

Todos os artigos →

Seu broker MQTT privado está a poucos cliques de distância

Crie uma conta, receba as credenciais do seu broker e conecte seu primeiro dispositivo — sem servidores para instalar, atualizar ou escalar.