An ESP32 that moves an MQTT connection from port 1883 to 8883 crosses more than a port boundary. The TCP stream is now expected to carry MQTT inside TLS. Data in transit is protected only when the TLS client also validates the broker certificate. MQTT username/password authentication is separate: credentials identify the client to the broker, while certificate validation identifies the broker to the ESP32.

Calling this “MQTT with HTTPS” mixes two application protocols. MQTT does not become HTTP when TLS is added. MQTT can run over plain TCP or over a TLS-protected TCP connection.

TLS sits below MQTT

A plain PubSubClient connection uses this stack:

PubSubClient
    |
MQTT packets
    |
WiFiClient
    |
TCP

For MQTT over TLS on ESP32, WiFiClientSecure replaces the plain transport:

PubSubClient
    |
MQTT packets
    |
WiFiClientSecure
    |
TLS
    |
TCP

PubSubClient still serializes MQTT CONNECT, SUBSCRIBE, PUBLISH, and other control packets. WiFiClientSecure handles the TLS session beneath them.

The EMQX public test broker documents port 1883 for plain MQTT and 8883 for MQTT over TLS. Private and managed brokers can use different listener ports, so firmware must follow the broker configuration it actually connects to.

Username and password belong to MQTT CONNECT

PubSubClient provides a connection overload with credentials:

client.connect(mqtt_client_id, mqtt_user, mqtt_pass);

Those values are carried by the MQTT connection exchange. They are not TLS handshake parameters.

TLS handshake
    |
    +-- ESP32 verifies broker certificate
    |
encrypted connection
    |
MQTT CONNECT
    |
    +-- client ID
    +-- username
    +-- password
    |
broker authentication / authorization

The broker must have an authentication mechanism configured for those credentials to have meaning. A public test broker may permit anonymous clients, while a private EMQX deployment can require username/password, mutual TLS, tokens, or another configured mechanism. Credentials therefore come from the broker configuration, not from arbitrary example values.

setInsecure() removes broker certificate verification

A common ESP32 test configuration is:

WiFiClientSecure espClient;

void setup() {
    espClient.setInsecure();
}

This can create an encrypted TLS session, but setInsecure() disables certificate verification. The ESP32 no longer proves that the certificate presented by the peer belongs to a trusted broker.

TLS + setInsecure()
    -> encrypted channel
    -> broker identity not verified

TLS + trusted CA
    -> encrypted channel
    -> certificate chain verified
    -> broker identity can be checked against its certificate

setInsecure() is useful during controlled diagnostics when certificate setup is deliberately bypassed. It is not an equivalent substitute for certificate validation on a device that sends credentials or telemetry over an untrusted network.

A trusted CA anchors the production connection

WiFiClientSecure can be given CA material used to validate the broker certificate chain:

#include <WiFiClientSecure.h>

static const char root_ca[] PROGMEM = R"EOF(
-----BEGIN CERTIFICATE-----
... trusted CA certificate ...
-----END CERTIFICATE-----
)EOF";

WiFiClientSecure espClient;

void setup() {
    espClient.setCACert(root_ca);
}

The placeholder must be replaced with CA material appropriate for the broker certificate chain. Copying an unrelated certificate into firmware either breaks validation or establishes the wrong trust boundary.

Certificate lifetime is also an operational constraint. Devices that embed trust anchors need a rotation strategy when certificate infrastructure changes instead of an emergency fallback to setInsecure().

The sensor sketch changes at the transport boundary

The DHT22 and LED logic does not need to change merely because the MQTT transport becomes TLS. The relevant changes are WiFiClientSecure, the TLS listener port, certificate trust, and the credential-bearing connect() call:

#include <WiFi.h>
#include <WiFiClientSecure.h>
#include <PubSubClient.h>
#include "DHT.h"

const char* ssid = "YOUR_WIFI_SSID";
const char* wifi_password = "YOUR_WIFI_PASSWORD";

const char* mqtt_server = "broker.emqx.io";
const uint16_t mqtt_port = 8883;
const char* mqtt_client_id = "YOUR_UNIQUE_CLIENT_ID";

