MITM Attack on a TLS IoT Device - What Breaks and What Doesn't

Cryptography for IoT Hackers - Part 7


Series Index

Part Title Status
1 What TLS Actually Does (And Why Your IoT Device Needs It) :white_check_mark: Published
2 Certificates From Scratch The Trust Chain Nobody Explains :white_check_mark: Published
3 Setting Up Your Own CA and Issuing Certs with OpenSSL :white_check_mark: Published
4 ESP32 + TLS: Your First Secure MQTT Connection :white_check_mark: Published
5 What the TLS Handshake Looks Like on the Wire (Wireshark Lab) :white_check_mark: Published
6 Mutual TLS (mTLS) Explained: When the Server Also Verifies the Client :white_check_mark: Published
7 - You are here MITM Attack on a TLS IoT Device What Breaks and What Doesn’t :white_check_mark: Published
8 Cert Pinning The Fix, and How Attackers Bypass It :soon_arrow:
9 Embedded Crypto Pitfalls: Hardcoded Keys and Weak RNG :soon_arrow:
10 Breaking mTLS: Stolen Certs and Certificate Confusion :soon_arrow:
11 Secure Provisioning How to Get Certs Onto Devices Safely :soon_arrow:
12 Building a Hardened ESP32 TLS Client Checklist and Final Lab :soon_arrow:
13 Secure OTA Updates Why Your Update Channel Is an Attack Surface :soon_arrow:

Before You Read This

You need Parts 1 through 6 before this one. Specifically:

  • Part 3 - you have rootCA.crt, server.crt, server.key, esp32.crt, esp32.key in ~/my-ca
  • Part 4 - ESP32 talking to Mosquitto over TLS on port 8883
  • Part 6 - mTLS working, broker checking ESP32’s certificate

In Part 6 we built the most locked-down version of our setup. Both sides verify each other. Only devices with a certificate signed by our Root CA can connect. We proved it by trying to connect without a cert and getting kicked out.

Now in Part 7 we attack it.

But here is the thing I want to say upfront properly configured TLS does not break. A MITM attack against a correctly set up TLS connection fails. Every time.

So what is this blog about then?

It is about what happens when developers make mistakes. And the most common mistake in IoT firmware is one line: setInsecure(). We are going to see exactly what that one line does to your security, live on the wire in Wireshark.


What a MITM Attack Actually Is

MITM stands for Man In The Middle. The attacker puts themselves between your ESP32 and the real broker. The ESP32 thinks it is talking to the broker. The attacker is actually the one receiving everything.

Normal setup:
ESP32 ──────────────────────► Real Broker (port 8883)

MITM setup:
ESP32 ──► Attacker's Fake Broker (port 8884) ✗ Real Broker never sees anything

The attacker is not sniffing passively. They are actively impersonating the broker. The ESP32 connects to them, does the TLS handshake with them, and publishes all its messages to them.

For this to work, the attacker needs the ESP32 to trust their fake broker. And that is exactly where TLS is supposed to stop them.


Why MITM Fails Against Proper TLS

Let me walk through what happens when an attacker tries this against a properly configured ESP32.

The attacker sets up their own Mosquitto broker with their own self-signed certificate one that was NOT signed by our Root CA. Then they try to get the ESP32 to connect to it.

ESP32  → Fake Broker : Client Hello
Fake Broker → ESP32  : fake_server.crt  ← signed by attacker's own fake CA
ESP32 checks: who signed this?
ESP32 has rootCA.crt hardcoded in sketch ← the real Root CA
Signature does not match → ❌ SSL handshake REJECTED
ESP32: drops connection
Attacker: gets nothing

The ESP32 caught them. The certificate the fake broker presented was not signed by our Root CA. The ESP32 refuses to continue. Game over for the attacker.

This is why we hardcode rootCA.crt in the sketch. Not the server certificate the Root CA. Because the Root CA is the one thing we trust absolutely. Any certificate not signed by it gets rejected.

But what if the ESP32 stops checking?

That is the entire attack. And it is one line of code away.


The One Line That Breaks Everything

In Arduino’s WiFiClientSecure library there is a method called setInsecure().

What it does: tells the ESP32 to skip certificate verification entirely. Connect to any broker. Accept any certificate. Don’t check signatures. Don’t check the CA. Just connect.

