How Does an ESP32 Securely Connect to AWS IoT Core? A Practical MQTT/TLS Deep Dive

Objective

The objective was to build a simple end-to-end IoT pipeline:

DHT11 Sensor → ESP32 → Wi-Fi → MQTT/TLS → AWS IoT Core → MQTT Test Client

The ESP32 reads:

:thermometer: Temperature
:droplet: Humidity

and publishes the values as JSON to AWS IoT Core.

I also wanted to verify the reverse communication path by publishing a message from AWS IoT Core and receiving it on the ESP32.

Prerequisites

AWS

  • AWS account
  • Access to AWS IoT Core
  • Permission to create:
    • Things
    • Certificates
    • IoT policies

AWS’s own IoT Core getting-started flow starts with an AWS account and then moves through creating IoT resources, configuring the device, and testing MQTT messages.

Hardware

  • ESP32 development board
  • DHT11 sensor
  • Breadboard
  • Jumper wires

Software

  • Arduino IDE
  • ESP32 board package
  • DHTesp
  • PubSubClient
  • ArduinoJson

For simulation

I used Wokwi first, so I could validate the application flow before moving to physical hardware, and then you can try with actual devices.

Step 1 — Connect the DHT11 to ESP32

For setup,

DHT11 ESP32
VCC 3V3
DATA D15
GND GND

The DHT11 should also not be polled too frequently. I kept the reading interval at around 2 seconds or more.

Step 2 — Open AWS IoT Core

Go to:

AWS Console → IoT Core

AWS IoT Core represents connected devices using Thing objects in the IoT registry. A Thing can represent a physical device or sensor.

Before connecting the ESP32, I created the AWS-side resources required for the device.

The important pieces are:

Thing + Certificate + IoT Policy

The AWS documentation describes the same relationship:

  • Thing → represents the device
  • X.509 certificate → authenticates the device
  • IoT policy → authorizes what the device can do

Step 3 — Create the IoT Policy

Go to:

AWS IoT Core → Security → Policies → Create policy

For this POC, I used a simple policy:

{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Allow",
      "Action": [
        "iot:Connect",
        "iot:Publish",
        "iot:Subscribe",
        "iot:Receive"
      ],
      "Resource": "*"
    }
  ]
}


For example:

  • iot:Connect → connect to AWS IoT Core
  • iot:Publish → publish MQTT messages
  • iot:Subscribe → subscribe to MQTT topics
  • iot:Receive → receive messages

AWS documents that the certificate authenticates the device, while the policy attached to the certificate determines which IoT operations the device is permitted to perform.

:warning: Important

The Resource: "*" approach is convenient for a POC, but it should not be treated as a production policy.

AWS also recommends restricting resources instead of using the wildcard when stronger security is required.

Step 4 — Create the AWS IoT Thing

Go to:

AWS IoT Core → All devices → Things → Create things

Then:

  1. Choose Create single thing
  2. Click Next
  3. Enter a Thing name
  4. Keep the remaining Thing properties as required for this POC
  5. Choose Next

For my setup, I used a Thing name that was also used as the MQTT client ID.

AWS recommends choosing Thing names carefully because the name cannot simply be renamed later; a new Thing must be created if the name needs to change.

Step 5 — Generate the Device Certificate

During Thing creation:

Configure device certificate → Auto-generate a new certificate

Then attach the IoT policy created earlier.

The final relationship becomes:

ESP32 → Device Certificate → Thing

and

Device Certificate → IoT Policy

AWS specifically notes that attaching the certificate to a Thing establishes the device-to-Thing relationship, while attaching the policy authorizes actions such as connecting and publishing.

Step 6 — Download the Certificates

This is one of the most important steps.

Download and securely save:

1. Device Certificate

device.pem.crt

2. Private Key

private.pem.key

3. Amazon Root CA

Amazon-root-CA-1.pem

The AWS Developer Guide identifies these files as the certificate and key files required for device configuration.

:warning: Important

Save the files before leaving the certificate download screen.

AWS specifically warns that the certificate files generated during this process are not available again from that page after leaving it.

The private key is especially important.

Do not:

:cross_mark: Commit it to Git
:cross_mark: Push it to GitHub
:cross_mark: Share it in a community post
:cross_mark: Put it directly into publicly accessible source code

For my POC, I kept the certificates in a separate secrets.h file.


Step 7 — Get the AWS IoT Endpoint

Go to:

AWS IoT Core → Settings → Device data endpoint

You will get an endpoint similar to:

xxxx-ats.iot.<region>.amazonaws.com

This endpoint is used by the ESP32 to connect to AWS IoT Core over MQTT/TLS.

AWS’s troubleshooting guidance also recommends checking that the device is using the correct device data endpoint when diagnosing connectivity problems.


Step 8 — Add Certificates to secrets.h

I kept the sensitive configuration separate from the main Arduino code.

Example:

#include <pgmspace.h>

#define THINGNAME "your-thing-name"

const char WIFI_SSID[] = "Wokwi-GUEST";
const char WIFI_PASSWORD[] = "";

const char AWS_IOT_ENDPOINT[] =
    "xxxx-ats.iot.<region>.amazonaws.com";

static const char AWS_CERT_CA[] PROGMEM = R"EOF(
-----BEGIN CERTIFICATE-----
...Amazon Root CA...
-----END CERTIFICATE-----
)EOF";

static const char AWS_CERT_CRT[] PROGMEM = R"KEY(
-----BEGIN CERTIFICATE-----
...device certificate...
-----END CERTIFICATE-----
)KEY";

