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
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 linhasBEGINeENDe confirme a hora NTP impressa no monitor serial.- 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.
state=4oustate=5apó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.- 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 deString. - Funciona na 1883, mas não na 8883. Você ainda está usando
WiFiClientem vez deWiFiClientSecure, ou osetServer()aponta para a porta 1883. - 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.



