Zero-Trust IoT lab at home (ESP32 + OpenWrt + mTLS + IDS + Honeypot)
ESP32 talks MQTT over mTLS, the IoT Wi-Fi is isolated on its own VLAN, Suricata watches the traffic, a Cowrie honeypot lures attackers, and a small Flask dashboard ties the logs together.
If you want the code and configs: GitHub → zero-trust-iot. I’ll keep refining it as I go.
If you don’t care about this blog post and just want the official report, here you go:
Google Drive → Detailed PDF Report
Why I did this (and what I wanted)
I needed an end-of-studies project for my bachelor’s degree that wasn’t another “blink the LED” project. And as someone who loves to bring networking and security into every discussion, I ended up doing all this work for a bachelor’s project, which is overkill. I SAW IT ON THE JURY’S FACES.
1. Network Segmentation (VLAN Isolation)
- Default VLAN (10.0.0.0/24): default LAN
- IoT VLAN (10.0.20.0/24): complete isolation for IoT devices
- Firewall rules preventing inter-VLAN communication
- OpenWrt router providing enterprise-grade network control
I bought a mid-range Xiaomi AX3000T router from AliExpress back in 2023 for about 30 USD.
I flashed it with the latest OpenWrt image, 24.10, and it was stable and so much more fun to work with than the stock firmware. OpenWrt is one of my go-tos now.
2. Certificate-Based Authentication (mTLS)
- Private Certificate Authority on a Raspberry Pi
- X.509 certificates for the ESP32, which acts as a gateway for all sensors
- Mutual TLS (mTLS) authentication, hence zero-trust
- On-device key generation, ensuring keys never leave the device (the C++ code is here)
I installed Raspberry Pi OS Lite on my Raspberry Pi 5 (8 GB RAM) and used OpenSSL to generate the keys, certificates, and so on. So my whole PKI is hosted on the Pi.
At first, I generated the ESP32’s private key on the Raspberry Pi, used SFTP to transfer it to my PC, and then into the ESP32 code. But later I managed to make the ESP32 generate its own private key, to avoid any risk of it leaking. SFTP is secure, but a key that never travels is even more secure.
I also managed (I SWEAR TO GOD I SPENT 6 DAYS ON THIS ALONE) to upload the ESP32’s private key and certificate, plus the COMODO CA certificate that signed ThingsBoard’s server certificate, into the ESP32’s flash memory using LittleFS. The plugin is below:
3. Real-Time Threat Monitoring
- Suricata IDS analyzing all network traffic
- Cowrie honeypot detecting unauthorized access attempts
- Automated alerting via Telegram with <2.3-second response time
- Centralized logging with a Flask web dashboard
If I asked you which is better in general, blocking all SSH attempts or redirecting them to a honeypot, you would definitely say block them! I agree, but for drama and practice reasons, I set up port forwarding on my OpenWrt router to redirect all SSH requests to my honeypot.
4. End-to-End Encryption
- TLS 1.2 with strong cipher suites (AES-256-GCM)
- Perfect Forward Secrecy protecting past communications
- Certificate validation on every connection
- Encrypted MQTT communication with cloud platforms
Weeks of work, but it all felt like nothing when I saw that list of packets in Wireshark. Look how beautiful this is!
Results Summary
| Metric | Result |
|---|---|
| Total Cost | $150 USD |
| Setup Time | 3 weeks (mostly debugging) |
| Threat Detection Rate | 100% (all test attacks detected) |
| Average Alert Time | 2.3 seconds |
| Raspberry Pi CPU Usage | 6% (full security stack) |
| Network Isolation | 100% (zero cross-VLAN communication) |
| ESP32 TLS Overhead | <5% |
| Supported IoT Devices | 50-70 (limited by router RAM) |
Architecture (one paragraph you can screenshot)
OpenWrt splits the network into VLANs: 10.0.20.0/24 for IoT and 10.0.0.0/24 for the default LAN. A Raspberry Pi runs a private CA, the Suricata IDS, a Cowrie honeypot (Docker), and a small Flask dashboard. The ESP32 stores its X.509 certificate and key in flash memory using LittleFS, and publishes telemetry to ThingsBoard over MQTT-over-TLS 1.2 with mutual authentication on mqtt.eu.thingsboard.cloud:8883. Anything not explicitly allowed is blocked. If something noisy appears, it shows up in the dashboard (and in a Telegram channel; I bring Telegram into every project).
The network: OpenWrt and why VLANs
OpenWrt was the fastest way to get proper segmentation at home without buying enterprise gear.
What I actually changed
/etc/config/network
config interface 'iot' option proto 'static' option ipaddr '10.0.20.1' option netmask '255.255.255.0' option type 'bridge'Tag/untag your switch ports as needed (device-specific).
/etc/config/firewall
config zone option name 'iot' option network 'iot' option input 'REJECT' # no inbound to router option output 'ACCEPT' # can reach WAN option forward 'REJECT' # no lateral movementYou can find all the OpenWrt configuration here:
GitHub → OpenWrt Configuration
Quick captures from the router (these helped a lot while debugging TLS):
opkg updateopkg install tcpdump openssh-sftp-server# Capture on the AP radio, single host filtered the 10.0.20.100 is ESP32 static IPtcpdump -i phy0-ap1 -w /tmp/mtls_capture.pcap -n 'host 10.0.20.100'# Pull the filescp root@10.0.0.254:/tmp/mtls_capture.pcap ./captures/Whenever I faced an issue with the network or mTLS, I started capturing on OpenWrt and analyzing in Wireshark, and that was a game changer for me. Here are some of the captures I took while working on this project:
ESP32 side (LittleFS + WiFiClientSecure + PubSubClient)
#include <WiFi.h>#include <WiFiClientSecure.h>#include <PubSubClient.h>#include <LittleFS.h>#include <time.h>// Wi-Fi network credentials (set your SSID and password)const char* WIFI_SSID = "IOT_VLAN_SSID";const char* WIFI_PASSWORD = "00001111";// ThingsBoard MQTT server detailsconst char* THINGSBOARD_HOST = "mqtt.eu.thingsboard.cloud"; // adjust for region if needed, e.g. "mqtt.eu.thingsboard.cloud"const uint16_t THINGSBOARD_PORT = 8883;// File paths in LittleFSconst char* CA_CERT_PATH = "/aaa.pem";const char* CLIENT_CERT_PATH = "/cert.pem";const char* PRIVATE_KEY_PATH = "/key.pem";// Global variables for TLS certificate contentsString caCert;String clientCert;String privateKey;// WiFi and MQTT clientsWiFiClientSecure secureClient;PubSubClient mqttClient(secureClient);// Function to read a text file from LittleFS into a Stringbool readFileToString(fs::FS &fs, const char* path, String &dest) { File file = fs.open(path, "r"); if (!file) { Serial.printf("Failed to open file: %s\n", path); return false; } dest.clear(); // Reserve capacity to avoid fragmentation (optional) dest.reserve(file.size()); while (file.available()) { // Read file character by character to preserve all contents (including newlines) dest += char(file.read()); } file.close(); // Debug: print file size loaded Serial.printf("Loaded %s (%d bytes)\n", path, dest.length()); return true;}// Ensure time is initialized (required for TLS)void syncTime() { configTime(0, 0, "pool.ntp.org", "time.nist.gov"); // UTC timezone Serial.print("Synchronizing time via NTP"); struct tm timeinfo; const uint8_t MAX_RETRIES = 30; uint8_t attempt = 0; // Wait for time to be set or until MAX_RETRIES reached while (attempt < MAX_RETRIES) { if (getLocalTime(&timeinfo)) { Serial.println(" - Time synchronized!"); Serial.printf("Current time: %04d-%02d-%02d %02d:%02d:%02d UTC\n", timeinfo.tm_year + 1900, timeinfo.tm_mon + 1, timeinfo.tm_mday, timeinfo.tm_hour, timeinfo.tm_min, timeinfo.tm_sec); return; } Serial.print("."); delay(500); attempt++; } Serial.println(" - Failed to synchronize time!");}// Attempt to (re)connect to ThingsBoard MQTTvoid reconnectMQTT() { // Generate a unique client ID based on MAC address String clientId = "ESP32-"; clientId += WiFi.macAddress(); clientId.replace(":", ""); // remove colons from MAC while (!mqttClient.connected()) { Serial.print("Connecting to ThingsBoard MQTT..."); // Note: using client certificate authentication, no username/password required if (mqttClient.connect(clientId.c_str())) { Serial.println(" connected."); // You can subscribe to topics or publish a message here if needed } else { Serial.printf(" failed, rc=%d. Retrying in 5 seconds...\n", mqttClient.state()); delay(5000); } }}void setup() { Serial.begin(115200); delay(100); // small delay to let Serial init Serial.println(); Serial.println("=== Device booting... ==="); // Initialize LittleFS Serial.println("Mounting LittleFS..."); if (!LittleFS.begin()) { Serial.println("ERROR: LittleFS Mount failed! Halting."); while (true) { delay(100); } } Serial.println("LittleFS mounted successfully."); // Load TLS certificates/keys from LittleFS Serial.println("Reading TLS credentials from LittleFS..."); bool ok = true; ok &= readFileToString(LittleFS, CA_CERT_PATH, caCert); ok &= readFileToString(LittleFS, CLIENT_CERT_PATH, clientCert); ok &= readFileToString(LittleFS, PRIVATE_KEY_PATH, privateKey); if (!ok) { Serial.println("ERROR: Failed to load one or more PEM files. Check LittleFS contents."); while (true) { delay(100); } } // Connect to Wi-Fi Serial.printf("Connecting to WiFi SSID: %s\n", WIFI_SSID); WiFi.begin(WIFI_SSID, WIFI_PASSWORD); // Wait for connection uint8_t attempt = 0; while (WiFi.status() != WL_CONNECTED) { delay(500); Serial.print("."); if (++attempt % 40 == 0) { // print a newline every 40 * 0.5s = 20s Serial.println(); } } Serial.println("\nWiFi connected."); Serial.print("IP address: "); Serial.println(WiFi.localIP()); // Sync time for TLS syncTime(); // Configure the secure client with certificates Serial.println("Configuring TLS credentials..."); secureClient.setCACert(caCert.c_str()); secureClient.setCertificate(clientCert.c_str()); secureClient.setPrivateKey(privateKey.c_str()); // Optionally, disable certificate verification for testing (NOT recommended in production) // secureClient.setInsecure(); // **Do not use in production, for debug only** // Configure MQTT server and port mqttClient.setServer(THINGSBOARD_HOST, THINGSBOARD_PORT); // Optionally, set callback for incoming messages (not used in this example) // mqttClient.setCallback(mqttCallback); // Attempt initial MQTT connection reconnectMQTT();}void loop() { if (!mqttClient.connected()) { reconnectMQTT(); } mqttClient.loop(); static unsigned long lastSend = 0; if (millis() - lastSend > 10000) { lastSend = millis(); const char* payload = "{\"temperature\":99}"; mqttClient.publish("v1/devices/me/telemetry", payload); Serial.println("↗ Published telemetry to ThingsBoard"); }}CLI smoke test (good for diagnosing ThingsBoard profile issues):
mosquitto_pub -d -h mqtt.eu.thingsboard.cloud -p 8883 --cafile /etc/ssl/certs/ca-certificates.crt --cert ~/ca/clients/esp32_01/esp32.cert.pem --key ~/ca/clients/esp32_01/esp32.key.pem -i esp32-01-$(date +%s) -t v1/devices/me/telemetry -m '{"diag":"ok"}'If you see (5) not authorised, re‑check the Device Profile and CA. If you see (2) identifier rejected, simplify the -i value.
Detection: Suricata IDS + Cowrie honeypot (and a tiny dashboard)
I run both on the Pi (Docker for Cowrie). Suricata watches the IoT VLAN and default LAN; Cowrie attracts the bored internet.
Suricata essentials
/etc/suricata/suricata.yaml (the bit everyone forgets)
vars: address-groups: HOME_NET: "[10.0.20.0/24, 10.0.0.0/24]" EXTERNAL_NET: "!$HOME_NET"Update rules and start:
sudo suricata-updatesudo systemctl enable --now suricatasudo suricata -T -c /etc/suricata/suricata.yaml # test configGenerate a harmless alert to validate the pipeline:
curl http://testmyids.com # should fire GPL ATTACK_RESPONSE in eve.jsonIf you don’t see it in your dashboard, confirm the eve.json path and permissions, the HOME_NET ranges, and that your parser selects event_type == "alert".
Cowrie in Docker
docker run -d --name honeypot-container \ -p 22:2222 -p 23:2223 \ -v ~/cowrie_data:/cowrie/var \ --restart always cowrie/cowrieI pull the structured logs from the container and display sessions (successful/failed logins, commands tried) in a Flask view next to the IDS table.
The Six-Day War: Debugging the ESP32 mTLS Connection
This was, without a doubt, the most challenging part of the project. I had a working network, a working CA, and a working cloud endpoint. My mosquitto_pub test from the Raspberry Pi, using the exact same certificates, connected to ThingsBoard flawlessly.
Yet the ESP32 refused to connect, repeatedly failing with a generic rc=-2 error. This kicked off a 6-day debugging marathon.
- Was it the firewall? No, the
mosquitto_pubtest proved traffic could get through. - Was it the certificates? No, the
openssl verifycommand and themosquitto_pubtest proved the chain of trust was perfect. - Was it the ESP32’s clock? This was a major issue. TLS certificates require accurate time, and the initial code was failing because the ESP32’s clock wasn’t synchronized. I fixed this by adding NTP time sync logic. The error persisted.
- Was it DNS? The time sync was failing because the ESP32 couldn’t resolve
pool.ntp.org. I bypassed this by using a direct IP (216.239.35.0). The error still persisted.
The final breakthrough came from analyzing Wireshark captures of the failed handshakes. The ESP32 was establishing a TCP connection and then immediately sending a [FIN, ACK] packet to tear it down before the TLS handshake even began. This pointed to one final culprit: RAM exhaustion. The ESP32’s WiFiClientSecure library was likely failing to allocate enough memory to handle the multiple large certificates required for the handshake.
The final, working code came from researching alternative MQTT libraries and finding a more memory-efficient implementation that could successfully complete the handshake.
The “After” Picture: A Secure, Encrypted Tunnel
With the final code, it all came together. The ESP32 connected, and another Wireshark capture showed the beautiful result.
Success: the MQTT traffic is now unreadable, hidden inside a secure TLSv1.2 tunnel.
As you can see, Wireshark no longer identifies the protocol as “MQTT.” It only sees TLSv1.2. The sensitive data is now hidden inside the “Encrypted Application Data” payload. This is the tangible proof of end-to-end encryption.
Performance notes (real numbers)
Implementing comprehensive security doesn’t require expensive hardware:
- Raspberry Pi 5 (8GB): 6% CPU usage with the full security stack
- OpenWrt Router (256MB RAM): handles 50-70 IoT devices
- ESP32: <5% overhead for TLS encryption
- Total Cost: ~$162 USD for the complete infrastructure
Comparison with Enterprise Solutions
| Feature | Our Implementation | Enterprise Solution | Cost Difference |
|---|---|---|---|
| Network Segmentation | OpenWrt VLANs | Cisco/Juniper | -$5,000+ |
| Certificate Authority | OpenSSL | Microsoft CA | -$2,000+ |
| IDS/IPS | Suricata | Cisco Firepower | -$10,000+ |
| Honeypot | Cowrie | Commercial | -$1,000+ |
| Total Savings | $162 | $18,000+ | $17,838 |
Deployment Guide: From Lab to Production
Phase 1: Network Foundation
- Deploy an OpenWrt router with VLAN configuration
- Implement firewall rules and access controls
- Test network isolation thoroughly
Phase 2: Security Infrastructure
- Set up the Raspberry Pi with Docker
- Deploy the Certificate Authority (ephemeral container)
- Install and configure Suricata IDS
- Deploy the Cowrie honeypot
Phase 3: Device Integration
- Generate device certificates
- Implement TLS communication
- Configure cloud platform integration
- Test end-to-end encryption
Phase 4: Monitoring and Maintenance
- Set up alerting systems
- Configure log aggregation
- Implement backup procedures
- Schedule security updates
What I’d improve next
- Automatic cert rotation (short‑lived certs) + revocation (CRL isn’t supported in ThingsBoard, so I need a workaround)
- Anomaly detection on Suricata flows (stats first, ML later)
- A proper Grafana view on top of eve.json + Cowrie
Appendix: quick OpenWrt cheats I kept using
# See routes and VLAN interfacesip a; ip r# Verify DNS/SNI reachability to TBnslookup mqtt.eu.thingsboard.cloudopenssl s_client -connect mqtt.eu.thingsboard.cloud:8883 -servername mqtt.eu.thingsboard.cloud -alpn mqtt# Copy pcaps off the routerscp root@10.0.0.254:/tmp/mtls_capture.pcap .You can absolutely do this at home with open-source tools and a weekend. (EVIL LAUGH: you will hate me after that weekend, for sure.)
I believe in the power of open source and sharing knowledge. I’ve documented the entire process and made all the configuration files and code available in my GitHub repository.
About the Author
I’m Radouane ELHAJALI. This was my end-of-studies project for my bachelor’s degree in IoT and Intelligent Systems. I’m now in my fourth year of cybersecurity engineering.
Connect with me:
- LinkedIn: https://www.linkedin.com/in/elhajali-radouane/
- GitHub: https://github.com/RadouaneElhajali
- Email: {rdwnlocal}@proton.me
Keywords: Zero Trust IoT, IoT security architecture, cybersecurity for IoT devices, MQTT security, TLS encryption, network segmentation, VLAN configuration, Suricata IDS, Cowrie honeypot, certificate authority, ESP32 security, Raspberry Pi security, mTLS authentication, IoT threat detection, enterprise IoT security