ESP32 Secure firmware update Over-The-Air (OTA)

ESP32 after the automatic firmware update has completed.

Once you deploy your IoT device, you won’t have physical access to reprogram or update it. It is critical to plan ahead and to have a secure mechanism for updating your embedded system or IoT device. Sometimes you will have the requirement to update your IoT device because of a new feature update, security issues, bugs, you did not have enough time to finish something on time and you had to ship your device and etc.

Since we want to rely on existing and working software platforms, in this tutorial I will use the NGINX as a web server, “encrypted” traffic by an SSL certificate from Let’s Encrypt. I will use my test domain on, I will describe everything in a step-by-step manner so that anyone can replicate and follow it. First, I will describe how to set up your domain and web server, then we will proceed with the ESP32 code. I present you with a basic Let’s Encrypt setup, it is not a production-ready setup but rather something to start you of. Let’s call it a proof of concept. The core point of the article is on the ESP32 part, however, the webserver setup is required. Keep that in mind!

Before you start, make sure your DNS records are properly set:

CNAME record: www to @
A record: * to your server IP address
A record: @ to your server IP address
My DNS records from my server

In this tutorial, I used Ubuntu Linux, feel free to use any other Linux distribution, maybe it will only differ in the used installation commands.

Installing Nginx webserver and setting up the firewall

First, make sure your Ubuntu is updated:

sudo apt-get update
sudo apt-get upgrade

If there are packages to be installed, press Y to install them.

Then install Nginx, the webserver I am going to use in this tutorial:

sudo apt install nginx

Once you have installed it, check that your website is set up properly by going to it, (your domain in this case.) You should see the default Nginx site that comes with it when it is installed. You will note, we are not yet using HTTPS but rather the unencrypted old protocol HTTP.

Nginx is running and your traffic is not yet encrypted.

Let us set up the firewall so that only HTTPS (encrypted) traffic is allowed to and from your server, aside from your SSH connection. We will use the Uncomplicated Firewall, UFW. It should come preinstalled on Ubuntu.

sudo ufw app list

Your response should look as the following:

Available applications:
  Nginx Full
  Nginx HTTP
  Nginx HTTPS

Then enable the following two options, OpenSSH and Nginx HTTPS.

sudo ufw allow 'OpenSSH'
sudo ufw allow 'Nginx HTTPS'

You should get some response like the following:

Rules updated
Rules updated (v6)

Then, enable the firewall by typing.

sudo ufw enable

You will get the following response:

Command may disrupt existing ssh connections. Proceed with operation (y|n)? y
Firewall is active and enabled on system startup

At this point, no incoming connection may be accepted aside from the one on HTTPS port 443 and SSH on port 22.
When you try to go to (your domain), you will just see connecting but actually your browser cannot connect to it, yet.

To check your firewall status, this command may come up as handy:

sudo ufw status

Your Nginx site content is located in /var/www/html/

Your Nginx configuration for your site, since it is the default one, is located in /etc/nginx/sites-available/default

Setting up the Let’s Encrypt SSL certificate

We will modify the settings in this file soon. Let’s set up the HTTPS Let’s Encrypt SSL certificate, which is free of charge. For the purpose of this demo, I used a Let’s Encrypt certificate, which needs to be renewed every three months (thanks for the comment THEAMK). Instead, you could use OpenSSL to generate self-signed certificates that you don’t need to renew every three months and download on your device. You can google how it is done with openssl.

We need to install the Let’s encrypt certbot that will install the certificate for us:

sudo apt install python3-certbot-nginx

After you have installed it, to run it, execute the following commands, make sure to replace everything with your domain.

sudo certbot --server -d -d * --manual --preferred-challenges dns-01 certonly

It will ask you a couple of questions, answer them and the response should look like:

