MQTT Security

End-to-End MQTT Payload Encryption with AES-GCM

TLS protects MQTT messages in transit, but the broker still sees plaintext. Learn how to add end-to-end MQTT payload encryption with AES-GCM in Python, and how to handle keys, nonces and topics safely.

MQTT payload encryption illustration with a sealed envelope travelling through a broker from a sensor to an application
Run this on your own MQTT brokerGet a private, managed MQTT broker on the Free plan and start from ready-made code samples.
Try it free Code samples No card required

MQTT payload encryption means encrypting the message body itself, so that only the publisher and the intended subscribers can read it, regardless of who operates the broker. In this guide you will learn when end-to-end encryption is worth adding on top of TLS, how to design the message format safely, and how to implement it in Python with AES-GCM over a real broker connection.

TLS vs MQTT Payload Encryption

TLS encrypts the connection between each client and the broker. That is essential, and you should always use it; see our MQTT TLS configuration guide. But TLS terminates at the broker. Inside the broker, a message is plaintext: it has to be, so the broker can route it, store it as a retained message, or queue it for an offline session.

Payload encryption adds a second layer that travels with the message from publisher to subscriber.

Property TLS only TLS + payload encryption
Protected on the network Yes Yes
Readable by broker operator Yes No
Readable in retained or queued messages at rest Yes No
Topic names hidden No (visible to broker) No (still visible to broker)
Broker can inspect or decode payloads Yes No
Key management needed Broker certificate only Application keys on every client

Notice the two "No" rows that people often miss. Topics stay readable, because the broker needs them for routing, so never put secrets in topic names. And any broker feature that reads payloads stops working for encrypted messages. For example, ConnectMQ's live topic explorer, threshold alert rules and Sparkplug B decoding can only show or evaluate data they can parse, so encrypted payloads will appear as opaque bytes there.

When End-to-End Encryption Makes Sense

Add payload encryption when:

  • Regulations or contracts require that infrastructure providers cannot read the data.
  • Messages pass through shared or public brokers, bridges or gateways you do not fully control.
  • Retained messages or queued sessions hold sensitive values, such as personal data or access codes.
  • You want defence in depth in case credentials to one part of the system leak.

Skip it when you need broker-side visibility (dashboards in the broker portal, alert rules on values) and you trust the operator, or when constrained devices cannot afford the extra code and key handling. Many teams encrypt only a few sensitive topics, such as .../commands/unlock, and leave telemetry in plaintext over TLS.

Choosing an Algorithm

Use an authenticated encryption algorithm. It encrypts the payload and adds a tag that detects any modification. Two good choices:

  • AES-256-GCM: widely available, hardware-accelerated on many CPUs and on the ESP32 (via mbedTLS). Requires a unique 96-bit nonce per message under the same key.
  • XChaCha20-Poly1305 (libsodium crypto_aead_xchacha20poly1305_ietf): fast in software, and its 192-bit nonce can be generated randomly with no realistic risk of collision.

Do not use AES-CBC without a MAC, ECB mode of any kind, or "encryption" with XOR or Base64. They give a false sense of security.

Designing the Encrypted Message Format

A simple, robust format for AES-GCM is:

| version (1 byte) | nonce (12 bytes) | ciphertext + 16-byte tag |

Three design rules matter more than the algorithm:

  1. Never reuse a nonce with the same key. With AES-GCM, reuse can expose plaintext and allow forgeries. A random 12-byte nonce is acceptable for moderate message volumes per key; rotate keys periodically for high-volume publishers.
  2. Bind the ciphertext to its topic. Pass the topic as additional authenticated data (AAD). Otherwise an attacker with broker access could replay an encrypted "unlock" message on a different device's topic.
  3. Include a timestamp or counter inside the plaintext so subscribers can reject old messages that are replayed later.

Python Example: Encrypted Publish and Subscribe

The example below uses paho-mqtt 2.x and the cryptography package. Install them first:

pip install "paho-mqtt>=2.0" cryptography

Generate a key once and share it with the publisher and subscriber through your secret store or environment, not through MQTT:

python -c "import os,base64;print(base64.b64encode(os.urandom(32)).decode())"

Save the following as 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 still protects credentials and metadata
    # Private ConnectMQ broker:
    # 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()

Run the subscriber and publisher in two terminals with the same key:

export MQTT_PAYLOAD_KEY="paste-the-base64-key-here"
python secure_mqtt.py sub      # terminal 1
python secure_mqtt.py pub      # terminal 2

