A Distributed Honeypot Network With OpenCanary
March 5, 2026
The cheapest, highest-signal detection in the lab isn't a rule: it's a honeypot. A sensor that offers no legitimate service has, by definition, no legitimate traffic. So every packet it sees is an incident. This is how I run a distributed OpenCanary network across the lab and turn a canary trip into an automatic firewall block.
The idea: high signal, near-zero noise
Most detection is a fight against false positives: you're trying to separate the one malicious login from ten thousand legitimate ones. Deception inverts the problem. Nothing should ever talk to a honeypot, so there's nothing to tune out. A single hit is worth more than a day of log correlation because it carries almost no ambiguity.
The tradeoff: a honeypot only catches an attacker who interacts with it. That makes placement, not sophistication, the thing that matters. I'd rather have ten dumb sensors in the right subnets than one elaborate one in the wrong place.
Architecture
OpenCanary nodes sit in every network segment, each emulating a handful of tempting services. They log as JSON to a central collector, which Wazuh ingests:
MGMT VLAN ── canary (SSH, HTTP) ─┐
SERVER VLAN ─ canary (SSH,SMB,MySQL)┼─▶ syslog/JSON ─▶ Wazuh ─▶ pf-block (active response)
DMZ ──────── canary (HTTP, FTP) ─┤
IOT VLAN ─── canary (telnet, HTTP)─┘They're deliberately lightweight: a Python service in a container, a few MB of RAM. That's what lets me put one in every segment without thinking about cost.
Service configuration
OpenCanary is driven by a single JSON config. I enable the services that an attacker doing discovery would expect to find on that segment: SSH and SMB on the server VLAN, telnet on the IOT VLAN (because that's exactly what a Mirai-style scanner hunts for):
{
"device.node_id": "canary-server-vlan",
"ip.ignorelist": ["10.20.30.5"],
"logtype": "opencanary",
"ssh.enabled": true,
"ssh.port": 22,
"ssh.version": "SSH-2.0-OpenSSH_8.4p1 Debian-5",
"smb.enabled": true,
"smb.port": 445,
"mysql.enabled": true,
"mysql.port": 3306,
"logger": {
"class": "PyLogger",
"kwargs": {
"handlers": {
"json-socket": {
"class": "logging.handlers.SocketHandler",
"host": "10.20.30.20",
"port": 5000
}
}
}
}
}Two details that matter:
- `ip.ignorelist` carries the vuln scanner's IP. Tenable/Nessus sweeps everything, including the honeypots, and I don't want a scheduled scan paging me every night.
- `ssh.version` is set to a realistic banner. A default or obviously-fake banner is a tell that a savvy attacker will notice and avoid.
The pipeline into Wazuh
The collector receives JSON over the socket handler and writes it where the Wazuh agent tails it. A custom decoder parses the OpenCanary fields, and a rule escalates hard on any hit. This is the rule that active response keys on:
<!-- decoder -->
<decoder name="opencanary">
<prematch>"logtype": "opencanary"</prematch>
<plugin_decoder>JSON_Decoder</plugin_decoder>
</decoder>
<!-- rule: any interaction is a level-12 event -->
<rule id="100200" level="12">
<decoded_as>json</decoded_as>
<field name="logtype">opencanary</field>
<description>Honeypot hit on $(device.node_id) from $(src_host)</description>
<mitre><id>T1046</id></mitre> <!-- Network Service Discovery -->
</rule>Because Wazuh active response is scoped to rule 100200, a canary trip drops the source into the pfSense blocklist for an hour automatically. Discovery scan → detected → contained, no human in the loop.
Gotchas
Gotcha #1: port binding collisions. Running SSH on port 22 means the honeypot container needs port 22, which fights with the host's real SSH daemon. I move the host's actual sshd to a management-only port on a separate interface, and let the canary own 22 on the monitored VLAN. Get this wrong and you either can't reach the host or the honeypot isn't listening where attackers look.
Gotcha #2: a honeypot is still attack surface. These are internet-adjacent services designed to be poked. Keep them in their own restricted VLAN with egress filtering so a compromised sensor can't pivot inward. The honeypot should be a dead-end, not a doorway.
Gotcha #3: the ignorelist is load-bearing. Beyond the scanner, monitoring tools, uptime checks, and even my own curiosity generate hits. Every non-attacker source that touches a canary needs to be on the ignorelist or your "zero false positive" detector slowly becomes noisy and you stop trusting it.
Why this earns its place
- It catches what rules miss. Lateral movement and internal reconnaissance often look like normal admin activity to a log-based rule. A honeypot doesn't care what it looks like: nobody should be there at all.
- It's asymmetric in my favor. Minutes to deploy, near-zero maintenance, and it raises the attacker's cost: they now have to worry that any host they probe might be a tripwire.
- It feeds the rest of the stack. Source IPs from canary hits get promoted into the MISP feed as local IOCs, so a hit here becomes a detection everywhere.
What's next
- Add an OpenCanary web-login template that mimics the lab's real service login pages for higher-interaction deception.
- Correlate canary hits with pfSense/Suricata flow data to reconstruct the full scan path across segments.
- Rotate node IDs and banners periodically so a returning attacker can't fingerprint and skip them.