Secure Over-The-Air Firmware Updates for ESP32 IoT Devices
A device that can’t be patched is a ticking time bomb. In the fast‑moving world of IoT, a single vulnerable sensor can open the door to a whole network. That’s why getting OTA (over‑the‑air) updates right on an ESP32 matters more than ever.
Why OTA Is No Longer a Luxury
When I first started tinkering with ESP32 boards in my garage, I used a USB cable to flash new code. It worked, but every time a device was in a hard‑to‑reach spot—say, a water‑meter in a basement—I had to pull the whole unit out. That was a nightmare. Today, with devices deployed in factories, farms, and smart homes, pulling hardware for a bug fix is not an option. OTA lets you push fixes, new features, or even a whole new bootloader without ever touching the device.
But “push” is only half the story. If the update channel is not secured, an attacker can inject malicious firmware, turn your sensor into a spy, or simply brick the device. The challenge is to make the OTA path as tight as a sealed chip package while keeping the process simple enough for a hobbyist and robust enough for an industrial deployment.
The Building Blocks of a Secure OTA Flow
1. Signed Firmware Images
Think of a signed image as a sealed envelope. The firmware is the letter, the signature is the wax seal. Only a firmware image that carries a valid seal should be accepted.
- Private key – Kept on your build server, never leaves the CI/CD pipeline.
- Public key – Embedded in the ESP32 flash at a known location. The device uses it to verify the signature.
- Signature algorithm – ECDSA with the NIST P‑256 curve is a good balance of security and speed on the ESP32.
When you compile a new build, you run a small script that hashes the binary (SHA‑256 is the go‑to) and then signs that hash with the private key. The resulting signature is appended to the image or stored in a separate metadata file.
2. Encrypted Transport
Even a signed image can be sniffed and replayed if the transport is plain HTTP. Use TLS (HTTPS) for the download server. The ESP32’s built‑in mbedTLS library handles the handshake with minimal RAM overhead.
If you want an extra layer, encrypt the firmware payload itself with AES‑256 in GCM mode. The decryption key can be derived from a device‑specific secret stored in the ESP32’s efuse block, making each unit’s payload unique.
3. Atomic Flash Writes
The ESP32 flash is split into two partitions: one for the running firmware (factory) and one for the OTA update (OTA_0, OTA_1). The OTA API writes the new image to the inactive partition, verifies the signature, then flips a flag so the next reboot boots from the new slot. If verification fails, the device simply boots from the old, known‑good partition. This “fail‑safe” approach prevents a bad update from leaving the device bricked.
4. Version and Rollback Checks
Keep a small version record in NVS (non‑volatile storage). When a new image arrives, compare its version number to the stored one. Reject downgrades unless you explicitly allow them for a rollback scenario. A rollback counter can also limit how many times a device can revert, protecting against a “downgrade attack” where an attacker forces you back to a vulnerable version.
Step‑by‑Step Implementation
Below is a practical outline that I use on my own ESP32 projects. Feel free to adapt it to your own build system.
Step 1 – Prepare the Build Environment
- Install the ESP‑IDF (the official development framework).
- Add a signing script to your CI pipeline. A simple Python snippet using
cryptographylibrary can do the job:
import hashlib, sys
from cryptography.hazmat.primitives import hashes, serialization
from cryptography.hazmat.primitives.asymmetric import ec
binary = sys.argv[1]
priv_key = serialization.load_pem_private_key(
open('private_key.pem','rb').read(),
password=None
)
digest = hashlib.sha256(open(binary,'rb').read()).digest()
signature = priv_key.sign(digest, ec.ECDSA(hashes.SHA256()))
open(binary + '.sig','wb').write(signature)
- Store the public key in a header file and flash it to a dedicated partition (e.g.,
ota_key).
Step 2 – Set Up the OTA Server
A tiny HTTPS server is enough. I use python -m http.server behind an Nginx reverse proxy with a valid cert. Place the signed binary and its .sig file in the same directory. Enable HTTP range requests so the ESP32 can resume a partially downloaded file.
Step 3 – ESP32 OTA Client Code
In your app_main add the OTA routine:
#include "esp_ota_ops.h"
#include "esp_https_ota.h"
#include "esp_log.h"
static const char *TAG = "ota";
void perform_ota(void)
{
esp_http_client_config_t config = {
.url = "https://my-ota-server.com/firmware.bin",
.cert_pem = (char *)server_cert_pem_start,
};
esp_https_ota_config_t ota_config = {
.http_config = &config,
.partial_http_download = true,
};
esp_err_t ret = esp_https_ota(&ota_config);
if (ret == ESP_OK) {
ESP_LOGI(TAG, "OTA update successful, restarting...");
esp_restart();
} else {
ESP_LOGE(TAG, "OTA update failed: %s", esp_err_to_name(ret));
}
}
The esp_https_ota function automatically checks the signature if you have set CONFIG_OTA_ENABLE_SIGNED_IMAGES in menuconfig. It also writes to the inactive partition and validates the image before swapping.
Step 4 – Verify and Test
- Run the firmware on a dev board. Trigger the OTA function via a button press or a MQTT command.
- Watch the logs; you should see “signature verified” and “image validated”.
- Power‑cycle the board and confirm it boots the new version.
- Try a corrupted image (tamper with a byte) and verify the device rejects it and stays on the old firmware.
Common Pitfalls and How to Avoid Them
- Running out of RAM during verification – The ESP32 has limited heap. Keep the signature size small (ECDSA‑256 is 64 bytes) and avoid loading the whole binary into RAM. The OTA API streams the data, so you’re safe as long as you don’t add extra buffering.
- Clock drift breaking TLS – The ESP32’s RTC can lose time after a power loss, causing TLS handshake failures. Use SNTP to sync time at boot before any OTA attempt.
- Hard‑coded URLs – If you bake the server address into the firmware, a change in infrastructure forces a full reflashing. Store the URL in NVS so you can update it via a small “config OTA” payload that does not require a full firmware flash.
A Quick Personal Tale
The first time I tried OTA on a field‑deployed soil sensor, I forgot to embed the public key. The device downloaded the new binary, tried to verify, and then simply rebooted into the old firmware. I spent an afternoon climbing a hill, pulling the sensor out, and flashing it via USB. After that, I made a checklist: key embedded, TLS cert verified, version bump recorded. Now I can push updates from my laptop while sipping coffee on the porch.
Bottom Line
Secure OTA on ESP32 is not a “nice‑to‑have” feature; it’s a necessity for any production IoT deployment. By signing your images, encrypting the transport, using atomic flash writes, and adding version checks, you close the biggest attack windows. The ESP‑IDF already gives you most of the heavy lifting—your job is to wire it together thoughtfully and test rigorously.
Stay curious, keep your keys safe, and let the chips do the heavy lifting.
- → Securing IoT Deployments with Smart Card Authentication: Step‑by‑Step Implementation @smartcardsolutions
- → A Practical Checklist for Auditing Your Smart Home’s Security Settings @smarthomewatch
- → DIY Motion-Detection Alerts: Building a Low‑Cost Surveillance System @smarthomewatch
- → Choosing the Right Low-Power RF Transceiver for Battery‑Operated IoT Devices @circuittalk
- → Integrating IoT with Card Readers: Step‑by‑Step Checklist to Boost Transaction Security @cardreadershub