El cifrado del payload MQTT consiste en cifrar el propio cuerpo del mensaje, de modo que solo el publicador y los suscriptores previstos puedan leerlo, sin importar quién opere el broker. En esta guía aprenderás cuándo merece la pena añadir cifrado extremo a extremo además de TLS, cómo diseñar el formato del mensaje de forma segura y cómo implementarlo en Python con AES-GCM sobre una conexión real a un broker.
TLS frente al cifrado del payload MQTT
TLS cifra la conexión entre cada cliente y el broker. Es imprescindible y deberías usarlo siempre; consulta nuestra guía de configuración TLS en MQTT. Pero TLS termina en el broker. Dentro del broker, un mensaje está en texto plano: tiene que estarlo para que el broker pueda enrutarlo, guardarlo como retained message o encolarlo para una sesión sin conexión.
El cifrado del payload añade una segunda capa que viaja con el mensaje desde el publicador hasta el suscriptor.
| Propiedad | Solo TLS | TLS + cifrado del payload |
|---|---|---|
| Protegido en la red | Sí | Sí |
| Legible por el operador del broker | Sí | No |
| Legible en mensajes retenidos o encolados en reposo | Sí | No |
| Nombres de topic ocultos | No (visibles para el broker) | No (siguen visibles para el broker) |
| El broker puede inspeccionar o decodificar los payloads | Sí | No |
| Gestión de claves necesaria | Solo el certificado del broker | Claves de aplicación en cada cliente |
Fíjate en las dos filas con "No" que la gente suele pasar por alto. Los topics siguen siendo legibles, porque el broker los necesita para enrutar, así que nunca pongas secretos en los nombres de topic. Y cualquier función del broker que lea payloads deja de funcionar con mensajes cifrados. Por ejemplo, el explorador de topics en vivo, las reglas de alerta por umbral y la decodificación de Sparkplug B de ConnectMQ solo pueden mostrar o evaluar datos que sean capaces de analizar, así que los payloads cifrados aparecerán ahí como bytes opacos.
Cuándo tiene sentido el cifrado extremo a extremo
Añade cifrado del payload cuando:
- La normativa o los contratos exigen que los proveedores de infraestructura no puedan leer los datos.
- Los mensajes pasan por brokers, bridges o gateways compartidos o públicos que no controlas por completo.
- Los mensajes retenidos o las sesiones encoladas contienen valores sensibles, como datos personales o códigos de acceso.
- Quieres defensa en profundidad por si se filtran las credenciales de una parte del sistema.
Prescinde de él cuando necesites visibilidad en el broker (dashboards en el portal del broker, reglas de alerta sobre valores) y confíes en el operador, o cuando los dispositivos con recursos limitados no puedan permitirse el código extra y la gestión de claves. Muchos equipos cifran solo unos pocos topics sensibles, como .../commands/unlock, y dejan la telemetría en texto plano sobre TLS.
Elegir un algoritmo
Usa un algoritmo de cifrado autenticado. Cifra el payload y añade una etiqueta que detecta cualquier modificación. Dos buenas opciones:
- AES-256-GCM: ampliamente disponible, acelerado por hardware en muchas CPU y en el ESP32 (a través de mbedTLS). Requiere un nonce único de 96 bits por mensaje con la misma clave.
- XChaCha20-Poly1305 (libsodium
crypto_aead_xchacha20poly1305_ietf): rápido en software, y su nonce de 192 bits puede generarse aleatoriamente sin riesgo realista de colisión.
No uses AES-CBC sin MAC, ningún tipo de modo ECB ni "cifrado" con XOR o Base64. Dan una falsa sensación de seguridad.
Diseñar el formato del mensaje cifrado
Un formato sencillo y robusto para AES-GCM es:
| version (1 byte) | nonce (12 bytes) | ciphertext + 16-byte tag |
Tres reglas de diseño importan más que el algoritmo:
- Nunca reutilices un nonce con la misma clave. Con AES-GCM, la reutilización puede exponer el texto plano y permitir falsificaciones. Un nonce aleatorio de 12 bytes es aceptable para volúmenes de mensajes moderados por clave; rota las claves periódicamente en los publicadores de gran volumen.
- Vincula el texto cifrado a su topic. Pasa el topic como datos adicionales autenticados (AAD). De lo contrario, un atacante con acceso al broker podría reproducir un mensaje cifrado de "unlock" en el topic de otro dispositivo.
- Incluye una marca de tiempo o un contador dentro del texto plano para que los suscriptores puedan rechazar mensajes antiguos reproducidos más tarde.
Ejemplo en Python: publicación y suscripción cifradas
El ejemplo usa paho-mqtt 2.x y el paquete cryptography. Instálalos primero:
pip install "paho-mqtt>=2.0" cryptography
Genera una clave una sola vez y compártela con el publicador y el suscriptor a través de tu almacén de secretos o de variables de entorno, no por MQTT:
python -c "import os,base64;print(base64.b64encode(os.urandom(32)).decode())"
Guarda lo siguiente como secure_mqtt.py:
import base64
import json
import os
import sys
import time
import paho.mqtt.client as mqtt
from cryptography.hazmat.primitives.ciphers.aead import AESGCM
from cryptography.exceptions import InvalidTag
BROKER = "broker.connectmq.com"
PORT = 8883
TOPIC = "connectmq/demo/e2e-91c4/door/command"
VERSION = b"\x01"
MAX_AGE_SECONDS = 30
KEY = base64.b64decode(os.environ["MQTT_PAYLOAD_KEY"])
aead = AESGCM(KEY)
def encrypt(topic: str, data: dict) -> bytes:
data["ts"] = int(time.time())
plaintext = json.dumps(data).encode()
nonce = os.urandom(12)
ciphertext = aead.encrypt(nonce, plaintext, topic.encode())
return VERSION + nonce + ciphertext
def decrypt(topic: str, blob: bytes) -> dict:
if len(blob) < 1 + 12 + 16 or blob[:1] != VERSION:
raise ValueError("unknown message format")
nonce, ciphertext = blob[1:13], blob[13:]
plaintext = aead.decrypt(nonce, ciphertext, topic.encode())
data = json.loads(plaintext)
if abs(time.time() - data["ts"]) > MAX_AGE_SECONDS:
raise ValueError("stale or replayed message")
return data
def on_connect(client, userdata, flags, reason_code, properties):
print("Connected:", reason_code)
if userdata == "sub":
client.subscribe(TOPIC, qos=1)
def on_message(client, userdata, msg):
try:
print("Decrypted:", decrypt(msg.topic, msg.payload))
except (InvalidTag, ValueError) as exc:
print("Rejected message:", exc)
def make_client(role: str) -> mqtt.Client:
client = mqtt.Client(
mqtt.CallbackAPIVersion.VERSION2,
client_id=f"e2e-{role}-91c4",
userdata=role,
)
client.tls_set() # TLS sigue protegiendo las credenciales y los metadatos
# Broker privado de ConnectMQ:
# client.username_pw_set("YOUR_USERNAME", "YOUR_PASSWORD")
client.on_connect = on_connect
client.on_message = on_message
client.connect(BROKER, PORT, keepalive=60)
return client
if __name__ == "__main__":
role = sys.argv[1] if len(sys.argv) > 1 else "sub"
client = make_client(role)
if role == "pub":
client.loop_start()
time.sleep(1)
payload = encrypt(TOPIC, {"action": "unlock", "door": "lab-2"})
client.publish(TOPIC, payload, qos=1).wait_for_publish()
print("Published", len(payload), "encrypted bytes")
client.loop_stop()
client.disconnect()
else:
client.loop_forever()
Ejecuta el suscriptor y el publicador en dos terminales con la misma clave:
export MQTT_PAYLOAD_KEY="paste-the-base64-key-here"
python secure_mqtt.py sub # terminal 1
python secure_mqtt.py pub # terminal 2
Si te suscribes al mismo topic con mosquitto_sub solo verás ruido binario, que es precisamente la idea. Cambia un byte de un payload, publícalo en otro topic o espera más de 30 segundos antes de descifrarlo, y el suscriptor lo rechazará. Como el ejemplo usa el servidor MQTT público gratuito, pensado para pruebas y prototipos y no para datos de producción, cualquiera podría ver el texto cifrado, pero nadie sin la clave puede leerlo ni falsificarlo.
Gestión de claves: la parte difícil
El código de cifrado es corto; la gestión de claves es donde los proyectos triunfan o fracasan.
- Acota las claves. Usa una clave por dispositivo o por grupo de dispositivos, no una clave para toda la flota. Una clave filtrada debe exponer lo menos posible.
- Guarda las claves de forma segura en los dispositivos. Usa flash cifrada o un elemento seguro en los microcontroladores, y un almacén de claves del sistema operativo o un gestor de secretos en los servidores. Nuestra guía para almacenar credenciales MQTT en dispositivos se aplica igualmente a las claves de cifrado.
- Incluye un ID de clave. Amplía la cabecera con un identificador de clave para que los suscriptores elijan la clave correcta y puedas rotar sin cortes.
- Rota las claves. Introduce una clave nueva, deja que los suscriptores acepten ambas, cambia los publicadores y después retira la clave antigua.
- Considera esquemas asimétricos para flujos de muchos a uno. Con las sealed boxes de libsodium, miles de dispositivos pueden cifrar con la clave pública de un backend, mientras que solo el backend tiene la clave privada.
Dispositivos embebidos
En un ESP32, mbedTLS (ya presente para TLS) ofrece mbedtls_gcm_crypt_and_tag y mbedtls_gcm_auth_decrypt, así que el cifrado del payload añade poco tamaño de código. Usa el generador de números aleatorios por hardware (esp_fill_random) para los nonces y asegúrate de que el Wi-Fi o el Bluetooth estén en marcha, o de que haya una fuente de entropía adecuada habilitada, para que la salida del RNG sea realmente aleatoria.
Errores de implementación habituales
Al revisar código de cifrado de payloads en proyectos IoT, aparecen una y otra vez los mismos errores:
- Un nonce fijo o un contador que se reinicia al arrancar. Un dispositivo que pone su contador a cero tras cada ciclo de alimentación reutilizará nonces. Usa nonces aleatorios o guarda el contador en flash.
- Ignorar los fallos de descifrado. Un suscriptor que registra un error
InvalidTagy aun así procesa el mensaje no tiene ninguna protección. Trata cualquier fallo como un mensaje descartado. - Comprimir después de cifrar. El texto cifrado no se comprime, así que comprime primero el JSON si el tamaño importa.
- Mezclar formatos sin un byte de versión. Cuando cambies de algoritmo o añadas un ID de clave, los dispositivos antiguos y nuevos deben poder distinguirse. El byte de versión del ejemplo existe exactamente por eso.
- Poner la clave en el mismo repositorio que el código. Las claves van en un almacén de secretos, variables de entorno o almacenamiento seguro en el dispositivo, nunca en Git.
Prueba deliberadamente los casos negativos: publica un mensaje truncado, un mensaje con un bit cambiado y un mensaje antiguo reproducido, y confirma que cada uno se rechaza.
El cifrado no sustituye a la seguridad del broker
El cifrado del payload protege la confidencialidad y la integridad de los datos, pero no impide que alguien se conecte, inunde topics o se suscriba a metadatos. Sigues necesitando TLS, credenciales sólidas por dispositivo y protecciones en el broker. Un broker MQTT gestionado como ConnectMQ ofrece TLS, credenciales vinculadas a client ID, cuotas por plan, protección contra floods y bloqueo de IP tras varios logins fallidos, mientras que el cifrado extremo a extremo mantiene privados los propios datos. Juntos cubren la checklist de nuestras buenas prácticas de seguridad MQTT.
Empieza con ConnectMQ
Crea un broker gratuito en app.connectmq.com con el plan Serverless, sin tarjeta de crédito, y ejecuta el ejemplo anterior con tus propias credenciales en el puerto 8883. ConnectMQ se encarga de TLS, la autenticación y el escalado, mientras tu aplicación conserva las claves de los payloads.
Preguntas frecuentes
¿Tiene MQTT cifrado del payload integrado?
No. MQTT 3.1.1 y 5.0 no definen cifrado del payload; los payloads son bytes opacos. El cifrado lo aporta TLS en la conexión y, opcionalmente, tu aplicación sobre el payload.
¿Puedo cifrar también el nombre del topic?
No sin romper el enrutamiento, porque el broker compara las suscripciones con los nombres de topic. Mantén los topics libres de información sensible y usa ID de dispositivo opacos en lugar de nombres o direcciones.
¿El cifrado del payload aumenta el tamaño del mensaje?
Ligeramente. El formato AES-GCM anterior añade 29 bytes por mensaje (versión, nonce y etiqueta), más la sobrecarga de codificación si conviertes el resultado a Base64.
¿Debo seguir usando TLS si los payloads van cifrados?
Sí. TLS protege tus credenciales MQTT, los client ID, los nombres de topic y los metadatos del protocolo, y autentica al broker; nada de eso lo cubre el cifrado del payload.



