Keeping secure MQTT credentials on devices means making sure the username and password your device uses to reach the broker cannot be read from firmware images, dumped flash chips, debug ports or log files. This guide compares the practical options on ESP32 and Linux devices, shows the configuration for each, and lists the mistakes that most often leak credentials in the field.
The Threat Model in Plain Terms
Before choosing a storage method, be clear about who you are defending against:
| Attacker | Access | What they try |
|---|---|---|
| Someone with your firmware file | A .bin from a download page or support ticket |
Search the image for strings |
| Someone with a device | Physical access, a USB-serial adapter | Dump flash over UART, attach JTAG |
| Malware or a bug on the device | Code running on the device | Read files or memory |
| Someone on the network | Packet capture | Read the password from CONNECT |
The network attacker is handled by TLS on port 8883 (or 8084 for WebSockets), which every ConnectMQ broker supports with a Let's Encrypt certificate; MQTT username password authentication explains why credentials must never travel over port 1883. This article focuses on the other three: protecting the secret at rest on the device.
Rule Zero: Credentials Do Not Belong in Firmware
A #define MQTT_PASSWORD "..." in source code ends up in the firmware binary, in your Git history, in CI logs and in every copy of the image. Running strings firmware.bin | grep -i pass on a typical hobby project often finds it within seconds. Firmware must be identical for every device; credentials must be unique per device. Keep them separate and write credentials into storage during secure IoT device provisioning.
Options on the ESP32
Plain NVS (Baseline, Not Secure)
The Non-Volatile Storage library (or the Arduino Preferences wrapper) stores key-value pairs in a flash partition. It separates credentials from firmware, which is already an improvement, but the data is stored unencrypted. Anyone who can read the flash chip can read the password.
NVS Encryption Plus Flash Encryption
ESP-IDF can encrypt NVS partitions. In the classic scheme the NVS encryption keys live in a dedicated nvs_keys partition, and that partition is protected by flash encryption, whose key is stored in eFuses that software cannot read back. Newer chips such as the ESP32-S3 and ESP32-C3 also support an HMAC-based scheme that derives NVS keys from an eFuse key without an nvs_keys partition.
A partition table with an encrypted keys partition:
# Name, Type, SubType, Offset, Size, Flags
nvs, data, nvs, , 0x6000,
nvs_keys, data, nvs_keys, , 0x1000, encrypted
phy_init, data, phy, , 0x1000,
factory, app, factory, , 1M,
Relevant sdkconfig options:
CONFIG_SECURE_FLASH_ENC_ENABLED=y
CONFIG_SECURE_FLASH_ENCRYPTION_MODE_RELEASE=y
CONFIG_NVS_ENCRYPTION=y
CONFIG_SECURE_BOOT=y
CONFIG_SECURE_BOOT_V2_ENABLED=y
Release mode matters: development mode leaves ways to reflash plain-text images, which defeats the purpose on shipped units. Secure Boot complements encryption by refusing to run firmware that you did not sign, so an attacker cannot flash code that reads and prints the decrypted credentials. Both burn eFuses irreversibly, so test on spare boards first and follow the Espressif flash encryption documentation step by step.
Reading the credentials in an Arduino-on-ESP32 sketch looks the same whether NVS is encrypted or not, which is exactly the point: the protection is below the API.
#include <Preferences.h>
struct MqttCreds { String user, pass, clientId; };
bool loadCreds(MqttCreds &c) {
Preferences prefs;
if (!prefs.begin("mqtt", true)) return false; // read-only
c.user = prefs.getString("user", "");
c.pass = prefs.getString("pass", "");
c.clientId = prefs.getString("cid", "");
prefs.end();
return c.user.length() && c.pass.length() && c.clientId.length();
}
void logCreds(const MqttCreds &c) {
// Log identity, never the password
Serial.printf("MQTT user=%s clientId=%s pass=<%u bytes>\n",
c.user.c_str(), c.clientId.c_str(), c.pass.length());
}
Secure Elements
A secure element such as Microchip's ATECC608 is a separate chip that stores keys and performs cryptographic operations without ever releasing private keys. It is strongest for certificate-based identity, where the private key for mutual TLS never leaves the chip. With password authentication, a secure element can still help by holding the key used to encrypt the password blob in flash, raising the bar for physical extraction. It adds BOM cost, board space and driver integration, so it is most common in products with high-value data or regulatory pressure.
Options on Linux Devices
Gateways and edge computers running Linux have richer tools.
File Permissions (Baseline)
Store credentials in a file readable only by the service account, outside the application directory:
sudo install -d -m 0700 -o mqttsvc -g mqttsvc /etc/mqtt-agent
sudo install -m 0600 -o mqttsvc -g mqttsvc credentials.env /etc/mqtt-agent/credentials.env
This protects against other users and services, not against someone who removes the SD card. Combine it with full-disk encryption for physical protection.
systemd Encrypted Credentials With a TPM
On systems with systemd and a TPM 2.0 chip, systemd-creds encrypts a secret so it can only be decrypted on that machine, and only for the service that declares it:
# Encrypt the password, bound to this machine's TPM
echo -n 'YOUR_PASSWORD' | sudo systemd-creds encrypt --with-key=tpm2 \
--name=mqtt-password - /etc/credstore.encrypted/mqtt-password
# /etc/systemd/system/mqtt-agent.service
[Unit]
Description=MQTT telemetry agent
After=network-online.target
Wants=network-online.target
[Service]
User=mqttsvc
LoadCredentialEncrypted=mqtt-password:/etc/credstore.encrypted/mqtt-password
Environment=MQTT_USERNAME=YOUR_USERNAME MQTT_CLIENT_ID=YOUR_CLIENT_ID
ExecStart=/usr/bin/python3 /opt/mqtt-agent/agent.py
Restart=on-failure
RestartSec=10
[Install]
WantedBy=multi-user.target
The service reads the decrypted secret from a private, in-memory directory:
import os
import ssl
import paho.mqtt.client as mqtt
cred_dir = os.environ["CREDENTIALS_DIRECTORY"]
with open(os.path.join(cred_dir, "mqtt-password")) as f:
password = f.read().strip()
client = mqtt.Client(mqtt.CallbackAPIVersion.VERSION2,
client_id=os.environ["MQTT_CLIENT_ID"],
protocol=mqtt.MQTTv5)
client.username_pw_set(os.environ["MQTT_USERNAME"], password)
client.tls_set(cert_reqs=ssl.CERT_REQUIRED)
client.connect("YOUR_BROKER_HOST", 8883, keepalive=60)
client.loop_forever()
A copied disk image is useless on another machine because the TPM will not unseal the credential.
Comparison of Storage Options
| Method | Platform | Protects against firmware leaks | Protects against flash dump | Effort |
|---|---|---|---|---|
| Hard-coded in firmware | Any | No | No | None |
| Plain NVS / config file | ESP32 / Linux | Yes | No | Low |
| NVS encryption plus flash encryption, Secure Boot | ESP32 | Yes | Yes | Medium, irreversible eFuses |
| Secure element | ESP32, Linux | Yes | Yes, strongest for keys | High |
| File permissions plus disk encryption | Linux | Yes | Partially | Low to medium |
| systemd-creds with TPM | Linux | Yes | Yes | Medium |
Credentials in Memory, Backups and Updates
Encrypted storage protects the credential at rest, but it still has to be decrypted into RAM to build the CONNECT packet. A few habits reduce exposure along the rest of its path:
- Keep it in one place. Load the credential once into a single buffer owned by the MQTT module instead of copying it into global strings, configuration structures and diagnostic objects.
- Exclude it from diagnostics. Core dumps, crash reports and remote log uploads should never contain the credential store. On Linux, keep the credential file out of backup sets and support bundles.
- Do not round-trip it through the cloud. A configuration sync feature that uploads the device's settings to your backend should skip the credential fields entirely.
- Write updates atomically. When a new credential arrives during rotation, write it to a second slot and switch only after a successful connection, so a power cut cannot leave the device with a half-written password.
- Wipe on decommissioning. Erase the NVS namespace or credential file as part of the factory reset and decommissioning procedures.
Mistakes That Leak Credentials
- Printing the password in debug logs or crash dumps.
- Exposing it on a device web page "for convenience", even masked in HTML.
- Storing it in a retained MQTT message or sending it to a shared topic.
- Leaving UART download mode and JTAG enabled on production units.
- Using one credential for the whole fleet, so one extraction compromises everything.
- Never rotating, so a credential leaked years ago still works.
Point 5 deserves emphasis. On ConnectMQ each client ID is bound to its issued credentials, so give every device its own set; an extracted password then exposes one device, which you can revoke and replace with the process in how to rotate MQTT credentials. Tenant isolation means even a compromised device's credentials only reach your own broker, and IP blocking after repeated failed logins slows attackers guessing other passwords. The wider set of controls is covered in MQTT security best practices.
Get Started with ConnectMQ
Pair protected storage on the device with a fully managed MQTT service that handles TLS and credential issuance for you. Create a free Serverless broker at app.connectmq.com, no credit card required, and test your ESP32 or Linux credential storage against a real TLS endpoint. If you are new to the hardware side, the ESP32 MQTT tutorial is a good starting point, and the cloud MQTT broker guide explains the hosting options.
Frequently Asked Questions
Is the ESP32 Preferences library secure for MQTT passwords?
Only if NVS encryption and flash encryption are enabled. Without them, Preferences stores data in plain text in flash, which anyone with physical access can dump.
Do I need a secure element for MQTT credentials?
Not always. Encrypted storage with flash encryption and Secure Boot is adequate for many products; a secure element is most valuable for certificate-based identity or high-value deployments.
Where should a Linux gateway store its MQTT password?
At minimum in a 0600 file owned by the service account. Where a TPM is available, systemd encrypted credentials bind the secret to that machine.
Does TLS protect credentials stored on the device?
No. TLS protects credentials in transit. Storage protection is a separate layer you need on the device itself.



