IoT Security Logging and Monitoring: ESP32 Lab

Static analysis finds vulnerabilities before deployment. TLS (Lab 6) protects data in transit. Flash encryption (Lab 5) protects firmware at rest. But none of these controls tell you when an attacker is actively probing your device. Security event logging bridges that gap: it records every significant security event on the device, detects patterns that indicate an active attack, generates structured alerts and can trigger automated defensive responses ā all in real time, on the device itself, without requiring an external SIEM or cloud backend. This lab implements a complete security logging and monitoring system on the ESP32 with a circular log buffer, five detection algorithms and five simulated attack scenarios that demonstrate each one triggering correctly.
Why Device-Side Security Logging Matters
Enterprise security relies on centralised logging: every device ships events to a SIEM (Security Information and Event Management) platform where analysts write detection rules and hunt for threats across thousands of devices simultaneously. This works well for servers and workstations. It works poorly for IoT devices for several reasons: many IoT devices have no persistent network connection, cloud logging introduces latency that is unacceptable for real-time threat response, shipping every event to the cloud is expensive at scale, and many IoT deployments operate in environments where cloud connectivity cannot be assumed.
Device-side security logging solves these problems by putting the detection logic on the device itself. When the device detects a brute force attack ā five failed login attempts in 60 seconds ā it can immediately lock the account, trigger an alert and log the event, all within milliseconds of the threshold being crossed. No cloud round-trip, no analyst in the loop, no delay. The device-side log also provides forensic evidence for post-incident analysis and can be periodically synchronised to a central store when connectivity is available.
For the ESP32 specifically, device-side logging is the only practical real-time option: the device often has no persistent internet connection during an attack, and by the time an analyst reviews centralised logs the attack may be complete. The detection algorithms in this lab run in under 1 millisecond of CPU time per event check, making them suitable for inclusion in any request-handling path without affecting device responsiveness.
What Events to Log and at What Severity
Not every event deserves a log entry ā logging too many low-value events fills the buffer with noise and makes genuine threats harder to find. The principle is to log events that are either inherently security-relevant or that become security-relevant in combination with other events. The following classification guides the implementation in this lab:
| Event Type | Severity | Log Always? | Reason |
|---|---|---|---|
| Device boot / restart | INFO | Yes | Unexpected reboots may indicate attack or fault injection |
| Successful login | INFO | Yes | Establishes baseline of legitimate access patterns |
| Failed login | WARNING | Yes | Individual failures are normal; patterns indicate brute force |
| Unauthorized access attempt | WARNING | Yes | Access to restricted resource without authentication |
| Configuration change | INFO | Yes | Audit trail for all settings changes |
| Brute force detected | CRITICAL | Yes | Active attack in progress ā immediate response required |
| DoS / rate limit exceeded | CRITICAL | Yes | Active attack or misconfigured client |
| Low memory warning | WARNING | Yes | May indicate memory leak or heap spray attack |
| Anomalous heap drop | WARNING | Yes | Sudden memory loss may indicate exploitation in progress |
| System error | CRITICAL | Yes | Unexpected errors may indicate exploit attempts |
Lab Prerequisites and Equipment
Required: ESP32 DOIT DevKit V1, USB cable, Arduino IDE with ESP32 board support. No additional libraries are required ā the logging system uses only built-in ESP32 Arduino framework APIs. Estimated time: 20ā25 minutes. The demonstration runs five simulated attack scenarios automatically on startup ā watch the Serial Monitor at 115200 baud to observe each detection triggering.
Complete Lab Code: Security Event Logging System
/*
* Security Event Logging and Monitoring System for ESP32
*
* Implements:
* - Circular log buffer (50 entries) with structured SecurityLog records
* - 10 event types: BOOT, LOGIN_SUCCESS, LOGIN_FAILURE, UNAUTHORIZED_ACCESS,
* CONFIG_CHANGE, LOW_MEMORY, SUSPICIOUS_ACTIVITY, DOS_ATTEMPT,
* ANOMALY_DETECTED, SYSTEM_ERROR
* - 3 severity levels: INFO, WARNING, CRITICAL
* - Brute force detection (5 failures / 60s ā CRITICAL)
* - DoS detection (100 requests / 60s ā CRITICAL)
* - Memory anomaly detection (heap drop > 50KB ā WARNING)
* - Automated threat response
*
* Hardware: ESP32 DOIT DevKit V1
* Baud rate: 115200
*/
#include <Arduino.h>
/* āā Event types āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā */
enum EventType {
EVT_BOOT = 0,
EVT_LOGIN_SUCCESS = 1,
EVT_LOGIN_FAILURE = 2,
EVT_UNAUTHORIZED = 3,
EVT_CONFIG_CHANGE = 4,
EVT_LOW_MEMORY = 5,
EVT_SUSPICIOUS = 6,
EVT_DOS_ATTEMPT = 7,
EVT_ANOMALY = 8,
EVT_SYSTEM_ERROR = 9
};
const char* EVENT_NAMES[] = {
"BOOT", "LOGIN_SUCCESS", "LOGIN_FAILURE", "UNAUTHORIZED_ACCESS",
"CONFIG_CHANGE", "LOW_MEMORY", "SUSPICIOUS_ACTIVITY", "DOS_ATTEMPT",
"ANOMALY_DETECTED", "SYSTEM_ERROR"
};
/* āā Severity levels āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā */
enum Severity { SEV_INFO = 0, SEV_WARNING = 1, SEV_CRITICAL = 2 };
const char* SEV_NAMES[] = { "INFO", "WARNING", "CRITICAL" };
const char* SEV_ICONS[] = { "ā¹", "ā ", "šØ" };
/* āā Log entry structure āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā */
struct SecurityLog {
unsigned long timestamp; /* millis() at time of event */
EventType eventType;
Severity severity;
char description[80];
char sourceIP[16]; /* "LOCAL" for device-originated events */
bool responded; /* Was an automated response taken? */
};
/* āā Circular log buffer āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā */
const int LOG_BUFFER_SIZE = 50;
SecurityLog log_buffer[LOG_BUFFER_SIZE];
int log_head = 0; /* Index of next write position */
int log_count = 0; /* Total entries written (capped at buffer size) */
/* āā Detection counters āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā */
struct LoginTracker {
int failures;
unsigned long window_start; /* Start of the current 60-second window */
bool locked;
};
struct RequestTracker {
int count;
unsigned long window_start;
};
LoginTracker login_tracker = { 0, 0, false };
RequestTracker request_tracker = { 0, 0 };
/* āā Detection thresholds āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā */
const int BRUTE_FORCE_THRESHOLD = 5; /* Failures before lockout */
const unsigned long BRUTE_FORCE_WINDOW = 60000; /* 60 second window */
const int DOS_THRESHOLD = 100; /* Requests before DoS alert */
const unsigned long DOS_WINDOW = 60000; /* 60 second window */
const int LOW_MEMORY_THRESHOLD = 30000; /* Bytes ā alert below this */
const int ANOMALY_DROP_BYTES = 50000; /* Sudden drop triggers alert */
int last_heap = 0; /* For anomaly detection */
/* āā Log a security event āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā */
void logEvent(EventType type, Severity sev,
const char* description, const char* sourceIP = "LOCAL") {
SecurityLog* entry = &log_buffer[log_head];
entry->timestamp = millis();
entry->eventType = type;
entry->severity = sev;
entry->responded = false;
strncpy(entry->description, description, sizeof(entry->description) - 1);
entry->description[sizeof(entry->description) - 1] = '\0';
strncpy(entry->sourceIP, sourceIP, sizeof(entry->sourceIP) - 1);
entry->sourceIP[sizeof(entry->sourceIP) - 1] = '\0';
log_head = (log_head + 1) % LOG_BUFFER_SIZE;
if (log_count < LOG_BUFFER_SIZE) log_count++;
/* Print to Serial in structured format */
Serial.printf("[%8lums] %s [%s] %s (src: %s)\n",
entry->timestamp,
SEV_ICONS[sev],
SEV_NAMES[sev],
description,
sourceIP);
if (sev == SEV_CRITICAL) {
Serial.println(" ā CRITICAL ā automated response triggered");
}
}
/* āā Automated threat response āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā */
void respondToBruteForce(const char* attacker_ip) {
login_tracker.locked = true;
char msg[80];
snprintf(msg, sizeof(msg), "Account locked after brute force from %s", attacker_ip);
logEvent(EVT_SUSPICIOUS, SEV_CRITICAL, msg, attacker_ip);
/* In production: also block IP at network level via ACL, send alert via MQTT */
Serial.println(" ā Account locked; IP block recommended");
}
void respondToDoS(const char* attacker_ip) {
char msg[80];
snprintf(msg, sizeof(msg), "Rate limit enforced ā %d req/min from %s",
DOS_THRESHOLD, attacker_ip);
logEvent(EVT_DOS_ATTEMPT, SEV_CRITICAL, msg, attacker_ip);
Serial.println(" ā Rate limiting active; dropping excess requests");
}
/* āā Login event handler āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā */
bool handleLoginAttempt(const char* username, bool success,
const char* ip = "192.168.1.100") {
if (login_tracker.locked) {
logEvent(EVT_UNAUTHORIZED, SEV_WARNING,
"Login attempt while account locked", ip);
return false;
}
/* Reset window if expired */
if (millis() - login_tracker.window_start > BRUTE_FORCE_WINDOW) {
login_tracker.failures = 0;
login_tracker.window_start = millis();
}
if (success) {
login_tracker.failures = 0; /* Reset on success */
char msg[80];
snprintf(msg, sizeof(msg), "Login success for user: %s", username);
logEvent(EVT_LOGIN_SUCCESS, SEV_INFO, msg, ip);
return true;
} else {
login_tracker.failures++;
char msg[80];
snprintf(msg, sizeof(msg), "Login failure #%d for user: %s",
login_tracker.failures, username);
logEvent(EVT_LOGIN_FAILURE, SEV_WARNING, msg, ip);
if (login_tracker.failures >= BRUTE_FORCE_THRESHOLD) {
respondToBruteForce(ip);
}
return false;
}
}
/* āā Request rate handler āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā */
bool handleRequest(const char* endpoint, const char* ip = "192.168.1.200") {
if (millis() - request_tracker.window_start > DOS_WINDOW) {
request_tracker.count = 0;
request_tracker.window_start = millis();
}
request_tracker.count++;
if (request_tracker.count == (int)(DOS_THRESHOLD * 0.8)) {
char msg[80];
snprintf(msg, sizeof(msg), "Request rate at 80%% of limit (%d/%d) from %s",
request_tracker.count, DOS_THRESHOLD, ip);
logEvent(EVT_SUSPICIOUS, SEV_WARNING, msg, ip);
}
if (request_tracker.count >= DOS_THRESHOLD) {
respondToDoS(ip);
return false; /* Reject this request */
}
return true; /* Accept this request */
}
/* āā Memory monitoring āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā */
void checkMemory() {
int current_heap = (int)ESP.getFreeHeap();
if (current_heap < LOW_MEMORY_THRESHOLD) {
char msg[80];
snprintf(msg, sizeof(msg), "Free heap critical: %d bytes (threshold: %d)",
current_heap, LOW_MEMORY_THRESHOLD);
logEvent(EVT_LOW_MEMORY, SEV_CRITICAL, msg);
}
if (last_heap > 0 && (last_heap - current_heap) > ANOMALY_DROP_BYTES) {
char msg[80];
snprintf(msg, sizeof(msg), "Heap dropped %d bytes suddenly (%d ā %d)",
last_heap - current_heap, last_heap, current_heap);
logEvent(EVT_ANOMALY, SEV_WARNING, msg);
}
last_heap = current_heap;
}
/* āā Print log report āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā */
void printLogReport() {
Serial.println("\nāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā");
Serial.println("ā SECURITY LOG REPORT ā");
Serial.println("āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā\n");
int total = log_count;
int critical_count = 0, warning_count = 0, info_count = 0;
/* Count by severity */
int start = (log_count == LOG_BUFFER_SIZE) ? log_head : 0;
for (int i = 0; i < log_count; i++) {
int idx = (start + i) % LOG_BUFFER_SIZE;
switch (log_buffer[idx].severity) {
case SEV_CRITICAL: critical_count++; break;
case SEV_WARNING: warning_count++; break;
case SEV_INFO: info_count++; break;
}
}
Serial.printf("Total events logged: %d\n", total);
Serial.printf("Critical: %d\n", critical_count);
Serial.printf("Warnings: %d\n", warning_count);
Serial.printf("Info: %d\n\n", info_count);
Serial.println("Recent events (newest last):");
Serial.println("āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā");
for (int i = 0; i < log_count; i++) {
int idx = (start + i) % LOG_BUFFER_SIZE;
SecurityLog* e = &log_buffer[idx];
Serial.printf("[%8lums] %-9s %-22s %s\n",
e->timestamp,
SEV_NAMES[e->severity],
EVENT_NAMES[e->eventType],
e->description);
}
Serial.println("āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā\n");
/* Security recommendations */
Serial.println("Security Status:");
Serial.printf(" Account locked: %s\n", login_tracker.locked ? "YES ā investigate" : "No");
Serial.printf(" Login failures: %d in current window\n", login_tracker.failures);
Serial.printf(" Requests this min: %d\n", request_tracker.count);
Serial.printf(" Free heap: %d bytes\n\n", ESP.getFreeHeap());
if (critical_count > 0) {
Serial.println("ā CRITICAL events detected ā review log and investigate source IPs.");
} else if (warning_count > 0) {
Serial.println("ā¹ Warning events present ā monitor for escalation.");
} else {
Serial.println("ā No critical or warning events ā normal operation.");
}
}
/* āā Simulation scenarios āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā */
void scenario1_normalOperation() {
Serial.println("\nāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā");
Serial.println(" SCENARIO 1: Normal Login Operations");
Serial.println("āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā\n");
handleLoginAttempt("alice", true, "192.168.1.10");
delay(200);
handleLoginAttempt("bob", false, "192.168.1.11"); /* One wrong password */
delay(200);
handleLoginAttempt("bob", true, "192.168.1.11"); /* Corrects it */
delay(200);
logEvent(EVT_CONFIG_CHANGE, SEV_INFO,
"Temperature threshold changed: 25C ā 28C", "192.168.1.10");
}
void scenario2_bruteForce() {
Serial.println("\nāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā");
Serial.println(" SCENARIO 2: Brute Force Attack Detection");
Serial.println("āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā\n");
/* Reset tracker for clean demonstration */
login_tracker = { 0, (unsigned long)millis(), false };
const char* attacker = "192.168.1.250";
for (int i = 1; i <= 6; i++) {
char user[20];
snprintf(user, sizeof(user), "admin");
bool triggered = handleLoginAttempt(user, false, attacker);
delay(100);
if (login_tracker.locked) {
Serial.println("\n ā³ Brute force detected and account locked!\n");
break;
}
}
}
void scenario3_dosAttack() {
Serial.println("\nāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā");
Serial.println(" SCENARIO 3: Denial of Service Detection");
Serial.println("āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā\n");
/* Reset tracker for clean demonstration */
request_tracker = { 0, (unsigned long)millis() };
const char* attacker = "192.168.1.251";
bool alerted = false;
for (int i = 0; i < 125; i++) {
bool accepted = handleRequest("/api/data", attacker);
if (!accepted && !alerted) {
Serial.printf("\n ā³ DoS threshold crossed at request %d ā dropping!\n\n", i + 1);
alerted = true;
}
if (i < 78 || i > 118) delay(10); /* Skip printing mid-range */
}
}
void scenario4_unauthorizedAccess() {
Serial.println("\nāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā");
Serial.println(" SCENARIO 4: Unauthorized Access Attempts");
Serial.println("āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā\n");
logEvent(EVT_UNAUTHORIZED, SEV_WARNING,
"Access to /admin without authentication", "192.168.1.100");
delay(200);
logEvent(EVT_UNAUTHORIZED, SEV_WARNING,
"Access to /api/keys without valid token", "192.168.1.100");
delay(200);
logEvent(EVT_SUSPICIOUS, SEV_WARNING,
"Port scan detected ā 12 probes in 2 seconds", "10.0.0.55");
}
void scenario5_resourceAnomalies() {
Serial.println("\nāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā");
Serial.println(" SCENARIO 5: Resource Exhaustion and Anomaly");
Serial.println("āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā\n");
/* Simulate a sudden heap drop */
last_heap = ESP.getFreeHeap() + 55000; /* Pretend heap was 55KB higher */
checkMemory();
last_heap = ESP.getFreeHeap();
delay(200);
logEvent(EVT_SYSTEM_ERROR, SEV_CRITICAL,
"Unexpected reboot detected ā watchdog timer triggered", "LOCAL");
}
/* āā Setup āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā */
void setup() {
Serial.begin(115200);
delay(2000);
last_heap = (int)ESP.getFreeHeap();
Serial.println("\nāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā");
Serial.println("ā Security Event Logging and Monitoring ā Lab 8 ā");
Serial.println("āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā\n");
Serial.printf("Chip: %s Rev %d\n", ESP.getChipModel(), ESP.getChipRevision());
Serial.printf("Free heap: %d bytes\n", ESP.getFreeHeap());
Serial.printf("Log buffer: %d entries (circular ā oldest overwritten when full)\n\n",
LOG_BUFFER_SIZE);
logEvent(EVT_BOOT, SEV_INFO,
"Device started ā security monitoring active");
delay(1000);
/* Run all five demonstration scenarios */
scenario1_normalOperation(); delay(500);
scenario2_bruteForce(); delay(500);
scenario3_dosAttack(); delay(500);
scenario4_unauthorizedAccess(); delay(500);
scenario5_resourceAnomalies(); delay(500);
/* Print consolidated report */
printLogReport();
Serial.println("Entering monitoring mode. Memory checked every 30 seconds.");
Serial.println("All events logged to circular buffer. Buffer wraps at 50 entries.");
}
/* āā Main loop: continuous monitoring āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā */
void loop() {
static unsigned long lastMemCheck = 0;
static unsigned long lastReport = 0;
/* Check memory every 30 seconds */
if (millis() - lastMemCheck > 30000) {
lastMemCheck = millis();
checkMemory();
}
/* Print summary every 5 minutes */
if (millis() - lastReport > 300000) {
lastReport = millis();
printLogReport();
}
delay(100);
}
Step 1: Upload and Run the Demonstration
Upload the code and open Serial Monitor at 115200 baud. The program boots, logs the boot event, then runs five simulated attack scenarios automatically in sequence. Each scenario generates a series of log entries that demonstrate one detection algorithm triggering. The entire demonstration takes approximately 30 seconds to complete, after which the consolidated log report is printed showing all events grouped by severity.
Read the Serial Monitor output carefully ā each line shows the timestamp, severity icon, severity level, event type and description. Critical events are followed by an automated response message showing what action the system took. This is the core value of device-side logging: the response happens in the same process, in the same millisecond as detection, without waiting for human review.
Step 2: Brute Force Detection Scenario
Scenario 2 simulates an attacker making six consecutive failed login attempts against the admin account from IP 192.168.1.250. The first four generate WARNING-level LOGIN_FAILURE events. The fifth attempt crosses the threshold of 5 failures within the 60-second window, triggering a CRITICAL-level response: the account is locked, a SUSPICIOUS_ACTIVITY event is logged with the attacker’s IP, and the Serial Monitor shows the automated response message.
The sixth attempt is rejected with an UNAUTHORIZED_ACCESS event because the account is now locked ā the attacker gets no further opportunities to try passwords regardless of how many requests they send. In production firmware, this lockout state should also: send an alert via MQTT to a monitoring topic, add the source IP to a temporary block list enforced at the TCP accept layer, and require administrator intervention to unlock the account.
Step 3: Denial of Service Detection Scenario
Scenario 3 simulates 125 rapid requests from IP 192.168.1.251 in a short period. At 80 requests (80% of the 100-request threshold), a WARNING-level SUSPICIOUS_ACTIVITY event is logged ā this is an early warning that gives an administrator time to investigate before the threshold is crossed. At 100 requests, a CRITICAL-level DOS_ATTEMPT event is logged and the handleRequest() function returns false, signalling to the caller that this request should be dropped.
The two-stage alerting (80% warning then 100% critical) is deliberate: it distinguishes between a misconfigured legitimate client (which might generate a burst of requests but unlikely to exceed the hard limit) and an actual denial of service attempt (which will rapidly reach and exceed the limit). The warning stage allows for automated notification and manual investigation before the hard cutoff is applied.
Step 4: Unauthorized Access Detection
Scenario 4 logs two UNAUTHORIZED_ACCESS events (attempts to reach protected endpoints without a valid authentication token) and one SUSPICIOUS_ACTIVITY event (a port scan detected from a different IP). In production firmware, each call to the route handler for a protected endpoint should call a function equivalent to logEvent(EVT_UNAUTHORIZED, SEV_WARNING, ...) when the authentication check fails, rather than simply returning a 401 response without any logging. The difference is that the 401 response tells the attacker their request was rejected, while the log entry tells you the attack is happening.
Step 5: Anomaly and Resource Exhaustion Detection
Scenario 5 simulates a sudden 55KB drop in available heap memory and logs it as an ANOMALY_DETECTED WARNING event. A sudden large heap drop in a running device can indicate: a memory leak that has been accumulating and just crossed a threshold, a large allocation by an unexpected code path (potentially triggered by a malicious packet), or a heap spray attack attempting to position data at a specific address.
The scenario also logs a SYSTEM_ERROR CRITICAL event for a simulated watchdog timer reboot. An unexpected reboot is a significant security event: watchdog timer resets in embedded systems are sometimes caused by deliberate fault injection (voltage glitching or electromagnetic interference to disrupt execution) used by attackers attempting to bypass secure boot checks or reset security state. Logging the reboot reason and count allows detection of repeated unexpected reboots that would suggest active fault injection attempts.
Understanding the Log Structure
Each SecurityLog entry in the circular buffer contains six fields: a millisecond timestamp (for ordering events within a session), the event type (one of 10 defined categories), the severity level (INFO/WARNING/CRITICAL), a human-readable description string (up to 79 characters), the source IP address or “LOCAL” for device-originated events, and a boolean flag indicating whether an automated response was taken. This structure is compact enough to hold 50 entries in approximately 8KB of RAM, which is well within the ESP32’s available heap.
The circular buffer overwrites the oldest entry when full. This means the log always contains the most recent 50 events, regardless of how long the device has been running. For forensic analysis of older events, production devices should periodically flush the buffer to SPIFFS (SPI Flash File System) or transmit it via MQTT to a persistent store. The flush should happen on every CRITICAL event (immediate) and on a scheduled basis (every hour or every power cycle) to ensure nothing is lost.
Tuning Detection Thresholds for Your Device
The default thresholds in this lab are conservative starting points. Your production device will need thresholds tuned to its specific use case:
| Parameter | Default | Too Low (Risk) | Too High (Risk) |
|---|---|---|---|
| Brute force threshold | 5 failures / 60s | Legitimate users locked out on typos | Allows many guesses before detection |
| Brute force window | 60 seconds | Slow attacker evades by spacing attempts | Legitimate retry patterns trigger lockout |
| DoS threshold | 100 req/min | Legitimate batch operations rejected | High-rate attack succeeds before detection |
| Low memory threshold | 30KB | False alarms on normal allocation patterns | OOM occurs before alert triggers |
| Anomaly heap drop | 50KB | Normal large allocations trigger false alarm | Significant leaks go undetected |
The correct approach is to run the device in a monitored test environment for a period representing normal operation, collect the statistics of login attempts, request rates and heap usage, and set thresholds at 3ā5x the 99th percentile of normal values. This ensures the thresholds are above normal operational noise but well below any realistic attack pattern.
Production Enhancements: Persistent Storage and Remote Logging
The in-memory circular buffer in this lab loses all events on power cycle. Production devices need persistent storage and remote alerting. The four most important enhancements are:
SPIFFS persistent log: Flush the circular buffer to a JSON file on SPIFFS on every CRITICAL event and on a scheduled interval. Use a rotating file scheme (log_1.json, log_2.json) to avoid overwriting the current log if the device reboots during a write.
MQTT security alerts: Publish CRITICAL events to a dedicated MQTT security topic immediately on detection. This gives near-real-time visibility to a central monitoring system even for devices behind NAT or firewalls, since the MQTT publish is an outbound connection initiated by the device.
UDP syslog: For devices on a local network with a syslog server, UDP syslog (RFC 5424) is a lightweight way to ship events to a central log collector. The ESP32’s UDP socket API makes this straightforward to implement without any additional libraries.
/*
* MQTT alert publisher ā add to logEvent() for CRITICAL events.
* Requires WiFi connection and a configured PubSubClient instance.
*/
void publishSecurityAlert(SecurityLog* entry) {
if (!mqttClient.connected()) return;
char payload[200];
snprintf(payload, sizeof(payload),
"{\"ts\":%lu,\"severity\":\"%s\",\"type\":\"%s\","
"\"description\":\"%s\",\"source\":\"%s\"}",
entry->timestamp,
SEV_NAMES[entry->severity],
EVENT_NAMES[entry->eventType],
entry->description,
entry->sourceIP);
mqttClient.publish("iot/security/alerts", payload, /* retain= */ false);
}
Conclusion
Security logging transforms an ESP32 from a passive target into an active participant in its own defence. The system implemented in this lab detects brute force attacks within seconds of the threshold being crossed, identifies denial of service attempts before they succeed, flags memory anomalies that may indicate exploitation in progress, and maintains a structured audit trail of every security-relevant event. The detection algorithms run in under 1 millisecond per event and add negligible overhead to any request-handling path. Integrating this logging system into production firmware is a matter of calling logEvent() at the appropriate points in the authentication, request handling and memory management code ā the detection and response logic is entirely self-contained. Lab 9 covers the final piece of the production security picture: secure OTA firmware updates that allow the detection thresholds, the CA certificates, and the firmware itself to be updated safely over the lifetime of a deployed device.