Who uses it: developers who are debugging and want to “just get it working.” They add setInsecure() to skip the cert errors, the thing connects, they move on. The line never gets removed before production.

This is one of the most common IoT security mistakes in the field. And we are going to prove exactly why it is dangerous.


The Attack Setup

We have two brokers running side by side on the same Kali machine:

Real Broker  → port 8883 — TLS, mTLS, rootCA signed cert, require_certificate true
Fake Broker  → port 8884 — TLS with self-signed fake cert, no client cert required

The fake broker represents an attacker’s machine on the same network. In a real attack this would be a separate machine. For our lab both live on the same Kali IP.

The attacker’s fake broker uses a self-signed certificate that has nothing to do with our Root CA:

# Generate attacker's fake cert NOT signed by our rootCA
openssl req -x509 -newkey rsa:2048 -keyout /tmp/fake.key \
  -out /tmp/fake.crt -days 365 -nodes \
  -subj "/CN=fakeBroker" \
  -addext "subjectAltName=IP:192.168.28.71"

What this does: creates a completely independent self-signed certificate. The CN is fakeBroker. It has no relationship to our IoTSec Root CA whatsoever.

Then the fake broker config:

cat > /tmp/fake_broker.conf << EOF
listener 8884
allow_anonymous true
cafile /tmp/fake.crt
certfile /tmp/fake.crt
keyfile /tmp/fake.key
EOF

Notice what is missing compared to the real broker config:

  • No require_certificate true the fake broker does not check client certs
  • Using fake.crt as both the CA and the cert a self-signed setup

Start the fake broker:

mosquitto -c /tmp/fake_broker.conf -v

Expected output:

mosquitto version 2.0.22 starting
Config loaded from /tmp/fake_broker.conf.
Opening ipv4 listen socket on port 8884.
Opening ipv6 listen socket on port 8884.
mosquitto version 2.0.22 running

Fake broker is up. Waiting for victims.


Sabotaging the ESP32 Sketch

Take the mTLS sketch from Part 6. We are making exactly two changes.

Change 1 - point to the fake broker’s port:

const int mqtt_port = 8884;  // was 8883

Change 2 - replace all three cert lines in setup() with one line:

Remove these three lines:

espClient.setCACert(rootCA);
espClient.setCertificate(esp32_cert);
espClient.setPrivateKey(esp32_key);

Replace with:

espClient.setInsecure();

The rest of the sketch stays identical to Part 6. Here is the complete modified setup() and supporting functions:

void connectWiFi() {
  Serial.print("Connecting to WiFi");
  WiFi.begin(ssid, password);
  while (WiFi.status() != WL_CONNECTED) {
    delay(500);
    Serial.print(".");
  }
  Serial.println("\nWiFi connected");
  Serial.println(WiFi.localIP());
}

void connectMQTT() {
  while (!client.connected()) {
    Serial.print("Connecting to MQTT broker...");
    if (client.connect("ESP32-mTLS")) {
      Serial.println("Connected!");
      client.publish("test/topic", "hello from ESP32 mTLS");
    } else {
      Serial.print("Failed, rc=");
      Serial.println(client.state());
      delay(3000);
    }
  }
}

void setup() {
  Serial.begin(115200);
  connectWiFi();

  // Part 6 had these three lines for mTLS:
  // espClient.setCACert(rootCA);
  // espClient.setCertificate(esp32_cert);
  // espClient.setPrivateKey(esp32_key);

  // Part 7 — one line replaces all three. This is the mistake.
  espClient.setInsecure();

  client.setServer(mqtt_server, mqtt_port);
  connectMQTT();
}

void loop() {
  client.loop();
}

The entire security change is: three lines removed, one line added. That is the difference between a secure device and a compromised one.

Flash this sketch. Open Serial Monitor at 115200 baud.

Expected Serial Monitor output:

.....
WiFi connected
192.168.28.75
Connecting to MQTT broker...Connected!

And on Kali where the fake broker is running:

New connection from 192.168.28.75:58060 on port 8884.
New client connected from 192.168.28.75:58060 as ESP32-mTLS (p2, c1, k15).
Sending CONNACK to ESP32-mTLS (0, 0)
Received PUBLISH from ESP32-mTLS (d0, q0, r0, m0, 'test/topic', ... (21 bytes))

The ESP32 just published its message to the attacker’s broker. The real broker on port 8883 never saw a single packet from this device.


