How-To-Tutorials · September 28, 2025

How to Implement Secure OTA Firmware Updates on ESP32 with Arduino IDE

how to implement secure ota firmware updates on esp32 with arduino ide

Secure OTA Firmware Updates on ESP32 with HTTPS

Shipping an IoT device without OTA update capability is asking for trouble. The moment you find a bug in the field, you'll wish you had it. But OTA done wrong is a security nightmare -- an attacker who can push a malicious firmware image to your device owns it completely. HTTPS-based OTA with proper certificate validation is the baseline you should start from.

Here's how to set up secure OTA updates on ESP32 using the Arduino IDE 2.x and the ESP32 Arduino core v3.x.

Prerequisites

  • Arduino IDE 2.x installed with the ESP32 board package v3.x
  • An ESP32 development board (ESP32, ESP32-S3, ESP32-C3, etc.)
  • A Wi-Fi network the ESP32 can connect to
  • A web server with HTTPS enabled to host firmware binaries
  • Basic understanding of TLS/SSL certificates

Parts and Tools

  • ESP32 development board
  • USB cable for initial programming
  • Arduino IDE 2.x
  • HTTPS-capable web server (even a simple cloud instance with Let's Encrypt works)
  • OpenSSL (for certificate extraction and verification)

Steps

  1. Set Up Arduino IDE for ESP32

    If you haven't already, open Arduino IDE 2.x. Go to File > Preferences and add the ESP32 board manager URL:

    https://espressif.github.io/arduino-esp32/package_esp32_index.json

    Then go to Tools > Board > Boards Manager, search for "esp32", and install the Espressif ESP32 package. As of early 2026, you'll get v3.x of the Arduino ESP32 core, which is built on ESP-IDF v5.3 under the hood.

    Quick note: the old board manager URL (dl.espressif.com/dl/package_esp32_index.json) still works but redirects to the new one. Use the new URL directly to avoid confusion.

  2. Understand the Two OTA Approaches

    There are two distinct OTA methods on ESP32, and people often confuse them:

    ArduinoOTA -- pushes firmware from your development machine to the ESP32 over the local network. Great for development, but not what you want for production field updates. It's unencrypted by default and requires the device and your machine to be on the same network.

    HTTP/HTTPS OTA (httpUpdate) -- the ESP32 pulls a firmware binary from a remote server. This is what you want for production devices. With HTTPS and certificate pinning, it's genuinely secure.

    We'll implement both below -- ArduinoOTA for development convenience, and HTTPS pull-based OTA for production use.

  3. Set Up ArduinoOTA for Development

    This is your development workflow accelerator. Instead of plugging in USB every time, push updates over Wi-Fi:

    #include <WiFi.h>
    #include <ArduinoOTA.h>
    
    const char* ssid = "your_SSID";
    const char* password = "your_PASSWORD";
    
    void setup() {
        Serial.begin(115200);
    
        WiFi.begin(ssid, password);
        while (WiFi.status() != WL_CONNECTED) {
            delay(500);
            Serial.print(".");
        }
        Serial.printf("\nConnected. IP: %s\n", WiFi.localIP().toString().c_str());
    
        // Set a password so random people on your network can't flash your device
        ArduinoOTA.setPassword("your_ota_password");
    
        ArduinoOTA.onStart([]() {
            String type = (ArduinoOTA.getCommand() == U_FLASH) ? "firmware" : "filesystem";
            Serial.println("OTA start: " + type);
        });
        ArduinoOTA.onEnd([]() {
            Serial.println("\nOTA complete");
        });
        ArduinoOTA.onProgress([](unsigned int progress, unsigned int total) {
            Serial.printf("Progress: %u%%\r", (progress * 100) / total);
        });
        ArduinoOTA.onError([](ota_error_t error) {
            Serial.printf("OTA Error[%u]: ", error);
            if (error == OTA_AUTH_ERROR) Serial.println("Auth Failed");
            else if (error == OTA_BEGIN_ERROR) Serial.println("Begin Failed");
            else if (error == OTA_CONNECT_ERROR) Serial.println("Connect Failed");
            else if (error == OTA_RECEIVE_ERROR) Serial.println("Receive Failed");
            else if (error == OTA_END_ERROR) Serial.println("End Failed");
        });
    
        ArduinoOTA.begin();
    }
    
    void loop() {
        ArduinoOTA.handle();
        // Your application code here
    }

    Always set an OTA password. Without it, anyone on your Wi-Fi network can flash arbitrary firmware to your device. Not great.

  4. Implement Secure HTTPS-Based OTA for Production

    This is the real deal. The ESP32 connects to your server over HTTPS, downloads the new firmware binary, and flashes itself. Here's a solid implementation:

    #include <WiFi.h>
    #include <HTTPClient.h>
    #include <Update.h>
    #include <WiFiClientSecure.h>
    
    const char* ssid = "your_SSID";
    const char* password = "your_PASSWORD";
    const char* firmwareUrl = "https://your-server.com/firmware/device_v2.bin";
    const char* currentVersion = "1.0.0";
    
    // Root CA certificate for your server (PEM format)
    // Extract with: openssl s_client -connect your-server.com:443 -showcerts
    const char* rootCACert = R"EOF(
    -----BEGIN CERTIFICATE-----
    MIIDdzCCAl+gAwIBAgIEAgAAuTANBgkqhki...
    -----END CERTIFICATE-----
    )EOF";
    
    void checkForUpdate() {
        WiFiClientSecure client;
        client.setCACert(rootCACert);
    
        HTTPClient https;
        https.begin(client, firmwareUrl);
        https.addHeader("X-Current-Version", currentVersion);
    
        int httpCode = https.GET();
    
        if (httpCode == HTTP_CODE_OK) {
            int contentLength = https.getSize();
            if (contentLength <= 0) {
                Serial.println("Invalid content length");
                return;
            }
    
            if (!Update.begin(contentLength)) {
                Serial.println("Not enough space for OTA");
                return;
            }
    
            WiFiClient* stream = https.getStreamPtr();
            size_t written = Update.writeStream(*stream);
    
            if (written == contentLength) {
                Serial.println("Firmware written successfully");
            } else {
                Serial.printf("Only wrote %d of %d bytes\n", written, contentLength);
            }
    
            if (Update.end()) {
                if (Update.isFinished()) {
                    Serial.println("Update successful. Rebooting...");
                    ESP.restart();
                }
            } else {
                Serial.printf("Update error: %s\n", Update.errorString());
            }
        } else if (httpCode == 304) {
            Serial.println("Firmware is up to date");
        } else {
            Serial.printf("HTTP error: %d\n", httpCode);
        }
    
        https.end();
    }
    
    void setup() {
        Serial.begin(115200);
        WiFi.begin(ssid, password);
        while (WiFi.status() != WL_CONNECTED) {
            delay(500);
        }
        checkForUpdate();
    }
    
    void loop() {
        // Check for updates periodically, e.g., every 24 hours
        static uint32_t lastCheck = 0;
        if (millis() - lastCheck > 86400000UL) {
            checkForUpdate();
            lastCheck = millis();
        }
        // Your application code here
    }

    The key security element here is setCACert(). This pins the TLS connection to your server's CA certificate, preventing man-in-the-middle attacks. Without it, an attacker on the same network could intercept the update and push malicious firmware.

    To extract your server's root CA certificate, run:

    openssl s_client -connect your-server.com:443 -showcerts 2>/dev/null | openssl x509 -outform PEM
  5. Add Version Checking and Rollback Protection

    You don't want the device re-flashing the same firmware every boot. Have your server return HTTP 304 (Not Modified) when the device already has the latest version, or embed version info in the firmware and compare before updating.

    On the ESP32 Arduino core v3.x, you can also leverage ESP-IDF's built-in rollback protection. If the new firmware crashes on first boot, the bootloader can automatically roll back to the previous working version. Enable this in your partition table configuration with an "ota_data" partition.

    For extra security, consider signing your firmware binaries. ESP-IDF v5.3 supports secure boot v2, which verifies firmware signatures before execution. The Arduino core v3.x can use this too, though configuration requires editing the sdkconfig.

  6. Test the Full OTA Flow

    Flash the initial firmware via USB. Then:

    1. Build a new version of your firmware (change the version string so you can tell the difference)
    2. Upload the .bin file to your HTTPS server
    3. Power-cycle the ESP32 and watch the Serial Monitor
    4. Verify it downloads and installs the new firmware
    5. Confirm the device boots with the new version

    Test failure scenarios too: kill the Wi-Fi mid-update, serve a corrupted binary, serve a binary that's too large for the OTA partition. Make sure the device recovers gracefully in each case.