/directory -d * --manual --preferred-challenges dns-01 certonly
Saving debug log to /var/log/letsencrypt/letsencrypt.log
Plugins selected: Authenticator manual, Installer None
Enter email address (used for urgent renewal and security notices) (Enter 'c' to

- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
Please read the Terms of Service at You must
agree in order to register with the ACME server at
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
(A)gree/(C)ancel: A

- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
Would you be willing to share your email address with the Electronic Frontier
Foundation, a founding partner of the Let's Encrypt project and the non-profit
organization that develops Certbot? We'd like to send you email about our work
encrypting the web, EFF news, campaigns, and ways to support digital freedom.
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
(Y)es/(N)o: N
Obtaining a new certificate
Performing the following challenges:
dns-01 challenge for

- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NOTE: The IP of this machine will be publicly logged as having requested this
certificate. If you're running certbot in manual mode on a machine that is not
your server, please ensure you're okay with that.

Are you OK with your IP being logged?
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
(Y)es/(N)o: Y

- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
Please deploy a DNS TXT record under the name with the following value:


Before continuing, verify the record is deployed.
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -

Please add the TXT record in your DNS settings.

Added TXT DNS record.

Before you press enter, please check that the record matches the settings provided by Let’s encrypt by typing (but type this on your host machine, not server machine):

dig TXT

You will get something as the following in the response:

; <<>> DiG 9.11.3-1ubuntu1.12-Ubuntu <<>> TXT
;; global options: +cmd
;; Got answer:
;; ->>HEADER<<- opcode: QUERY, status: NOERROR, id: 50125
;; flags: qr rd ra; QUERY: 1, ANSWER: 1, AUTHORITY: 0, ADDITIONAL: 1

; EDNS: version: 0, flags:; udp: 65494

;; ANSWER SECTION: 34 IN	TXT	"2r2n6NQRRxzxF7L6kw1VFfP1MC8YdlRXGKGv8oM2ga8"

;; Query time: 0 msec
;; WHEN: Sun Oct 04 16:52:14 CEST 2020
;; MSG SIZE  rcvd: 113

Once you can see it, go to your server ssh terminal and press enter, you should get a message where it says things have been setup successfully:

Press Enter to Continue
Waiting for verification...
Cleaning up challenges

 - Congratulations! Your certificate and chain have been saved at:
   Your key file has been saved at:
   Your cert will expire on 2021-01-02. To obtain a new or tweaked
   version of this certificate in the future, simply run certbot
   again. To non-interactively renew *all* of your certificates, run
   "certbot renew"
 - If you like Certbot, please consider supporting our work by:

   Donating to ISRG / Let's Encrypt:
   Donating to EFF:          

Setting up the Nginx server

Now we can proceed to set up the nginx server. Go to the nginx settings folder:

cd /etc/nginx/sites-available

Edit the settings file:

sudo vim default

Copy the following content over your existing “server” configuration:

server {
        server_name *;

        listen 443 ssl;
        listen [::]:443 ssl;
        ssl_certificate /etc/letsencrypt/live/;
        ssl_certificate_key /etc/letsencrypt/live/;

        root /var/www/html;

        # Add index.php to the list if you are using PHP
        index index.html index.htm index.nginx-debian.html;

        location / {
                # First attempt to serve request as file, then
                # as directory, then fall back to displaying a 404.
                try_files $uri $uri/ =404;

Make sure you replace with your own domain name (check that certbot saved the certificates in the same path as it did for me.)
Save it. Then test the Nginx configuration by typing:

sudo nginx -t

This command should tell you that everything in the settings file is OK:

nginx: the configuration file /etc/nginx/nginx.conf syntax is ok
nginx: configuration file /etc/nginx/nginx.conf test is successful

Then reload the Nginx server configuration:

sudo /etc/init.d/nginx reload

You should be able to access your web site only by as well as
Both links should produce a green keylock.

Web server traffic is encrypted now

Finally we are done with setting up the web server, we can proceed with setting up the ESP32 for update.

What is an OTA update?

Before we dive furthermore into how it works, let us make sure you understand what our goal was. Basically, instead of physically programming our ESP32 with the ESP32 programmer or serial port, we try to achieve the same only with the ESP32 downloading the compiled firmware code from the webserver by itself. What does that mean? As long as your IoT device is connected to a WiFi network that has an outgoing Internet connection, you can change the code (firmware) on it, no matter where your device is located.

In the video below, I will show you a working example. I used an ESP32 from TTGO with an LCD display on it because we can easily see the updated version (I show on the display the currently running software version.) This helps us distinguish the versions, you can use any other ESP32 board or MCU, without the display or whatsoever.

As you can see, version 1. of the software is running on the ESP32. Once I keep the right button pressed for more than two seconds, the ESP32 will check if there is an update for it on our (mine) server. And if the version on the server is newer, it will proceed with the automatic download and update. I will reset the device while it updates, just to show you that it is safe and you won’t lose the first working version just in case of a power outage while it is updating or some other internet downloading issues.

The OTA update mechanism allows a device to update itself based on data received while the normal firmware is running (for example, over WiFi or Bluetooth.)

OTA requires configuring the Partition Table of the device with at least two “OTA app slot” partitions (ie ota_0 and ota_1) and an “OTA Data Partition”.

The OTA operation functions write a new app firmware image to whichever OTA app slot is not currently being used for booting. Once the image is verified, the OTA Data partition is updated to specify that this image should be used for the next boot.

This was copied from the official ESP32 documentation:

This tutorial is based on the esp32FOTA library. There are a couple of other libraries as well, however, I did not try them out.

Since we have set up the webserver, let us get the certificate from our web server. We can get it by typing the following command, which will also modify the certificate so that we can nicely copy and paste it into our code. Make sure you replace my domain name with your own one (at the beginning and somewhere in the middle.)

openssl s_client -connect -showcerts 2>/dev/null </dev/null | awk '/^.*'""'/,/-----END CERTIFICATE-----/{next;}/-----BEGIN/,/-----END CERTIFICATE-----/{print}' | sed -e 's/\(.*\)/\"\1\\n\" /g'

This command will return you the formatted certificate with new line character at the end of each line.

"-----BEGIN CERTIFICATE-----\n" 
"-----END CERTIFICATE-----\n"
  1. Copy the above certificate into the siteCertificate variable, from line #24 to #49.
  2. Then make sure your WiFi network SSID and password match your current WiFi network settings (of course, these variables will be saved in the EEPROM for your device, they are here only for the DEMO purpose.) Line #20 and #21.
  3. Label your device with a unique name, line #57 in my case it is: “lab4iot_devicee”
  4. Set the firmware version number varible currentVersionNumber of your firmware, line #51, keep in mind it is an integer and the number should increment with the newer version.
  5. Set up your host address and the path to the firmware JSON file, line #71 setupOTAUpdate(“”, “/firmware.json”);
  6. Set up your firmware JSON file for the webserver with the device(s) unique name, firmware version, host address as well as the path to the location of the newly build firmware binary file.
  7. Build your new firmware version and upload it to the specified location.

Once you have completed this, and you got the basic idea how it works and understand all the steps. You can use Jenkins or some other tool to automatize the complete process of updates for your IoT device.

#include <esp32fota.h>
#include <WiFiClientSecure.h>

#include <TFT_eSPI.h>
#include <SPI.h>
#include <Button2.h>

#define RIGHT_BUTTON 35

void setScreen();
void displayText(String text);
void update();
void setupOTAUpdate(String host, String firmwareJSONLocation);
void connectToWiFi(const char *ssid, const char *password);
void longPress(Button2& btn);

const char *ssid = "YourSSIDName";         // your network SSID (name of wifi network)
const char *password = "YourSSIDPassword"; // your network password

char *siteCertificate = 
"-----BEGIN CERTIFICATE-----\n" 
"-----END CERTIFICATE-----\n";

const int currentVersionNumber = 1;

Button2 btn = Button2(RIGHT_BUTTON);

TFT_eSPI tft = TFT_eSPI(135, 240);
WiFiClientSecure clientForOta;
secureEsp32FOTA esp32OTA("lab4iot_devicee", currentVersionNumber);

void setup()

  String firmwareVersion = "Version " + String(currentVersionNumber);

  connectToWiFi(ssid, password);

  setupOTAUpdate("", "/firmware.json");

void loop() {

void setScreen() {
  tft.setCursor(0, 0);

void displayText(String text) {
  tft.setTextColor(TEXT_COLOR, TFT_BLACK);
  tft.drawString(text, tft.width() / 2, tft.height() / 2 - 16);

void update() {
  esp32OTA._certificate = siteCertificate;

void setupOTAUpdate(String host, String firmwareJSONLocation) {
  esp32OTA._host = host;
  esp32OTA._descriptionOfFirmwareURL = firmwareJSONLocation;

  esp32OTA._certificate = siteCertificate;
  esp32OTA.clientForOta = clientForOta;

void connectToWiFi(const char *ssid, const char *password) {
  WiFi.begin(ssid, password);

  // attempt to connect to Wifi network:
  while (WiFi.status() != WL_CONNECTED)
    // wait 1 second for re-trying

void longPress(Button2& btn) {
    unsigned int time = btn.wasPressedFor();
    if (time > 2000) {
      bool shouldExecuteFirmwareUpdate = esp32OTA.execHTTPSCheck();
      if (shouldExecuteFirmwareUpdate) {
    "type": "lab4iot_devicee",
    "version": 2,
    "host": "",
    "port": 443,
    "bin": "/firmware.bin"

Since I am using Platform IO for my project, make sure your settings file platform.ini is set up properly for the board you are using. In my case it is the following settings:

platform = espressif32
board = esp32doit-devkit-v1
framework = arduino
monitor_speed = 115200
lib_deps =

In case you got lost a bit, you can watch the video where I explain and show in detail what I do to get the working example above.

I hope you enjoyed this article as much as I had fun doing it. Thank you, feel free to leave comments.

Refik Hadzialic Written by:


  1. theamk
    February 22, 2021


    Let’s Encrypt certificates expire in 2-3 months – and your updater will break at that time.

    Make the self-signed cert with 20 year expiration instead — it is also free, The only downside is you’ll need a separate port for this, as this certificate will not work for the browser, only for IOT.

    • Refik Hadzialic
      February 22, 2021

      You are right regarding the expiry date. However, changing something on a regular basis is much better than trusting something forever. There should be just an additional mechanism that will allow the device to get a valid certificate instead of storing it forever and relying upon that it will never change. I used Let’s Encrypt because it is free of charge, and you get a signed certificate. Of course, you can use a self-signed certificate. Thanks for your comment, you have a valid point however from the security side I believe it makes less sense!

      • theamk
        February 22, 2021

        If someone follows your article as written, they will get the broken updater in 3 months. “Better security” does not help much if the thing does not work.

        Maybe in the future you can write another article which describes this additional mechanism, but right now, your article describes something that no one should ever do.

        • Refik Hadzialic
          February 22, 2021

          You are right and I updated the post with your comment. However, I expect someone who came this far to know that the Let’s Encrypt certificate will expire every three months and there is no such thing as a free lunch. This is just a post to get people started. My intention wasn’t to present a bulletproof production-ready design.

      • moep
        February 22, 2021

        Thanks for this guide. OTA updates are something i’d like to implement myself at some point in the future for my own projects. But I’m also not comfortable with the way you implemented the certificate validation, too. As theamk explained the need to update your certificate store on all of your IOT devices every few month is just not sustainable in long term.

        Isn’t the described problem exacly what certificate chains are for? Just embed the two known root certificates of Lets’s Encrypt (ISRG Root X1 and ISRG Root X2, can be found on the Let’s Encrypt page) in your code and let the ESP32 check if the certificate chain that your webserver provides originates from one of the stored root certificates. That way you should be good to go for the next few decades.

        Unfortunately if not yet worked with any of the cryptographic ESP32 libs, so i have no idea on how to implement this on an ESP32.

        • Refik Hadzialic
          February 22, 2021

          Thank you for the comment. You are right. This is not bulletproof and far away from the perfect solution, it is something to get you started. If you want an SSL certificate that works for longer periods, you can google “self-signed openssl certificate, howto”. Maybe there are solutions online that support this out of the box.

        • Mac
          February 24, 2021

          Hi MOEP,
          “update your certificate store on all of your IOT devices every few month is just not sustainable in long term”
          Isn’t this how web browsers work? Would anything prevent an MCU from doing something similar? I’m thinking this isn’t a bad approach and was wondering if there was already a library/framework that does this already? I’m thinking if this is done and combined with the ability for the MCU to check from *multiple* OTA firmware source URLs, this should work for a LONG time?
          I have a few ESP32’s and that’s what I’m hoping to do–unless I figure out there’s a better (and safer) way to do this before I’m done. 🙂

          • Refik Hadzialic
            February 24, 2021

            Thank you for your feedback, updating the certificates on a regular basis should be a rule of thumb and as you have said, browsers do it as well. I believe everyone has different perspectives of IoT devices and where they will be deployed. Some expect to use it all the time with a sufficient power supply, for some this is overhead and expect it to work for longer periods since it is maybe battery operated without the battery having charging capabilities.

Leave a Reply

Your email address will not be published. Required fields are marked *