Mantener credenciales MQTT seguras en los dispositivos significa garantizar que el nombre de usuario y la contraseña que usa tu dispositivo para llegar al broker no puedan leerse desde imágenes de firmware, volcados de chips de flash, puertos de depuración ni archivos de log. Esta guía compara las opciones prácticas en dispositivos ESP32 y Linux, muestra la configuración de cada una y enumera los errores que con más frecuencia filtran credenciales sobre el terreno.
El modelo de amenazas en términos sencillos
Antes de elegir un método de almacenamiento, ten claro de quién te estás defendiendo:
| Atacante | Acceso | Lo que intenta |
|---|---|---|
| Alguien con tu archivo de firmware | Un .bin de una página de descargas o de un ticket de soporte |
Buscar cadenas en la imagen |
| Alguien con un dispositivo | Acceso físico, un adaptador USB-serie | Volcar la flash por UART, conectar JTAG |
| Malware o un fallo en el dispositivo | Código que se ejecuta en el dispositivo | Leer archivos o memoria |
| Alguien en la red | Captura de paquetes | Leer la contraseña en CONNECT |
Del atacante de red se encarga TLS en el puerto 8883 (u 8084 para WebSockets), que todos los brokers de ConnectMQ admiten con un certificado de Let's Encrypt; la autenticación MQTT con usuario y contraseña explica por qué las credenciales nunca deben viajar por el puerto 1883. Este artículo se centra en los otros tres: proteger el secreto en reposo dentro del dispositivo.
Regla cero: las credenciales no van en el firmware
Un #define MQTT_PASSWORD "..." en el código fuente acaba en el binario del firmware, en tu historial de Git, en los logs de CI y en cada copia de la imagen. Ejecutar strings firmware.bin | grep -i pass sobre un proyecto aficionado típico suele encontrarla en segundos. El firmware debe ser idéntico en todos los dispositivos; las credenciales deben ser únicas por dispositivo. Mantenlos separados y escribe las credenciales en el almacenamiento durante el aprovisionamiento seguro de dispositivos IoT.
Opciones en el ESP32
NVS sin cifrar (base, no segura)
La biblioteca Non-Volatile Storage (o el wrapper Preferences de Arduino) guarda pares clave-valor en una partición de flash. Separa las credenciales del firmware, lo que ya es una mejora, pero los datos se almacenan sin cifrar. Cualquiera que pueda leer el chip de flash puede leer la contraseña.
Cifrado de NVS más cifrado de flash
ESP-IDF puede cifrar las particiones NVS. En el esquema clásico, las claves de cifrado de NVS viven en una partición nvs_keys dedicada, y esa partición está protegida por el cifrado de flash, cuya clave se guarda en eFuses que el software no puede leer. Los chips más recientes, como el ESP32-S3 y el ESP32-C3, también admiten un esquema basado en HMAC que deriva las claves de NVS de una clave en eFuse sin partición nvs_keys.
Una tabla de particiones con una partición de claves cifrada:
# 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,
Opciones relevantes de sdkconfig:
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
El modo release es importante: el modo de desarrollo deja vías para volver a flashear imágenes en texto claro, lo que anula el objetivo en las unidades distribuidas. Secure Boot complementa el cifrado negándose a ejecutar firmware que no hayas firmado, de modo que un atacante no puede flashear código que lea e imprima las credenciales descifradas. Ambos queman eFuses de forma irreversible, así que prueba primero en placas de repuesto y sigue paso a paso la documentación de cifrado de flash de Espressif.
Leer las credenciales en un sketch de Arduino para ESP32 es igual con NVS cifrado o sin cifrar, y esa es precisamente la idea: la protección está por debajo de la API.
#include <Preferences.h>
struct MqttCreds { String user, pass, clientId; };
bool loadCreds(MqttCreds &c) {
Preferences prefs;
if (!prefs.begin("mqtt", true)) return false; // solo lectura
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) {
// Registra la identidad, nunca la contraseña
Serial.printf("MQTT user=%s clientId=%s pass=<%u bytes>\n",
c.user.c_str(), c.clientId.c_str(), c.pass.length());
}
Elementos seguros
Un elemento seguro como el ATECC608 de Microchip es un chip independiente que guarda claves y realiza operaciones criptográficas sin liberar nunca las claves privadas. Es más potente para la identidad basada en certificados, donde la clave privada del TLS mutuo nunca sale del chip. Con autenticación por contraseña, un elemento seguro puede seguir ayudando al guardar la clave que cifra el blob de la contraseña en la flash, lo que dificulta la extracción física. Añade coste a la lista de materiales, espacio en la placa e integración de drivers, por lo que es más habitual en productos con datos de alto valor o presión regulatoria.
Opciones en dispositivos Linux
Los gateways y equipos edge con Linux disponen de herramientas más completas.
Permisos de archivo (base)
Guarda las credenciales en un archivo que solo pueda leer la cuenta de servicio, fuera del directorio de la aplicación:
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
Esto protege frente a otros usuarios y servicios, no frente a alguien que extraiga la tarjeta SD. Combínalo con cifrado de disco completo para obtener protección física.
Credenciales cifradas de systemd con TPM
En sistemas con systemd y un chip TPM 2.0, systemd-creds cifra un secreto para que solo pueda descifrarse en esa máquina y únicamente para el servicio que lo declara:
# Cifra la contraseña, vinculada al TPM de esta máquina
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
El servicio lee el secreto descifrado desde un directorio privado en memoria:
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()
Una imagen de disco copiada no sirve de nada en otra máquina, porque el TPM no desprecintará la credencial.
Comparativa de opciones de almacenamiento
| Método | Plataforma | Protege frente a filtraciones del firmware | Protege frente a volcados de flash | Esfuerzo |
|---|---|---|---|---|
| Incrustada en el firmware | Cualquiera | No | No | Ninguno |
| NVS sin cifrar / archivo de configuración | ESP32 / Linux | Sí | No | Bajo |
| Cifrado de NVS más cifrado de flash, Secure Boot | ESP32 | Sí | Sí | Medio, eFuses irreversibles |
| Elemento seguro | ESP32, Linux | Sí | Sí, el más sólido para claves | Alto |
| Permisos de archivo más cifrado de disco | Linux | Sí | Parcialmente | Bajo a medio |
| systemd-creds con TPM | Linux | Sí | Sí | Medio |
Credenciales en memoria, copias de seguridad y actualizaciones
El almacenamiento cifrado protege la credencial en reposo, pero aun así hay que descifrarla en la RAM para construir el paquete CONNECT. Algunos hábitos reducen la exposición en el resto de su recorrido:
- Mantenla en un solo lugar. Carga la credencial una vez en un único buffer propiedad del módulo MQTT en lugar de copiarla en cadenas globales, estructuras de configuración y objetos de diagnóstico.
- Exclúyela de los diagnósticos. Los volcados de núcleo, los informes de fallos y las subidas remotas de logs nunca deben contener el almacén de credenciales. En Linux, deja el archivo de credenciales fuera de las copias de seguridad y de los paquetes de soporte.
- No la hagas pasar por la nube. Una función de sincronización de configuración que sube los ajustes del dispositivo a tu backend debe omitir por completo los campos de credenciales.
- Escribe las actualizaciones de forma atómica. Cuando llegue una nueva credencial durante la rotación, escríbela en una segunda ranura y cambia solo tras una conexión correcta, para que un corte de luz no deje al dispositivo con una contraseña escrita a medias.
- Bórrala al retirar el dispositivo. Elimina el espacio de nombres NVS o el archivo de credenciales como parte de los procedimientos de restablecimiento de fábrica y retirada.
Errores que filtran credenciales
- Imprimir la contraseña en logs de depuración o volcados de fallos.
- Mostrarla en una página web del dispositivo "por comodidad", aunque esté enmascarada en el HTML.
- Guardarla en un mensaje MQTT retenido o enviarla a un topic compartido.
- Dejar activados el modo de descarga por UART y JTAG en las unidades de producción.
- Usar una sola credencial para toda la flota, de modo que una extracción lo compromete todo.
- No rotar nunca, de modo que una credencial filtrada hace años sigue funcionando.
El punto 5 merece especial atención. En ConnectMQ cada client ID está vinculado a sus credenciales emitidas, así que da a cada dispositivo su propio conjunto; una contraseña extraída expondrá entonces un único dispositivo, que puedes revocar y sustituir con el proceso descrito en cómo rotar credenciales MQTT. El aislamiento de tenants hace que incluso las credenciales de un dispositivo comprometido solo lleguen a tu propio broker, y el bloqueo de IP tras inicios de sesión fallidos repetidos frena a los atacantes que intentan adivinar otras contraseñas. El conjunto más amplio de controles se trata en las buenas prácticas de seguridad MQTT.
Empieza con ConnectMQ
Combina el almacenamiento protegido en el dispositivo con un broker MQTT gestionado que se encargue por ti de TLS y de la emisión de credenciales. Crea un broker Serverless gratuito en app.connectmq.com, sin tarjeta de crédito, y prueba el almacenamiento de credenciales de tu ESP32 o dispositivo Linux contra un endpoint TLS real. Si eres nuevo en la parte de hardware, el tutorial de MQTT con ESP32 es un buen punto de partida, y la guía del broker MQTT en la nube explica las opciones de alojamiento.
Preguntas frecuentes
¿Es segura la biblioteca Preferences del ESP32 para contraseñas MQTT?
Solo si están activados el cifrado de NVS y el cifrado de flash. Sin ellos, Preferences guarda los datos en texto claro en la flash, que cualquiera con acceso físico puede volcar.
¿Necesito un elemento seguro para las credenciales MQTT?
No siempre. El almacenamiento cifrado con cifrado de flash y Secure Boot es suficiente para muchos productos; un elemento seguro aporta más valor en identidades basadas en certificados o despliegues de alto valor.
¿Dónde debe guardar su contraseña MQTT un gateway Linux?
Como mínimo, en un archivo con permisos 0600 propiedad de la cuenta de servicio. Si hay un TPM disponible, las credenciales cifradas de systemd vinculan el secreto a esa máquina.
¿TLS protege las credenciales almacenadas en el dispositivo?
No. TLS protege las credenciales en tránsito. La protección del almacenamiento es una capa independiente que necesitas en el propio dispositivo.



