IoT MQTT mTLS Stack → Kafka → ksqlDB

by Müller | Mar 16, 2026 | Arquitetura | 0 comments

Why On-Premise?

Most IoT tutorials start with AWS IoT Core, Azure IoT Hub, or some SaaS broker. Those are good options — today I’ll show an architecture for companies such as carriers, or enterprises that prefer to run their own private clouds. Or on any cloud, in containers.

This stack was built on the following premises:

  • Total control over the data
  • Mandatory mTLS — no device connects without a valid certificate
  • Streaming pipeline
  • Near-zero operational cost.
    • To be fair on this item: the cost here is your own servers and your IT analysts — analysts you would need anyway to operate cloud solutions.

An architecture that makes sense in an industrial plant, a private network, or somewhere isolated from the internet.

Architecture Overview

 

Documento@3x

 

Each layer has a single responsibility. 

  • EMQX doesn’t know Kafka directly; it authenticates and manages the devices’ mTLS keys, validates who can publish to which topic, and fires a data transport rule. Here it plays the middleware/second-layer role described in this post
  • Kafka doesn’t know the device; it only receives messages with a topic and a payload.

Layer 1: The Device — M5StickC Plus2 with MicroPython

The client is an M5StickC Plus2 running MicroPython. It collects sensor data and publishes to the sensors/device-001 topic at a configurable interval.

The connection uses mTLS: the device carries three files in its internal filesystem — ca.pem, cert.pem, and key.pem. Without these certificates, the connection is refused at the TLS layer before it even reaches MQTT.

The payload published by the device is a JSON with system, network, environment, and hardware identity fields:

I’ll write a follow-up post about certificates, mTLS, and how to generate them on both the device and the server side.

Layer 2: EMQX 5.0

Why EMQX?

EMQX 5 has a native feature I really like: per-listener isolated authentication. That means you can have, in the same instance:

  • Port 8883: accepts only client certificates (mTLS), no username/password
  • Port 1883: accepts only username/password, no TLS
  • Port 8083: WebSocket with username/password

Each listener has its own authentication chain. A device without a certificate cannot connect on 8883, even if it presents valid credentials.

mTLS Listener Configuration

Using the Certificate CN as Identity

An important design decision: EMQX can extract the Common Name from the client certificate and use it automatically as the clientid and username.

This means the device’s identity is the certificate itself — not a password that can leak. The CN device-001 automatically becomes the clientid in the MQTT session.

Layer 3: Kafka + ksqlDB

Kafka in KRaft Mode

Kafka runs without Zookeeper using KRaft — Kafka’s native consensus mode since version 3. One less process to manage, one less source of failure.

The SQL rule in EMQX selects the payload fields and forwards them to the Kafka action:

ksqlDB — Input Stream (TELEMETRY_RAW)

The input stream maps the iot.telemetry.raw topic with the payload’s full schema. An important lesson learned in practice: inside STRUCT<>, ksqlDB doesn’t accept backticks or INTEGER — use INT. The word timestamp works normally inside the STRUCT, contrary to what you’d expect.

ksqlDB — Flat Derived Stream (SENSOR_RAW)

From the raw stream, we create a flat stream with only the environment variables, RSSI, and device identity. The clientid becomes device, and everything sits at the same level, with no nesting.

The result on the iot.telemetry.sensor topic is a flat JSON, easy for any sink to consume:

ksqlDB — Daily Statistics Table (SENSOR_DAILY_STATS)

A table with a 1-day tumbling window aggregates the minimum, maximum, and average of every variable per device. The table key is composed of device + window, so each device has exactly one record per day.

To query a specific device’s statistics for the day:

The result is a reactive pipeline: when the M5Stick publishes, the data crosses EMQX → Kafka → ksqlDB in under a second, and the daily statistics update in real time.

Design Decisions and Trade-offs

mTLS instead of username/password on devices: Certificates can’t be brute-forced. A compromised device can have its certificate revoked at the CA without affecting the others.

Kafka instead of writing straight to the database: The database is a sink, not the source of truth. With Kafka in the middle, you can add new consumers without touching the producer.

ksqlDB instead of custom consumers: For simple aggregations and transformations, SQL is enough — and more readable than Python or Java code.

Layered streams: TELEMETRY_RAW preserves the device’s full schema. SENSOR_RAW is the working stream — flat and lightweight. The SENSOR_DAILY_STATS table delivers ready-made aggregations with no additional queries.

Conclusion

This stack isn’t the simplest to set up. But it’s the most honest one to run on-premise.

The M5Stick could be any IoT device with mTLS support.

EMQX scales to millions of simultaneous connections while keeping the same configuration.

Kafka absorbs peaks without loss and decouples the architecture between writes and reads.

ksqlDB processes in real time with no additional infrastructure.

Table of Contents