What the Attacker Sees - Reading the Message

The attacker subscribes to the fake broker and reads everything:

mosquitto_sub -h 192.168.28.71 -p 8884 \
  --cafile /tmp/fake.crt \
  -t test/topic

Output:

hello from ESP32 mTLS

That is the ESP32’s message. In a real deployment this could be sensor readings, authentication tokens, device status, commands anything the device publishes.

The traffic is still TLS encrypted on the wire. But the attacker IS the broker. They do not need to decrypt anything they receive the plaintext message directly, because the ESP32 established the TLS session with them.

Encryption protects the channel between two endpoints. When the attacker IS one of the endpoints, encryption does nothing.


Wireshark - Seeing the Broken Handshake

Now open Wireshark on Kali. Capture on eth0. Filter:

tcp.port == 8884

Reboot the ESP32 to trigger a fresh handshake from the beginning. Here is what the capture looks like:

464   TCP      ESP32 → Broker   SYN
465   TCP      Broker → ESP32   SYN, ACK
466   TCP      ESP32 → Broker   ACK
467   TLSv1.2  ESP32 → Broker   Client Hello
468   TCP      Broker → ESP32   ACK
469   TLSv1.2  Broker → ESP32   Server Hello, Certificate, Server Key Exchange, Server Hello Done
515   TCP      ESP32 → Broker   ACK
557   TLSv1.2  ESP32 → Broker   Client Key Exchange
558   TLSv1.2  ESP32 → Broker   Change Cipher Spec
559   TCP      Broker → ESP32   ACK
560   TLSv1.2  ESP32 → Broker   Encrypted Handshake Message
561   TLSv1.2  Broker → ESP32   New Session Ticket, Change Cipher Spec, Encrypted Handshake Message
563   TLSv1.2  ESP32 → Broker   Application Data  ← MQTT message goes here

Now compare this directly with the Part 6 mTLS capture:

Packet Part 6 mTLS (real broker) Part 7 MITM (fake broker)
Server Hello Server Hello, Certificate, Server Key Exchange, **Certificate Request**, Server Hello Done Server Hello, Certificate, Server Key Exchange, Server Hello Done
After ACK Certificate ESP32 sends esp32.crt :cross_mark: Missing entirely
Next Client Key Exchange Client Key Exchange
Next Certificate Verify ESP32 proves ownership :cross_mark: Missing entirely
Result Both sides verified :white_check_mark: Only broker sends cert, ESP32 checks nothing :cross_mark:

Three things are missing in Part 7:

  1. Certificate Request the fake broker never asked for the ESP32’s cert
  2. Certificate from ESP32 never asked, so never sent
  3. Certificate Verify the challenge-response proof of ownership, completely gone

The fake broker just handed over its self-signed cert, the ESP32 accepted it without checking anything, and the connection went through.

Server Hello Done with no Certificate Request


The “But What About mTLS?” Question

When I first set this up I had the same question. We set up mTLS in Part 6. The broker checks the ESP32’s certificate. Shouldn’t that catch the attacker?

Here is the thing the attacker’s fake broker is not configured to check anything. They wrote the config. They set require_certificate false. Their broker never sends a Certificate Request, never asks for a client cert, never verifies anything.

mTLS is only enforced if the broker being connected to demands it. The real broker on port 8883 demands it. The fake broker on port 8884 does not.

Real Broker:  "Show me your certificate."  ← enforces mTLS
Fake Broker:  "Come right in."              ← attacker controls this

The ESP32 connected to the fake broker. The real broker’s rules do not apply here.

And the ESP32 side with setInsecure(), the ESP32 did not verify the fake broker’s certificate either. So neither side checked anything. The handshake completed. Both endpoints were blind.


What Would Have Stopped This

If the ESP32 had kept its original three lines from Part 6:

espClient.setCACert(rootCA);
espClient.setCertificate(esp32_cert);
espClient.setPrivateKey(esp32_key);

This is what would have happened:

ESP32  → Fake Broker : Client Hello
Fake Broker → ESP32  : fake_server.crt  ← not signed by rootCA
ESP32 checks: setCACert(rootCA) is verifying this signature
Signature does not match rootCA → ❌ SSL handshake FAILED
ESP32 serial output: Failed, rc=-2
Connection dropped. Attacker gets nothing.

