Putting a honeypot online is the easy part. Before long, scanners will find it and the event counter will start moving. The harder part is deciding what those events actually mean.
A connection does not automatically mean somebody compromised the device. A request for a suspicious path does not prove that anything was stolen. An attempted login does not mean the login succeeded. Most of what reached my ESP32 was automated scanning looking for anything that might answer.
That is why I wanted more than a counter. I wanted the service, source address, request, username, and enough of the payload to understand what happened. I also marked outside traffic separately from my own testing. Without that context, a test I ran myself could look like an attack when I reviewed the log later.
I learned not to make the logs collect everything blindly. Passwords were reduced to their length and a short fingerprint instead of being stored in plain text. Long fields were limited so one request could not fill the device’s memory. The ESP32 kept a rolling event history because it was never meant to become a full logging server.
Isolation mattered just as much as logging. I exposed only the services I intended to watch. I did not place the ESP32 in an unrestricted DMZ, expose its update service, or assume that an unlisted dashboard address was the same thing as real security.
I also tested the honeypot from an outside connection. A port-forwarding rule proves only that a rule was saved. It does not prove that traffic reached the device, that the service answered, or that the event was recorded.
The biggest lesson was not to confuse evidence with conclusions. A source IP tells me where a connection came from, not who was sitting behind it. A strange request tells me what reached the honeypot, not necessarily what the sender intended. Good notes should preserve that distinction.
Once I trusted the logs, the next step was turning selected events into alerts. I did not want my phone buzzing for every scanner on the internet. I wanted notifications for the activity that deserved a closer look.
One thing that surprised me was how much the same source could reveal by touching several services. One address might send an HTTP request to the SSH port, try a default Telnet login, and then test ADB a few seconds later. Looked at separately, those entries were just noise. Put together in order, they showed an automated tool working its way across the device. That was when the honeypot stopped feeling like a simple counter and started becoming something I could actually investigate.
Turning the Log Into an Alert
Once the logs started making sense, I wanted the honeypot to tell me when something more interesting happened. I did not need my phone buzzing every time an internet scanner touched port 80. That would have become useless almost immediately.
I used ntfy, a notification service that accepts ordinary HTTP requests and sends them to a phone or browser. There is no special library required. A script can send a message with a simple POST request.
The first step is installing the ntfy app and subscribing to a topic. Pick a long, random topic name that nobody could easily guess. On the public ntfy.sh service, anyone who knows the topic name may be able to subscribe or publish to it, so the topic should be treated like a password and should never be pasted into a public article or source file.
A basic test can be sent with:
curl -d "ESP32 honeypot test" https://ntfy.sh/YOUR_LONG_RANDOM_TOPICIf the phone receives that message, the notification side is working.
I did not alert on every event. My watcher looks specifically for this marker in the ESP32 log:
service=HTTP action=UPLOAD_ATTEMPT
When it finds one, it checks that the source is a public IP address and sends a notification containing the source address, HTTP method, requested path, and the short body fingerprint recorded by the honeypot. Local, loopback, and private-network addresses are ignored.
The notification contains enough information to tell me what happened without forwarding the complete request body, passwords, or other unnecessary material to a third-party service.
The watcher also starts at the end of the log instead of replaying everything already in the file. If the log is replaced or truncated, it notices and reopens it. That kept an old event from turning into a brand-new notification every time the script restarted.
Before starting it, I set two environment variables:
export ESP32_EVENT_LOG="/path/to/honeypot_events.log"
export NTFY_URL="https://ntfy.sh/YOUR_LONG_RANDOM_TOPIC"Then I ran the watcher:
python3 esp32_ntfy_alerts.pyThere is an important distinction here. The Python watcher reads a local event file that is being updated with ESP32 activity. It does not connect directly to the ESP32 or download the dashboard log by itself. The ESP32 V9 sketch keeps its own rolling event buffer and provides a protected log-dump function; a continuously updated local log is the input this watcher expects.
Here is the notification watcher I used:
#!/usr/bin/env python3
"""Send an ntfy alert for new external ESP32 HTTP upload attempts only."""
import ipaddress
import os
import re
import time
import urllib.request
from pathlib import Path
EVENT_LOG = Path(os.environ.get("ESP32_EVENT_LOG", "/path/to/honeypot_events.log"))
NTFY_URL = os.environ.get("NTFY_URL", "")
UPLOAD_MARKER = "service=HTTP action=UPLOAD_ATTEMPT"
def untrusted_text(value):
"""Prevent attacker-controlled text from becoming a clickable URL."""
return str(value).replace("://", "[:]//").replace("www.", "www[.]")
def field(line, name, default="?"):
match = re.search(rf"(?:^|\s){re.escape(name)}=([^\s]+)", line)
return match.group(1) if match else default
def is_external(address):
try:
ip = ipaddress.ip_address(address)
except ValueError:
return False
return not (ip.is_private or ip.is_loopback or ip.is_link_local)
def send_alert(line):
source = field(line, "ip")
if not is_external(source):
print(f"[esp32-ntfy] ignored local/private source {source}")
return
method = untrusted_text(field(line, "method"))
path = untrusted_text(field(line, "path"))
fingerprint = untrusted_text(field(line, "body_hex", "not recorded"))
body = (
f"From: {source}\n"
f"Method: {method}\n"
f"Path: {path}\n"
f"Body fingerprint: {fingerprint}"
)
request = urllib.request.Request(
NTFY_URL,
data=body.encode("utf-8"),
method="POST",
)
request.add_header("Title", "ESP32 external upload attempt")
request.add_header("Priority", "urgent")
request.add_header("Tags", "warning")
with urllib.request.urlopen(request, timeout=10) as response:
response.read()
print(f"[esp32-ntfy] alert sent for {source} {method} {path}")
def watch(path):
while not path.exists():
print(f"[esp32-ntfy] waiting for {path}")
time.sleep(5)
with path.open(encoding="utf-8", errors="replace") as stream:
opened_inode = os.fstat(stream.fileno()).st_ino
stream.seek(0, os.SEEK_END)
print(f"[esp32-ntfy] watching {path} from end of file")
while True:
line = stream.readline()
if line:
if UPLOAD_MARKER in line:
try:
send_alert(line)
except Exception as error:
print(f"[esp32-ntfy] notification failed: {error}")
continue
try:
current = path.stat()
if current.st_ino != opened_inode or current.st_size < stream.tell():
print("[esp32-ntfy] event log replaced or truncated; reopening")
return
except FileNotFoundError:
print("[esp32-ntfy] event log temporarily missing; reopening")
return
time.sleep(0.5)
if __name__ == "__main__":
while True:
try:
watch(EVENT_LOG)
except Exception as error:
print(f"[esp32-ntfy] watcher error: {error}; retrying in 5 seconds")
time.sleep(5)The ntfy app is free and available for iPhone and iPad through the Apple App Store and for Android through Google Play. More information, setup instructions, and examples are available at ntfy.sh. Once the app is installed, subscribe to the same long, random topic used by the Python script, send a test message, and make sure the notification arrives before leaving the watcher running.
In the next post, I’ll show what happened after those notifications started arriving: how I saved the activity outside the ESP32’s rolling log, separated outside traffic from my own testing, and worked out which events were solid enough to investigate and report. The alert got my attention; building a reliable record of what actually happened was the next step.

Leave a Reply