If you subscribe to the same topic with mosquitto_sub you will see only binary noise, which is the point. Change one byte of a payload, publish it to another topic, or wait longer than 30 seconds before decrypting, and the subscriber rejects it. Because the example uses the public MQTT test broker, anyone could see the ciphertext, but nobody without the key can read or forge it.

Key Management: The Hard Part

The encryption code is short; managing keys is where projects succeed or fail.

  1. Scope keys narrowly. Use one key per device or per device group, not one key for the whole fleet. A leaked key should expose as little as possible.
  2. Store keys securely on devices. Use encrypted flash or a secure element on microcontrollers, and an OS keystore or secret manager on servers. Our guide to storing MQTT credentials on devices applies equally to encryption keys.
  3. Include a key ID. Extend the header with a key identifier so subscribers can pick the right key and you can rotate without downtime.
  4. Rotate keys. Introduce a new key, let subscribers accept both, switch publishers, then retire the old key.
  5. Consider asymmetric schemes for many-to-one flows. With libsodium sealed boxes, thousands of devices can encrypt to a backend's public key while only the backend holds the private key.

Embedded Devices

On an ESP32, mbedTLS (already present for TLS) provides mbedtls_gcm_crypt_and_tag and mbedtls_gcm_auth_decrypt, so payload encryption adds little code size. Use the hardware random number generator (esp_fill_random) for nonces, and make sure Wi-Fi or Bluetooth is running, or a proper entropy source is enabled, so the RNG output is truly random.

Common Implementation Mistakes

Reviewing payload encryption code in IoT projects, the same errors appear again and again:

  • A fixed or counter nonce that resets on reboot. A device that starts its counter at zero after every power cycle will reuse nonces. Use random nonces, or persist the counter in flash.
  • Ignoring decryption failures. A subscriber that logs an InvalidTag error and then processes the message anyway has no protection at all. Treat any failure as a dropped message.
  • Compressing after encrypting. Ciphertext does not compress, so compress the JSON first if size matters.
  • Mixing formats without a version byte. When you change algorithms or add a key ID, old and new devices must be distinguishable. The version byte in the example exists for exactly that reason.
  • Putting the key in the same repository as the code. Keys belong in a secret store, environment variables or secure storage on the device, never in Git.

Test the negative paths deliberately: publish a truncated message, a message with one flipped bit, and a replayed old message, and confirm that each one is rejected.

Encryption Doesn't Replace Broker Security

Payload encryption protects confidentiality and integrity of data, but it does not stop someone from connecting, flooding topics or subscribing to metadata. You still need TLS, strong per-device credentials and broker-side protections. A hosted MQTT broker such as ConnectMQ provides TLS, credentials bound to client IDs, per-plan quotas, flood protection and IP blocking after repeated failed logins, while end-to-end encryption keeps the data itself private. Together they cover the checklist in our MQTT security best practices.

Get Started with ConnectMQ

Create a free broker at app.connectmq.com on the Serverless plan, no credit card required, and run the example above with your own credentials on port 8883. ConnectMQ handles TLS, authentication and scaling, while your application keeps the payload keys.

Frequently Asked Questions

Does MQTT have built-in payload encryption?

No. MQTT 3.1.1 and 5.0 define no payload encryption; payloads are opaque bytes. Encryption is done by TLS on the connection and, optionally, by your application on the payload.

Can I encrypt the topic name as well?

Not without breaking routing, because the broker matches subscriptions on topic names. Keep topics free of sensitive information and use opaque device IDs instead of names or addresses.

Does payload encryption increase message size?

Slightly. The AES-GCM format above adds 29 bytes per message (version, nonce and tag), plus any encoding overhead if you Base64 the result.

Should I still use TLS if payloads are encrypted?

Yes. TLS protects your MQTT credentials, client IDs, topic names and protocol metadata, and authenticates the broker, none of which payload encryption covers.

Related articles

How to Rotate MQTT Credentials Without Downtime

A step-by-step dual-credential strategy for rotating MQTT usernames and passwords across a fleet: stage the new credential, switch with fallback, verify and only then revoke the old one.

7 min read

Storing MQTT Credentials Securely on Embedded Devices

A practical comparison of ways to protect MQTT usernames and passwords on ESP32 and Linux devices, from encrypted NVS and flash encryption to secure elements and TPM-backed systemd credentials.

7 min read

All articles →

Your private MQTT broker is a few clicks away

Create an account, get your broker credentials and connect your first device — no servers to install, patch or scale.