One line espClient.setInsecure() was the entire attack surface. Remove it and the attacker cannot get in.

The ESP32 is the last line of defence. Not the broker. The broker on port 8884 is under attacker control. The only thing that could have stopped the connection was the ESP32 refusing to trust an unverified certificate.


The Three Scenarios - How Bad Each One Is

We tested setInsecure() in detail. Here is where the other two scenarios fit in:

Scenario 1 - setInsecure() (what we just did)

TLS is active. Traffic is encrypted on the wire. But the ESP32 accepts any certificate from anyone. An attacker on the same network with a fake broker intercepts all traffic. The encryption protects nothing because the attacker IS the endpoint.

Scenario 2 - No certificate loaded, no setInsecure() call

Some developers just never load a CA cert and never call setInsecure(). The behaviour depends on the library version in many cases it defaults to insecure mode silently. Same result as Scenario 1 but harder to notice because there is no explicit setInsecure() call to grep for in a code audit.

Scenario 3 - Plain MQTT on port 1883, no TLS

No encryption, no certificates, no handshake. An attacker does not even need a fake broker. They just run Wireshark and read every MQTT message directly off the wire. No MITM needed — passive sniffing is enough. This is worse than setInsecure() because the traffic is not even encrypted.

Scenario Wire Encrypted Auth Attack Needed
Plain 1883, no TLS :cross_mark: :cross_mark: Passive Wireshark only
setInsecure() :white_check_mark: :cross_mark: Fake broker
Proper TLS / mTLS :white_check_mark: :white_check_mark: Nothing works

The bottom row is where we want to be. The top two rows are what you find in real IoT firmware in the field more often than you would expect.


Common Errors in This Setup

Client <unknown> disconnected due to protocol error

This happened to us before we added TLS to the fake broker. The ESP32 was using WiFiClientSecure which always speaks TLS. A plain MQTT broker (no TLS) cannot respond to a TLS Client Hello protocol mismatch. The fix is to give the fake broker a cert so it can speak TLS.

ESP32 connects to real broker instead of fake one

Check mqtt_port in the sketch. It must be 8884 for this lab, not 8883. The real broker on 8883 has require_certificate true so the ESP32 with setInsecure() and no client cert would get rejected there anyway.

server.key permission error when restarting real broker

After changing ownership of server.key to iotsec for the CSR regeneration, Mosquitto cannot read it. Fix:

sudo chown mosquitto:mosquitto ~/my-ca/server.key
sudo chmod 640 ~/my-ca/server.key
sudo systemctl restart mosquitto

The Real World Parallel

This is not a theoretical attack. In real IoT deployments:

  • Developers add setInsecure() to debug a cert problem. The deadline comes. The firmware ships with setInsecure() still in it.
  • Devices get deployed in factories, hospitals, homes. They publish sensor data, commands, status all going to whoever is on the same network with a fake broker.
  • The traffic is encrypted. Wireshark shows TLS. The security team sees TLS and thinks the device is secure. It is not.

The lesson from this part: TLS without certificate verification is not TLS security. It is TLS theatre.

The encryption is real. The protection is gone.


What Just Happened - Full Picture

We took the most secure setup from Part 6 - mTLS, both sides verified, broker rejecting uncertified clients and broke it completely with one line.

espClient.setInsecure() told the ESP32 to stop being the watchdog. The fake broker presented a certificate that had nothing to do with our Root CA. The ESP32 accepted it without question.

We watched it in Wireshark. The Certificate Request from the broker was gone. The ESP32’s Certificate packet was gone. The Certificate Verify was gone. Three steps that defined mTLS in Part 6 all absent. The handshake completed anyway. The message arrived at the attacker’s broker.

The real broker on port 8883 never saw a single packet from the ESP32 in this entire session.

And the fix: remove setInsecure(), restore the three cert lines from Part 6. The ESP32 becomes the gatekeeper again. The fake cert gets rejected. The attacker is locked out.


What Is Coming in Part 8

Part 7 showed how MITM works when certificate verification is disabled.

Part 8 is about a more advanced defence: certificate pinning. Instead of trusting any cert signed by our Root CA, the ESP32 is hardcoded to trust only one specific certificate. Even if an attacker somehow gets a cert signed by a legitimate CA, pinning rejects it.

We will also cover how attackers bypass pinning because they do. Part 8 is where defence and offence meet properly.