const char* mqtt_user = "YOUR_MQTT_USERNAME";
const char* mqtt_pass = "YOUR_MQTT_PASSWORD";

const char* topic_sensor = "device/sensor";
const char* topic_led = "device/lamp";

#define DHTPIN 4
#define DHTTYPE DHT22
#define LED_PIN 5

static const char root_ca[] PROGMEM = R"EOF(
-----BEGIN CERTIFICATE-----
... CA certificate for the broker ...
-----END CERTIFICATE-----
)EOF";

WiFiClientSecure espClient;
PubSubClient client(espClient);
DHT dht(DHTPIN, DHTTYPE);

unsigned long lastPublish = 0;
const unsigned long interval = 5000;

void callback(char* topic, byte* payload, unsigned int length) {
    char message[length + 1];
    memcpy(message, payload, length);
    message[length] = '\0';

    if (strcmp(topic, topic_led) == 0) {
        if (strcmp(message, "ON") == 0) {
            digitalWrite(LED_PIN, HIGH);
        } else if (strcmp(message, "OFF") == 0) {
            digitalWrite(LED_PIN, LOW);
        }
    }
}

void setup_wifi() {
    WiFi.mode(WIFI_STA);
    WiFi.begin(ssid, wifi_password);

    while (WiFi.status() != WL_CONNECTED) {
        delay(500);
    }
}

void connect_mqtt() {
    if (client.connected()) {
        return;
    }

    if (client.connect(mqtt_client_id, mqtt_user, mqtt_pass)) {
        client.subscribe(topic_led, 1);
    }
}

void setup() {
    Serial.begin(115200);
    pinMode(LED_PIN, OUTPUT);
    dht.begin();
    setup_wifi();

    espClient.setCACert(root_ca);
    client.setServer(mqtt_server, mqtt_port);
    client.setCallback(callback);
}

void loop() {
    if (WiFi.status() != WL_CONNECTED) {
        setup_wifi();
    }

    connect_mqtt();
    client.loop();

    if (millis() - lastPublish >= interval) {
        lastPublish = millis();

        float temperature = dht.readTemperature();
        float humidity = dht.readHumidity();
        if (isnan(temperature) || isnan(humidity)) {
            return;
        }

        char payload[64];
        snprintf(
            payload,
            sizeof(payload),
            "{\"temperature\":%.2f,\"humidity\":%.2f}",
            temperature,
            humidity
        );

        client.publish(topic_sensor, payload, true);
        Serial.println(payload);
    }
}

Wi-Fi credentials, MQTT credentials, client ID, topics, and CA material are deployment values. The EMQX public broker is intended for testing and demonstration; private device secrets and telemetry should not depend on a shared public test broker.

TLS does not replace topic authorization

Successful username/password authentication should not grant access to every topic. Broker authorization can separately restrict which topics an identity may publish to and which topic filters it may subscribe to.

publish:   device/123/sensor
subscribe: device/123/lamp
deny:      everything else

A narrow policy limits the impact of leaked credentials. Without topic-level authorization, a valid account can have broader broker access than the firmware requires.

Reconnection needs failure visibility

A reconnect function that silently returns on failure makes TLS and authentication problems difficult to separate. PubSubClient exposes client.state() for MQTT-level connection failures, while TLS can fail before MQTT receives any broker response.

Production firmware should also avoid a tight reconnect loop. Delay or backoff reduces repeated handshakes during broker outages, certificate failures, invalid credentials, and unstable Wi-Fi.

Wi-Fi unavailable
        |
TCP/TLS connection fails
        |
certificate validation fails
        |
MQTT CONNECT rejected
        |
subscribe/publish authorization fails

Treating every condition as only “MQTT disconnected” hides the layer that actually rejected the connection.

Secure MQTT is a composition of controls

Each layer has a distinct job. TLS encrypts the TCP stream. CA validation authenticates the broker certificate. MQTT credentials authenticate the client according to the broker configuration. Topic authorization constrains what that authenticated identity may do.

Changing 1883 to 8883 is only the transport entry point. The security boundary is complete when certificate verification, credential handling, broker authentication, and topic authorization are configured together without substituting one mechanism for another.