IoT Security Basics: TLS for MQTT, Device Credentials and Safe Updates
Most early IoT projects send passwords and sensor data in plain text, use one shared password for every device, and never receive updates. That is acceptable for a sensor on your desk, but not for a relay that can switch a water pump or a door lock. Insecure devices have been hijacked in large numbers and used in botnets, and a compromised node on your home network is a doorway to everything else on it.
This tutorial covers the practical basics for an ESP32 and Mosquitto system: encrypting traffic with TLS, making the device verify it is talking to the real broker, giving each device its own credentials and limited permissions, keeping secrets out of source code, and keeping firmware updatable.
Threats and Matching Defences
| Threat | Example | Defence |
|---|---|---|
| Eavesdropping | Someone on the same Wi-Fi captures MQTT passwords with Wireshark | TLS encryption on port 8883 |
| Impersonating the server | A fake broker collects device logins | Device verifies the broker certificate against a trusted CA |
| Stolen device credentials | One node is opened and its password read | Unique credentials per device, ACLs limiting each to its own topics |
| Unauthorised commands | Anyone publishes ON to your relay topic | Authentication plus ACLs on command topics |
| Firmware extraction | Flash dumped over the serial port | ESP32 flash encryption, no secrets in public repositories |
| Unpatched bugs | A known flaw is never fixed | OTA updates over HTTPS, ideally signed |
| Default passwords | Portal or admin page left at factory password | Force a unique password at provisioning |
Step 1: Certificates and a TLS Listener
For a private system you can be your own certificate authority (CA). You create a CA key and certificate once, then use it to sign the broker's certificate. Devices store only the CA certificate, which is public information. Run these commands with OpenSSL on the broker machine, replacing the IP and hostname with your own.
# 1. Certificate authority (keep ca.key secret and offline)
openssl genrsa -out ca.key 2048
openssl req -x509 -new -key ca.key -sha256 -days 3650 \
-subj "/CN=Home IoT CA" -out ca.crt
# 2. Broker key and signing request
openssl genrsa -out server.key 2048
openssl req -new -key server.key -subj "/CN=broker.local" -out server.csr
# 3. Sign it; the SAN must contain the name or IP the ESP32 connects to
printf "subjectAltName=DNS:broker.local,IP:192.168.1.50" > san.ext
openssl x509 -req -in server.csr -CA ca.crt -CAkey ca.key -CAcreateserial \
-days 825 -sha256 -extfile san.ext -out server.crt
sudo mkdir -p /etc/mosquitto/certs
sudo cp ca.crt server.crt server.key /etc/mosquitto/certs/
sudo chown mosquitto:mosquitto /etc/mosquitto/certs/server.key
sudo chmod 600 /etc/mosquitto/certs/server.key
# Encrypted MQTT only; remove any plain 'listener 1883' block
listener 8883
cafile /etc/mosquitto/certs/ca.crt
certfile /etc/mosquitto/certs/server.crt
keyfile /etc/mosquitto/certs/server.key
allow_anonymous false
password_file /etc/mosquitto/passwd
acl_file /etc/mosquitto/acl
# Node-RED backend can read and write everything under home/
user nodered
topic readwrite home/#
# Every other user: %u is replaced by the username,
# so user 'node1' can only use home/node1/...
pattern readwrite home/%u/#
Create a separate password entry for every device with mosquitto_passwd, restart Mosquitto, and test from the PC with mosquitto_sub --cafile ca.crt -h 192.168.1.50 -p 8883 -u node1 -P <password> -t 'home/node1/#'. Then try subscribing as node1 to home/node2/#: no messages arrive, because the ACL silently blocks them.
Step 2: ESP32 MQTT over TLS
Keep credentials in a separate secrets.h file and add it to .gitignore, so your repository can be public without leaking passwords. Paste the contents of ca.crt into the sketch as a string.
#include <WiFi.h>
#include <WiFiClientSecure.h>
#include <PubSubClient.h>
#include <time.h>
#include "secrets.h" // WIFI_SSID, WIFI_PASS, MQTT_USER, MQTT_PASS
const char* MQTT_HOST = "192.168.1.50"; // must appear in the cert SAN
// Contents of ca.crt (public, not secret)
const char* CA_CERT = R"EOF(
-----BEGIN CERTIFICATE-----
...paste your ca.crt here...
-----END CERTIFICATE-----
)EOF";
WiFiClientSecure tlsClient;
PubSubClient mqtt(tlsClient);
void syncTime() {
// Correct time lets the device check certificate expiry dates
configTime(19800, 0, "pool.ntp.org"); // IST = UTC + 5:30 = 19800 s
struct tm now;
getLocalTime(&now, 10000); // wait up to 10 s
}
void setup() {
Serial.begin(115200);
WiFi.begin(WIFI_SSID, WIFI_PASS);
while (WiFi.status() != WL_CONNECTED) delay(500);
syncTime();
tlsClient.setCACert(CA_CERT); // reject any other server
mqtt.setServer(MQTT_HOST, 8883);
if (mqtt.connect("node1", MQTT_USER, MQTT_PASS)) {
mqtt.publish("home/node1/status", "online (TLS)", true);
Serial.println("Secure MQTT connected");
} else {
char err[100];
tlsClient.lastError(err, sizeof(err)); // TLS-level reason
Serial.printf("Failed, state %d, TLS: %s", mqtt.state(), err);
Serial.println();
}
}
void loop() {
mqtt.loop();
}
A matching secrets.h sits in the same folder as the sketch and contains only #define lines, for example #define MQTT_USER "node1". Commit a secrets.example.h with placeholder values so other people know which settings they must fill in. For devices provisioned in the field, the values can come from WiFiManager and Preferences instead, as shown in the provisioning tutorial.
Avoid setInsecure() outside quick experiments. It still encrypts, but it accepts any certificate, so a fake broker on the network would be trusted. With setCACert(), the handshake fails unless the broker presents a certificate signed by your CA for the address the ESP32 connected to.
Step 3: Hardening the Device and Network
- Unique credentials per device. If one node is stolen, delete only its entry from the password file.
- Flash encryption and secure boot. The ESP32 supports both through eFuses. Flash encryption stops someone reading your Wi-Fi password from a dumped flash image; secure boot stops unsigned firmware from running. Enabling them is permanent on that chip, so practise on a spare board and follow Espressif's documentation carefully.
- Updates. Plan OTA from the start, serve images over HTTPS, and check a version number. Devices that cannot be updated stay vulnerable forever.
- Separate network. Put IoT devices on a guest network or separate VLAN so a compromised bulb cannot reach your laptop.
- No port forwarding of 1883. If you need remote access, use TLS on 8883 with strong passwords, or a VPN to reach the home network.
- Minimal services. Remove debug web pages, Telnet consoles and unused OTA ports from production firmware.
Troubleshooting TLS
- Handshake fails with a certificate verification error: the address used by the ESP32 is not in the certificate's SAN, or the wrong CA was pasted.
- Works with mosquitto_sub but not the ESP32: check free heap, since TLS needs a large buffer, and make sure the CA string includes the BEGIN and END lines.
- Connection refused after enabling the ACL: the device's username does not match the topic path used by the
%upattern. - Broker fails to start: Mosquitto cannot read
server.key; check its owner and permissions, and read the log withjournalctl -u mosquitto.
Summary
Basic IoT security is a set of habits rather than one feature: TLS with certificate verification on every connection, a separate login and ACL for every device, secrets kept out of source code, flash protection for devices that leave your hands, an isolated network, and a working update path. Apply them from the first prototype and they cost very little.