Troubleshooting

  • Wi-Fi won't connect: Double-check SSID and password (case-sensitive). Make sure your router isn't doing MAC filtering. Try explicitly setting WiFi mode with WiFi.mode(WIFI_STA) before WiFi.begin().
  • TLS handshake fails: The root CA certificate is probably wrong or expired. Re-extract it from your server. Also check that the ESP32 has enough free heap -- TLS needs around 40-50KB of RAM, which can be tight if your app is memory-heavy.
  • Update.begin() fails: You're out of OTA partition space. Check your partition table -- the default "min_spiffs" partition scheme gives you more room for OTA. Your firmware binary must fit within the OTA partition size.
  • Device stuck in boot loop after OTA: The new firmware is crashing. If you have rollback enabled, it should recover automatically. If not, you'll need to re-flash via USB. This is exactly why you should test rollback protection before deploying to the field.

Security Checklist

Before deploying OTA-capable devices to production, verify these items:

  • HTTPS with certificate pinning is active (never use plain HTTP for firmware updates)
  • Firmware binaries are served over TLS 1.2 or 1.3
  • ArduinoOTA is disabled in production builds (it's a dev-only convenience)
  • Consider firmware signing with ESP-IDF's secure boot v2
  • OTA partition scheme is correct and tested for your binary size
  • Rollback protection is enabled and tested
  • Version checking prevents unnecessary re-flashing