End-to-End Encrypted MQTT IoT Gateway
MQTT is the backbone of industrial IoT. It was designed in 1999 for satellite telemetry — low bandwidth, high latency, unreliable links — and it has aged into the dominant protocol for device-to-cloud messaging. MQTT handles reliable delivery, topic routing, QoS levels, and retained messages. What it does not handle is confidentiality of the payload.
A standard MQTT deployment routes all messages through a central broker. The broker sees every message in plaintext. If the broker is compromised, all messages are exposed. If the broker is a third-party cloud service, you are trusting that provider with your sensor data. I built a system where the broker is structurally blind to the message content — encryption happens at the publisher before the message enters the network, and decryption happens at the subscriber after it exits.
The Problem with Transport-Level Encryption
Standard MQTT deployments use TLS to encrypt the transport — the connection between the device and the broker. This protects against a network eavesdropper. But it offers no protection against a compromised broker, a cloud provider's employees, or an API that exposes stored messages. The encryption terminates at the broker — not at the subscriber.
TLS-Only (Standard)
Encrypted in transit between publisher → broker and broker → subscriber. Broker sees plaintext at all times. A compromised broker, retained message leak, or cloud provider access exposes all data.
E2E Encryption (This System)
Encrypted from publisher's memory to subscriber's memory. Broker only ever handles ciphertext. Broker compromise yields useless encrypted bytes. Key management is decoupled from the messaging infrastructure.
The Encryption Protocol
AES-256-CBC (Cipher Block Chaining) is the encryption mode. CBC requires an Initialization Vector (IV) — a 16-byte random value that ensures identical plaintext payloads produce different ciphertext, preventing pattern analysis. The IV is not a secret; it is transmitted with the message. The key is the secret.
Wire Format
The MQTT payload is a compact binary structure: the IV followed immediately by the ciphertext. The subscriber knows the format and splits the payload accordingly.
function encryptPayload(data, sharedKey) {
const iv = crypto.randomBytes(16);
const cipher = crypto.createCipheriv('aes-256-cbc', sharedKey, iv);
const json = JSON.stringify(data);
const encrypted = Buffer.concat([
cipher.update(json, 'utf8'),
cipher.final()
]);
// IV prepended — subscriber splits at byte 16
return Buffer.concat([iv, encrypted]);
}
// Subscriber: decrypt on receive
function decryptPayload(buffer, sharedKey) {
const iv = buffer.subarray(0, 16);
const ciphertext = buffer.subarray(16);
const decipher = crypto.createDecipheriv('aes-256-cbc', sharedKey, iv);
const decrypted = Buffer.concat([
decipher.update(ciphertext),
decipher.final()
]);
return JSON.parse(decrypted.toString('utf8'));
}
System Architecture
│
├──► JSON.stringify(payload)
├──► IV = crypto.randomBytes(16)
├──► ciphertext = AES-256-CBC(key, IV, payload)
├──► wire = concat(IV + ciphertext)
│
▼
[ MQTT Broker (Mosquitto) ]
│ ← sees only: binary blob (IV + ciphertext)
│ ← topic routing only — zero plaintext access
│
▼
[ Subscriber (Node.js) ]
│
├──► IV = wire[0:16]
├──► ciphertext = wire[16:]
├──► plaintext = AES-256-CBC-Decrypt(key, IV, ciphertext)
└──► data = JSON.parse(plaintext)
Topic Design and QoS
MQTT topics are hierarchical strings — factory/line-3/sensor/temperature — that the broker uses for routing. Because the broker does routing based on topics, topics remain plaintext even in this E2E-encrypted system. This is an inherent constraint: the broker cannot route encrypted topic strings it cannot read.
QoS 1: At-Least-Once Delivery
The system uses QoS level 1. Under QoS 1, the broker acknowledges each message with a PUBACK. If the publisher does not receive a PUBACK within a timeout, it retransmits. This guarantees at-least-once delivery: messages survive broker restarts and transient network drops. Because each CBC-encrypted message uses a unique random IV, duplicate messages produce different ciphertext — duplicates are identifiable at the application layer via a message sequence number in the JSON payload.
Key Management Considerations
E2E encryption shifts the security boundary from the broker to the key. A compromised key compromises all past messages if the same key was used throughout. The system is designed with key rotation in mind:
- Per-topic keys: Each MQTT topic category uses a different AES key. Compromising one topic's key does not expose other topics
- Key rotation protocol: New publishers begin using a new key on a defined date. Subscribers accept both old and new keys during a transition window, then drop the old key
- Key derivation from a master secret: Rather than hardcoding AES keys, topic keys are derived from a master secret using HKDF-SHA256 with the topic name as the context input. Rotating the master secret rotates all derived keys atomically
Extending to IoT at Scale
The publish/subscribe pattern decouples publishers from subscribers in three ways: publishers don't know how many subscribers exist, subscribers don't know when publishers will publish, and neither needs the other to be online simultaneously (with retained messages). This makes the system naturally scalable:
- Add a new subscriber (e.g., a data lake ingestion service) without modifying the publisher
- Add a new publisher (e.g., a new device fleet) without modifying existing subscribers
- Scale the broker horizontally with a clustered Mosquitto or HiveMQ deployment — the encryption layer is broker-agnostic