You built a smart home controller with an ESP32. It reads temperature, toggles relays, and serves a slick web dashboard over your WiFi network. It works perfectly. You show it off to friends. Life is good.
Then someone on the internet finds your device, logs in with no password, and turns on your water heater at 3 AM.
This is not a hypothetical scenario. Shodan, the search engine for internet-connected devices, indexes millions of unprotected IoT endpoints every day. Many of them are hobby projects running on ESP32 or ESP8266 boards with zero authentication, zero encryption, and hardcoded credentials sitting right there in the firmware.
If your device touches the internet, it is attackable. This guide will walk you through five layers of security you should implement in every ESP32 project, with practical code you can use today.
Why IoT Security Matters for Makers
The maker community has a culture of "get it working first." That is a fine approach for prototyping on your bench. It becomes a serious problem when that prototype goes live on your home network, connected to your WiFi, potentially port-forwarded to the internet.
Here is what is at stake:
- Privacy: A compromised sensor can leak your daily routines, occupancy patterns, and environmental data.
- Physical safety: Actuators controlling heaters, locks, pumps, or power outlets can cause real damage if controlled by an attacker.
- Network pivot: An insecure ESP32 on your home network can be used as a stepping stone to attack your laptop, phone, or NAS.
- Botnet recruitment: Your device can be conscripted into a DDoS botnet (Mirai infected 600,000+ IoT devices in 2016).
- Legal liability: In India, CERT-In guidelines increasingly hold device operators responsible for unsecured endpoints.
The good news: securing an ESP32 project is not difficult. It just requires deliberate effort at each layer.
Common Vulnerabilities in Maker Projects
Before we fix things, let us understand what typically goes wrong:
| Vulnerability | Example | Risk Level |
|---|---|---|
| Hardcoded WiFi credentials | const char* password = "myWiFi123"; in source code |
High |
| No transport encryption | HTTP web server on port 80 | High |
| No authentication | Anyone on the network can access the dashboard | Critical |
| Default/no passwords | Admin interface with no login | Critical |
| Unvalidated inputs | Web form directly controlling GPIO without sanitization | Medium |
| No firmware signing | OTA updates accepted from any source | High |
| Open MQTT | Publishing/subscribing without TLS or credentials | High |
| Debug serial output in production | Printing credentials and tokens to Serial | Medium |
| No watchdog | Device hangs silently, no recovery | Low |
If your project has even two of these, it is vulnerable. Most hobby projects have all of them.
Threat Model: Who Attacks Home IoT?
Understanding your adversary helps you prioritize defenses:
Script kiddies and automated scanners are the most common threat. Tools like Shodan, Censys, and Masscan continuously scan the entire IPv4 address space. If your ESP32 web server is port-forwarded to the internet, it will be found within hours. These attackers exploit default credentials and known vulnerabilities.
Neighbours and local network users can attack devices on shared WiFi networks. If you run IoT devices on a flat home network with no segmentation, anyone with your WiFi password can access every device.
Malware and botnets actively seek out poorly secured IoT devices for recruitment. The Mirai botnet and its variants specifically target devices with default credentials and open Telnet/SSH ports.
Targeted attackers are rare for home projects but relevant if your device controls physical security (locks, cameras) or is deployed in a business context.
For most makers, the realistic threat is automated scanning and opportunistic attacks. The defences in this guide will protect you against all of these.
Layer 1: WiFi Security
Your first line of defence is the wireless network itself.
Use WPA2-PSK (AES) at minimum. WPA3 is preferred if your router and all devices support it. WPA3-SAE provides protection against offline dictionary attacks, which is a significant improvement.
Do not rely on hidden SSIDs. Hiding your network name does not provide security. WiFi probe requests from your own devices broadcast the SSID in plaintext. Any tool like Wireshark or Aircrack-ng can discover hidden networks in seconds.
Do not rely on MAC filtering. MAC addresses are transmitted in plaintext in WiFi frames. An attacker can observe a legitimate MAC address and clone it in under a minute using macchanger.
What actually helps:
- Use a strong, unique WiFi password (20+ characters, random).
- Enable WPA3 if your router supports it.
- Put IoT devices on a separate network or VLAN (more on this in the network segmentation section).
- Disable WPS (WiFi Protected Setup) on your router. WPS has known brute-force vulnerabilities.
Layer 2: Transport Encryption
Every byte your ESP32 sends over the network should be encrypted. This means HTTPS for web servers and TLS for MQTT.
HTTPS Web Server on ESP32
The ESP32 has hardware-accelerated cryptographic operations, making TLS feasible even on this resource-constrained platform. Here is a complete HTTPS web server with a self-signed certificate:
#include <WiFi.h>
#include <WiFiClientSecure.h>
#include <ESP32WebServer.h>
#include <ESPmDNS.h>
// In production, load these from NVS (see credential management section)
const char* ssid = "YOUR_SSID";
const char* password = "YOUR_PASSWORD";
// Self-signed certificate (generate with OpenSSL — see instructions below)
const char serverCert[] PROGMEM = R"EOF(
-----BEGIN CERTIFICATE-----
MIICqDCCAZACCQDMq2inYEWypjANBgkqhkiG9w0BAQsFADAWMRQwEgYDVQQDDAtl
c3AzMi5sb2NhbDAeFw0yNjAxMTAwMDAwMDBaFw0yNzAxMTAwMDAwMDBaMBYxFDAS
... (your certificate here) ...
-----END CERTIFICATE-----
)EOF";
const char serverKey[] PROGMEM = R"EOF(
-----BEGIN RSA PRIVATE KEY-----
MIIEpAIBAAKCAQEA... (your private key here) ...
-----END RSA PRIVATE KEY-----
)EOF";
ESP32WebServer server(443);
void handleRoot() {
server.send(200, "text/html",
"<html><body><h1>Secure ESP32 Server</h1>"
"<p>Connection is encrypted with TLS.</p></body></html>");
}
void setup() {
Serial.begin(115200);
WiFi.begin(ssid, password);
while (WiFi.status() != WL_CONNECTED) {
delay(500);
Serial.print(".");
}
Serial.println("\nConnected: " + WiFi.localIP().toString());
server.setServerCertificate(serverCert, serverKey);
server.on("/", handleRoot);
server.begin();
if (MDNS.begin("esp32")) {
Serial.println("mDNS: https://esp32.local");
}
}
void loop() {
server.handleClient();
}
Generate a self-signed certificate on your computer:
openssl req -x509 -nodes -days 365 -newkey rsa:2048 \
-keyout server.key -out server.crt \
-subj "/CN=esp32.local"
Copy the contents of server.crt and server.key into your code (or better yet, store them in NVS).
Important: Self-signed certificates will trigger browser warnings. For local network use, this is acceptable. For production deployments, consider using Let's Encrypt with a domain name pointing to your device via a reverse proxy.
MQTT with TLS
If your project uses MQTT (the standard protocol for IoT messaging), you must encrypt the connection. Here is how to connect to a broker over TLS on port 8883:
#include <WiFi.h>
#include <WiFiClientSecure.h>
#include <PubSubClient.h>
const char* mqtt_server = "your-broker.example.com";
const int mqtt_port = 8883;
const char* mqtt_user = "esp32_device";
const char* mqtt_pass = "strong_random_password_here";
// CA certificate of your MQTT broker
const char* ca_cert = R"EOF(
-----BEGIN CERTIFICATE-----
MIIDrzCCApegAwIBAgIQCDvgVp... (broker CA cert) ...
-----END CERTIFICATE-----
)EOF";
WiFiClientSecure espClient;
PubSubClient mqtt(espClient);
void reconnectMQTT() {
while (!mqtt.connected()) {
Serial.print("MQTT connecting...");
// Generate a unique client ID
String clientId = "esp32-" + String(random(0xffff), HEX);
if (mqtt.connect(clientId.c_str(), mqtt_user, mqtt_pass)) {
Serial.println("connected");
mqtt.subscribe("home/commands/#");
} else {
Serial.print("failed, rc=");
Serial.println(mqtt.state());
delay(5000);
}
}
}
void setup() {
Serial.begin(115200);
// ... WiFi connection code ...
espClient.setCACert(ca_cert);
// Optional: verify server certificate
// espClient.setInsecure(); // NEVER use this in production
mqtt.setServer(mqtt_server, mqtt_port);
mqtt.setCallback(mqttCallback);
}
void loop() {
if (!mqtt.connected()) {
reconnectMQTT();
}
mqtt.loop();
}
Never use setInsecure() in production. It disables certificate verification, making the connection vulnerable to man-in-the-middle attacks. Always pin the CA certificate of your broker.
Layer 3: Authentication
Encryption without authentication is like locking your door but leaving the key in the lock. Anyone who reaches your server should be required to prove their identity.
Basic Authentication on ESP32 Web Server
Here is a straightforward implementation of HTTP Basic Auth:
#include <WiFi.h>
#include <WebServer.h>
#include <base64.h>
WebServer server(443); // Use with HTTPS setup from above
// Store these in NVS in production, not in source code
const char* auth_user = "admin";
const char* auth_pass = "change_this_strong_password";
bool isAuthenticated() {
if (!server.hasHeader("Authorization")) {
return false;
}
String authHeader = server.header("Authorization");
if (!authHeader.startsWith("Basic ")) {
return false;
}
String encoded = authHeader.substring(6);
String decoded = base64::decode(encoded);
String expected = String(auth_user) + ":" + String(auth_pass);
return decoded == expected;
}
void requireAuth() {
if (!isAuthenticated()) {
server.sendHeader("WWW-Authenticate", "Basic realm=\"ESP32\"");
server.send(401, "text/plain", "Authentication required");
return;
}
}
void handleDashboard() {
if (!isAuthenticated()) {
server.sendHeader("WWW-Authenticate", "Basic realm=\"ESP32\"");
server.send(401, "text/plain", "Authentication required");
return;
}
server.send(200, "text/html",
"<html><body><h1>Authenticated Dashboard</h1>"
"<p>Welcome, admin.</p></body></html>");
}
void handleToggle() {
if (!isAuthenticated()) {
server.sendHeader("WWW-Authenticate", "Basic realm=\"ESP32\"");
server.send(401, "text/plain", "Authentication required");
return;
}
// Safe to perform action — user is verified
digitalWrite(2, !digitalRead(2));
server.send(200, "text/plain", "Toggled");
}
void setup() {
// ... WiFi setup ...
server.collectHeaders("Authorization");
server.on("/", handleDashboard);
server.on("/toggle", handleToggle);
server.begin();
}
Basic Auth is only secure over HTTPS. Over plain HTTP, credentials are sent in base64 encoding (which is trivially reversible, not encryption). Always pair Basic Auth with TLS.
For API-style authentication, consider using API keys passed in a custom header (X-API-Key), or bearer tokens for more complex setups. Full JWT implementation is possible on ESP32 using libraries like ArduinoJWT, but adds significant complexity.
Layer 4: Firmware Security
The ESP32 has built-in hardware support for Secure Boot and Flash Encryption. These features prevent attackers from reading your firmware (extracting credentials) or replacing it with malicious code.
Secure Boot
Secure Boot ensures that only firmware signed with your private key can run on the device. Enable it in your ESP-IDF project:
idf.py menuconfig
# Navigate to: Security features -> Enable hardware Secure Boot in bootloader
# Select Secure Boot V2
# Generate signing key (keep this safe — losing it bricks the device)
espsecure.py generate_signing_key secure_boot_signing_key.pem
# Build and flash (first time only — Secure Boot is permanent)
idf.py build flash
Warning: Enabling Secure Boot is irreversible on production ESP32 chips. It burns eFuses. Test thoroughly on a development board first. Once enabled, only firmware signed with your key will boot.
Flash Encryption
Flash encryption prevents someone from reading the contents of the SPI flash (where your code, certificates, and NVS data reside). Without it, anyone with physical access can dump your firmware using esptool.py:
# This is what an attacker can do to an unencrypted device:
esptool.py read_flash 0 0x400000 flash_dump.bin
strings flash_dump.bin | grep -i password
# Output: myWiFiPassword123 <-- your credentials exposed
Enable flash encryption:
idf.py menuconfig
# Navigate to: Security features -> Enable flash encryption on boot
# Select Development Mode (allows reflashing) or Release Mode (permanent)
In Development Mode, you can still reflash the device (useful during development). In Release Mode, flash encryption is permanent and the device can only be updated via signed OTA.
Layer 5: OTA Update Security
Over-the-air updates are convenient but introduce a significant attack surface. If an attacker can push a malicious firmware update to your device, they own it completely.
Signed OTA Updates
Always verify the integrity and authenticity of firmware before applying it:
#include <HTTPUpdate.h>
#include <WiFiClientSecure.h>
const char* firmwareURL = "https://your-server.com/firmware/latest.bin";
// Pin the server's CA certificate
const char* ota_ca_cert = R"EOF(
-----BEGIN CERTIFICATE-----
... (your OTA server CA certificate) ...
-----END CERTIFICATE-----
)EOF";
void checkForUpdate() {
WiFiClientSecure client;
client.setCACert(ota_ca_cert);
// Set the expected firmware version to prevent rollback attacks
httpUpdate.setAuthorization("Bearer your_ota_api_key");
t_httpUpdate_return result = httpUpdate.update(client, firmwareURL);
switch (result) {
case HTTP_UPDATE_OK:
Serial.println("OTA success, rebooting...");
break;
case HTTP_UPDATE_FAILED:
Serial.printf("OTA failed: %s\n",
httpUpdate.getLastErrorString().c_str());
break;
case HTTP_UPDATE_NO_UPDATES:
Serial.println("No update available");
break;
}
}
Best practices for OTA security:
- Always use HTTPS for firmware downloads. Never HTTP.
- Pin the CA certificate of your update server.
- Use API key authentication so only your devices can download firmware.
- Enable Secure Boot (Layer 4) so the device verifies firmware signatures before booting.
- Implement version checking to prevent rollback attacks (installing older, vulnerable firmware).
Credential Management: Stop Hardcoding Passwords
The single most common security mistake in maker projects is hardcoding credentials in source code. This is dangerous because:
- If you share your code on GitHub, your passwords are public.
- Anyone with physical access can extract credentials from unencrypted flash.
- Changing a password requires reflashing the entire firmware.
Using NVS (Non-Volatile Storage) with the Preferences Library
The ESP32's NVS partition provides key-value storage that persists across reboots. Combined with flash encryption, this is a secure way to store credentials:
#include <Preferences.h>
Preferences preferences;
void storeCredentials() {
preferences.begin("wifi", false); // namespace "wifi", read-write mode
// Store credentials (do this once via a setup mode or serial command)
preferences.putString("ssid", "YourNetworkName");
preferences.putString("password", "YourStrongPassword");
preferences.end();
Serial.println("Credentials stored in NVS");
}
void loadCredentials() {
preferences.begin("wifi", true); // read-only mode
String ssid = preferences.getString("ssid", "");
String password = preferences.getString("password", "");
preferences.end();
if (ssid.length() == 0) {
Serial.println("No credentials found. Entering setup mode...");
startAPConfigPortal(); // Start a config portal for first-time setup
return;
}
Serial.println("Connecting to: " + ssid);
WiFi.begin(ssid.c_str(), password.c_str());
}
// Store MQTT credentials separately
void storeMQTTCredentials(const char* server, const char* user,
const char* pass) {
preferences.begin("mqtt", false);
preferences.putString("server", server);
preferences.putString("user", user);
preferences.putString("pass", pass);
preferences.putInt("port", 8883);
preferences.end();
}
void setup() {
Serial.begin(115200);
// Check if this is first boot
preferences.begin("system", true);
bool configured = preferences.getBool("configured", false);
preferences.end();
if (!configured) {
// First boot — start WiFi AP for configuration
startAPConfigPortal();
} else {
loadCredentials();
}
}
Design pattern: On first boot, start the ESP32 as a WiFi Access Point with a configuration portal. The user connects to it, enters their WiFi and MQTT credentials via a web form, and the device stores them in NVS. On subsequent boots, it loads credentials from NVS and connects normally.
Libraries like WiFiManager automate this pattern. Combined with flash encryption (Layer 4), credentials stored in NVS are secure against physical extraction.
Input Validation
If your ESP32 serves a web interface with forms, you must validate and sanitize all inputs. Embedded devices are particularly vulnerable to buffer overflows.
void handleSetTemperature() {
if (!isAuthenticated()) {
server.send(401, "text/plain", "Unauthorized");
return;
}
if (!server.hasArg("temp")) {
server.send(400, "text/plain", "Missing 'temp' parameter");
return;
}
String tempStr = server.arg("temp");
// Validate length to prevent buffer overflow
if (tempStr.length() > 5) {
server.send(400, "text/plain", "Invalid temperature value");
return;
}
// Validate it is actually a number
float temp = tempStr.toFloat();
if (temp == 0 && tempStr != "0") {
server.send(400, "text/plain", "Temperature must be a number");
return;
}
// Validate range (physical sanity check)
if (temp < 10.0 || temp > 40.0) {
server.send(400, "text/plain", "Temperature must be between 10 and 40");
return;
}
// Safe to use the value
setTargetTemperature(temp);
server.send(200, "text/plain", "Temperature set to " + String(temp));
}
Key rules for input validation on ESP32:
- Always check string length before processing. The ESP32 has limited RAM (520 KB SRAM). A maliciously long input can crash your device.
- Validate data types. If you expect a number, verify it is a number.
- Enforce physical limits. A temperature setpoint of 9999 degrees is never valid.
- Sanitize HTML output. If you display user input back on a web page, escape HTML special characters to prevent XSS attacks.
Network Segmentation
The most impactful network-level defence is isolating your IoT devices from your main devices.
Option 1 — Guest network: Most routers let you create a guest network. Put all IoT devices on the guest network. This provides basic isolation since guest network devices typically cannot see main network devices.
Option 2 — VLAN: If you have a managed switch or a router that supports VLANs (like OpenWrt, pfSense, or Ubiquiti), create a dedicated IoT VLAN:
| VLAN | Subnet | Purpose | Internet Access |
|---|---|---|---|
| VLAN 1 | 192.168.1.0/24 | Main devices (laptop, phone) | Yes |
| VLAN 10 | 192.168.10.0/24 | IoT devices (ESP32, cameras) | Restricted |
| VLAN 20 | 192.168.20.0/24 | Home servers (NAS, Pi) | Yes |
Configure firewall rules so that:
- IoT VLAN can access the internet (for MQTT brokers, NTP, OTA updates).
- IoT VLAN cannot initiate connections to the main VLAN (prevents lateral movement).
- Main VLAN can access IoT VLAN (so you can reach your dashboards).
This way, even if an ESP32 is compromised, the attacker cannot pivot to your laptop or NAS.
Monitoring and Watchdog
Watchdog Timers
A hung device is a vulnerable device. The ESP32 has a hardware watchdog timer. Use it:
#include <esp_task_wdt.h>
#define WDT_TIMEOUT_SECONDS 30
void setup() {
esp_task_wdt_init(WDT_TIMEOUT_SECONDS, true); // true = panic on timeout
esp_task_wdt_add(NULL); // Add current task to watchdog
}
void loop() {
esp_task_wdt_reset(); // Feed the watchdog
// Your main loop code here
// If this loop takes longer than 30 seconds, the device reboots
}
Connection Monitoring
Log and monitor unusual connection patterns:
void monitorConnections() {
static int failedAuthAttempts = 0;
if (authFailed) {
failedAuthAttempts++;
Serial.printf("WARNING: Failed auth attempt #%d from %s\n",
failedAuthAttempts,
server.client().remoteIP().toString().c_str());
if (failedAuthAttempts > 5) {
Serial.println("ALERT: Possible brute force attack detected");
// Optionally: block the IP, increase delay, or trigger alert
delay(5000); // Rate limiting (simple but effective)
}
}
}
Common Mistakes to Avoid
These are the mistakes I see most often in maker project repositories:
- Using HTTP instead of HTTPS for web servers. All traffic is readable by anyone on the network.
- Default or no passwords on web interfaces. If it controls hardware, it needs authentication.
- Hardcoded credentials in source code pushed to public GitHub repositories. Use NVS or environment-based configuration.
- Using
setInsecure()for TLS connections because the "proper" way seems hard. It disables all certificate verification. - Port-forwarding ESP32 to the internet without authentication. Use a VPN (WireGuard or Tailscale) instead.
- No firmware updates after deployment. Vulnerabilities are discovered over time. Plan for OTA from day one.
- Serial debug output in production that prints credentials, tokens, or internal state.
- No input validation on web forms. A crafted request can crash or exploit your device.
- Using the same password everywhere. Your MQTT password, WiFi password, and web admin password should all be different.
- Ignoring physical security. If someone can touch your ESP32, they can dump the flash unless encryption is enabled.
10-Point Security Audit Checklist
Before deploying any ESP32 IoT project, run through this checklist:
| # | Check | Status |
|---|---|---|
| 1 | WiFi credentials stored in NVS, not hardcoded in source | |
| 2 | Web server uses HTTPS (TLS), not HTTP | |
| 3 | All web endpoints require authentication | |
| 4 | MQTT connection uses TLS (port 8883) with CA certificate pinning | |
| 5 | Input validation on all web form parameters and API inputs | |
| 6 | Serial debug prints removed or disabled in production firmware | |
| 7 | OTA updates delivered over HTTPS with authentication | |
| 8 | Flash encryption enabled (at minimum Development Mode) | |
| 9 | Device is on a separate network segment (VLAN or guest WiFi) | |
| 10 | Watchdog timer enabled, device recovers from hangs automatically |
Score yourself honestly. A score of 7 or above means your project is reasonably secure. Below 5, and you should not deploy it on any network connected to the internet.
Indian Context: CERT-In Guidelines
India's Computer Emergency Response Team (CERT-In) has issued guidelines relevant to IoT device security. While these primarily target manufacturers and enterprises, makers should be aware of them:
- Mandatory incident reporting: As of the 2022 CERT-In directive, cybersecurity incidents must be reported within 6 hours. If your compromised IoT device is part of a larger attack, you could be involved.
- Data protection: The Digital Personal Data Protection Act, 2023 (DPDP Act) applies if your IoT device collects personal data (occupancy patterns, health data, camera feeds). You are responsible for securing this data.
- ISP-level scanning: Indian ISPs have been directed to block traffic from known botnet command-and-control servers. If your device is part of a botnet, your internet connection could be flagged.
- BIS standards: The Bureau of Indian Standards is developing IoT security standards under the IS 16727 series. While not yet mandatory for DIY projects, they provide a useful reference framework.
For makers selling IoT products commercially in India, compliance with these guidelines is not optional. Even for hobby projects, following these practices keeps your devices and your network safe.
Recommended Secure Architecture
Putting it all together, here is the architecture you should aim for:
[Internet]
|
[Router + Firewall]
|
+-- VLAN 1: Main devices (laptop, phone)
|
+-- VLAN 10: IoT devices
|
+-- ESP32 (HTTPS + Auth + Flash Encryption)
| |
| +-- NVS: Encrypted credential storage
| +-- Secure Boot: Signed firmware only
| +-- OTA: HTTPS + API key + signature verification
|
+-- MQTT Broker (TLS on port 8883, ACLs per device)
Each layer reinforces the others. Encryption without authentication is incomplete. Authentication without encryption is pointless. Both without network segmentation leave you exposed to lateral movement.
Conclusion
IoT security is not a feature you add at the end. It is a design decision you make at the beginning. The ESP32 gives you the hardware capabilities for real security: TLS acceleration, secure boot, flash encryption, and sufficient resources for HTTPS and authenticated APIs.
The effort to implement these five layers is modest compared to the risk of leaving your devices unprotected. Start with the basics (HTTPS and authentication), then progressively add credential management, firmware security, and network segmentation.
Your smart home should be smart enough to keep intruders out.
Need ESP32 boards, IoT modules, or development kits for your next secure IoT project? Browse our collection at wavtron.in -- we stock genuine ESP32-DevKitC, ESP32-S3, and a wide range of sensors and actuators shipped across India.