static const char AWS_CERT_PRIVATE[] PROGMEM = R"KEY(
-----BEGIN RSA PRIVATE KEY-----
...private key...
-----END RSA PRIVATE KEY-----
)KEY";

For physical hardware, I changed:

Wokwi-GUEST

to actual Wi-Fi SSID and password.


Step 9 — Install Arduino Libraries

In Arduino IDE, install:

DHTesp
PubSubClient
ArduinoJson

The ESP32 code uses:

  • WiFi.h → Wi-Fi connectivity
  • WiFiClientSecure.h → TLS connection
  • PubSubClient.h → MQTT
  • ArduinoJson.h → JSON payload
  • DHTesp.h → DHT11 sensor reading

Step 10 — ESP32 Code

The basic flow in my code is:

Start ESP32
     ↓
Connect to Wi-Fi
     ↓
Synchronize time using NTP
     ↓
Load AWS certificates
     ↓
Connect to AWS IoT Core using MQTT
     ↓
Subscribe to MQTT topic
     ↓
Read DHT11
     ↓
Create JSON payload
     ↓
Publish to AWS IoT Core
     ↓
Listen for incoming messages
     ↓
Repeat

The key MQTT configuration was:

#define AWS_IOT_PUBLISH_TOPIC   "iotwokwi/pub"
#define AWS_IOT_SUBSCRIBE_TOPIC "iotwokwi/sub"

The sensor data was published as JSON:

{
  "humidity": "61.2",
  "temperature": "27.45"
}


Step 12 — Connect to AWS IoT Core

The ESP32 connects to port:

8883

using:

  • AWS IoT endpoint
  • Root CA
  • Device certificate
  • Private key

Example:

net.setCACert(AWS_CERT_CA);
net.setCertificate(AWS_CERT_CRT);
net.setPrivateKey(AWS_CERT_PRIVATE);

client.setServer(AWS_IOT_ENDPOINT, 8883);

Then the MQTT client connects using the Thing Name as the client ID:

client.connect(THINGNAME);

AWS IoT Core uses the certificate presented by the device for authentication, and MQTT policies determine whether the device is authorized to perform operations such as publish and subscribe.

Step 13 — Publish Sensor Data

After reading the DHT22:

TempAndHumidity data = dhtSensor.getTempAndHumidity();

I created a JSON payload:

StaticJsonDocument<200> doc;

doc["humidity"] = humidity;
doc["temperature"] = temperature;

and published it:

client.publish(
    AWS_IOT_PUBLISH_TOPIC,
    jsonBuffer
);

The AWS IoT message broker then receives the MQTT message.

AWS describes the message broker as the service that allows devices and applications to publish and receive messages from one another.

Step 14 — Subscribe and Receive Messages

The ESP32 also subscribes to:

iotwokwi/sub

From the AWS IoT MQTT Test Client, I published:

{
  "message": "hello device"
}

The ESP32 received the message through the MQTT callback and printed it in the Serial Monitor.

So the final communication was:

              AWS IoT Core
             /            \
            /              \
ESP32 → Publish          Subscribe ← ESP32
  Sensor Data                  ↑
                              |
                        Command / Message

Step 15 — Test Using AWS MQTT Test Client

Go to:

AWS IoT Core → Test → MQTT test client

Test ESP32 → AWS

Subscribe to:

iotwokwi/pub

You should start seeing messages such as:

{
  "humidity": "61.2",
  "temperature": "27.45"
}

AWS also recommends using the MQTT test client to view MQTT messages as they pass through the message broker.

Test AWS → ESP32

Publish to:

iotwokwi/sub

Payload:

{
  "message": "hello device"
}

The message should appear in the ESP32 Serial Monitor.

Step 16 — Verify in Serial Monitor

Open Arduino Serial Monitor at:

115200 baud

The expected flow is approximately:

[WiFi] Connecting...
[WiFi] Connected!

[Time] Syncing via NTP...
[Time] Synced!

[AWS] TLS certificates loaded

[AWS] Connecting to AWS IoT endpoint...
[AWS] IoT Connected!

[DHT] Temp: 27.45°C
[DHT] Humidity: 61.2%

[MQTT] Publishing payload...
[MQTT] Publish OK

The data should then appear in the AWS IoT MQTT Test Client.

Step 17 — Move from Wokwi to Physical Hardware

Once the Wokwi version worked, I moved to the physical ESP32.

The main changes were:

Wokwi

SSID = Wokwi-GUEST
Password = ""

Physical ESP32

SSID = <actual Wi-Fi>
Password = <actual password>

The AWS IoT configuration remained the same:

Thing → Certificate → Policy → Endpoint → MQTT Topics


Troubleshooting

client.state() = -2

Usually investigate TLS/network configuration:

  • Certificate
  • Private key
  • Root CA
  • Endpoint
  • Device clock
  • NTP synchronization
  • TLS configuration

AWS’s connectivity troubleshooting flow starts by checking that the connection is valid, the certificate is valid and active, and the policy permits the required operation.


client.state() = 5

Check:

  • Is the certificate active?
  • Is the policy attached?
  • Does the policy allow the requested action?
  • Are the MQTT topic permissions correct?

AWS explicitly notes that a device connection can be refused even when the certificate is active if there is no appropriate policy attached.

client.state() = 5

Check:

  • Is the certificate active?
  • Is the policy attached?
  • Does the policy allow the requested action?
  • Are the MQTT topic permissions correct?

AWS explicitly notes that a device connection can be refused even when the certificate is active if there is no appropriate policy attached.

:pushpin: Technical references:

AWS IoT Core Developer Guide — the official guide covers connecting devices, creating Things, certificates and policies, configuring devices, MQTT testing, fleet provisioning, and security.