Canary is a tiny, from-scratch FTP honeypot for ESP32 that lures scanners and opportunistic attackers, then blinks the on-board LED and (optionally) fires a Canarytoken containing the intruder’s IP and UTC timestamp.
It runs a believable FTP façade with PASV data connections (supports LIST and RETR), includes a WiFi captive portal for first-time setup and a web /admin UI to update the Canary URL without switching the device back into AP mode.
- Pure ESP32 implementation — no ESPCanary dependency.
- WiFiManager captive portal for initial Wi-Fi setup.
- Admin web UI (
/admin) to update the Canary URL and FTP labels while the device is running (no AP mode required). - LED alert: GPIO 2 stays LOW (off) after boot; on first intrusion it BLINKS until reset.
- Canarytoken GET with query parameters:
src=Canaryevt— one ofconnect,auth,list,retr(what triggered the alert)ip— intruder IPv4 addresst— UTC ISO-8601 timestamp (or uptime fallback if NTP not synced)
- Real PASV data channel; supports
LISTandRETRwith a small fake file set. - Config persistence via LittleFS (
/config.json).
- ESP32 dev board (e.g. ESP32-DevKitC / DOIT ESP32 DEVKIT V1)
- On-board LED wired to GPIO 2 (default). If your board uses a different LED pin, change
LED_PINin the sketch.
Power LED (red) stays on — the controlled LED is the GPIO one.
- Arduino IDE (or PlatformIO)
- ESP32 Arduino core installed
- Libraries (install from Library Manager):
WiFiManager(tzapu)ArduinoJson
- LittleFS support (included in the ESP32 core)
- Copy
canary_honeypot.inointo your Arduino project (or open it). - In Arduino IDE: Tools → Board → ESP32 Dev Module (or select your board).
- Verify and Upload.
- On first run (no stored Wi-Fi), the device creates an AP named
Canary-Setup. - Connect to that AP and open the captive portal to:
- Provide your Wi-Fi SSID & password
- (Optionally) paste a Canarytoken / webhook URL
- After saving the portal data, the device will join your Wi-Fi.
- Find the device IP (Serial Monitor prints it on boot, or check your router).
- Open
http://<device-ip>/admin. - Edit the Canary URL and the FTP Username/Password labels (labels are cosmetic) and click Save.
- New settings are persisted to LittleFS immediately.
- Control connection: port 21
- Supported control commands (plausible responses):
USER,PASS,SYST,FEAT,PWD,TYPE,NOOP,PASV,LIST,RETR,QUIT
- PASV: server opens an ephemeral data port in 20000–20999 and replies with a
227containing the chosen port. - LIST: returns a fake Unix-style directory listing (two demo files).
- RETR : returns fake file contents for the demo files.
On the first suspicious event (connect, auth, list, or retr):
- The on-board LED starts blinking (latched until you reset the device).
- If you set a Canary URL, the device performs an HTTP GET to your URL with appended parameters:
GET <your_canary_url>?src=Canary&evt=<connect|auth|list|retr>&ip=<A.B.C.D>&t=<YYYY-MM-DDTHH:MM:SSZ>
tis ISO-8601 UTC (if NTP synced) or a fallback uptime string if time is not yet synced.- The device fires the Canary on the first event and continues blinking thereafter. If you prefer repeated events, the code can be modified.
Stored in LittleFS, example:
{
"canary": "https://your-canary-url",
"ftpUser": "anonymous",
"ftpPass": "%"
}canary— the base token/webhook URL (Canary will append query parameters automatically).ftpUser/ftpPass— labels shown in admin UI (cosmetic).
You can edit these via /admin without entering AP mode.
You can use any service that exposes a URL which triggers an alert when fetched (e.g., Canarytokens.org, webhook.site, or your own webhook).
- Visit https://canarytokens.org.
- Choose Web / Web Bug / HTTP token type (names vary).
- Provide any required info (email for alerts, label, etc.) and generate the token.
- Copy the generated token URL (e.g.
https://canarytokens.org/abcd1234). - In Canary admin (
http://<device-ip>/admin) paste the token URL into the Canary URL field and Save.
- Create a webhook endpoint that logs incoming GET requests or notifies you.
- Use that URL as the Canary URL in the device admin UI.
When the device fires the token, it appends parameters:
?src=Canary&evt=<connect|auth|list|retr>&ip=<A.B.C.D>&t=<ISO8601_UTC_or_uptime>
So full example:
GET https://canarytokens.org/abcd1234?src=Canary&evt=connect&ip=10.0.0.55&t=2025-10-06T13:45:02Z
A Test Canary button lets you verify the Canary token from the device immediately (sends evt=test and current IP/time). Below are the minimal code snippets to add to your sketch.
Replace or extend the admin HTML form with a test button (example snippet — insert into the handleRoot() HTML where the form is rendered):
<form method="POST" action="/setcanary">
Canary URL: <input type="text" name="canary" style="width:80%" value="..."><br><br>
FTP Username (label): <input type="text" name="ftpuser" value="..."><br><br>
FTP Password (label): <input type="text" name="ftppass" value="..."><br><br>
<input type="submit" value="Save">
</form>
<form method="POST" action="/testcanary" style="margin-top:10px;">
<input type="submit" value="Test Canary (send evt=test)">
</form>adminServer.on("/setcanary", HTTP_POST, handleSetCanary);
adminServer.on("/testcanary", HTTP_POST, handleTestCanary);void handleTestCanary() {
// Use last known intruder IP if set, else use device IP
String ip = lastIntruderIP.length() ? lastIntruderIP : WiFi.localIP().toString();
fireCanary("test", ip); // same function used for real triggers
String html = "<!doctype html><html><body style='font-family:sans-serif'>";
html += "<h3>Test Canary fired</h3>";
html += "<p>URL: " + canaryURL + "</p>";
html += "<p>IP used: " + ip + "</p>";
html += "<p><a href='/admin'>Back to admin</a></p>";
html += "</body></html>";
adminServer.send(200, "text/html", html);
}This will send a single GET to your configured Canary URL with evt=test, ip, and t. Use this to quickly verify alerts are delivered.
From any machine (or using curl):
curl "https://<token-url>?src=Canary&evt=test&ip=1.2.3.4&t=2025-10-06T13:45:02Z"This helps verify that your token provider accepts GET requests and that alerts are delivered.
- No captive portal: Connect to SSID
Canary-Setupand openhttp://192.168.4.1/. - Cannot find IP: Check Serial Monitor at 115200 baud — the sketch prints the device IP at boot.
- LED never blinks: Ensure your board’s user LED is actually on GPIO 2 or update
LED_PIN. - Canary not firing: Verify the URL in
/adminand ensure the device has Internet access. - LIST/RETR failing: Ensure your FTP client uses PASV.
If you prefer PlatformIO for builds, here is a minimal platformio.ini you can drop into the project root:
[env:esp32dev]
platform = espressif32
board = esp32dev
framework = arduino
monitor_speed = 115200
lib_deps =
tzapu/WiFiManager@^0.18.0
bblanchon/ArduinoJson@^6.20.0
build_flags =
-DCORE_DEBUG_LEVEL=0Adjust
lib_depsversions if you need different releases.LittleFSis provided by the ESP32 Arduino core.
- Serve real files from LittleFS instead of fake contents.
- Log intruder activity (commands, timestamps) to LittleFS for later review.
- Change blink pattern or switch to solid ON after N seconds of blinking.
- Add JSON
/api/statusendpoints for automated monitoring. - Allow multiple Canary URLs or periodic re-firing for repeated alerts.
Contributions welcome! Ideas that help the project:
- Improve FTP realism (directory trees, timestamps).
- Add persistent logging of intruder session data.
- Add authenticated admin UI or TLS for the control web server.
- Add PlatformIO examples or CI build scripts.
This project is released under the MIT License. Include this LICENSE file in your repo and/or copy the text below.
MIT License
Copyright (c) 2025 Mike de Landgraaf
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.