DEV Community

Cover image for Building a Real Time Attack Visualizer: SSH Honeypot and Cloudflare Workers
F4LCON
F4LCON

Posted on AI-assisted

Building a Real Time Attack Visualizer: SSH Honeypot and Cloudflare Workers

Every second, bots scan the internet for open SSH ports. I wanted to see exactly what they were doing, so I built a honeypot that catches them all and puts them on a live world map.

HIVE is a low-interaction honeypot written in Rust. A small sensor pretends to be an SSH server and a forgotten nginx box. Bots find it on their own. Every login attempt and web probe goes to a Cloudflare Worker, and the map at xivlabs.tech shows them as they happen.

Right now it's catching around 7000 attempts a day from ~130 unique IPs. The most-tried password is 123456.

Live SSH attempts hitting the honeypot

How it works

bots ──▶ :22 / :80
           │
           ▼
      hive-sensor (Rust, VM)
      counts every event, hourly buckets on disk
           │  1 signed request/min
           ▼
      hive Worker (Rust/WASM)
           │
           ▼
      D1  ──▶ /stats, /recent (30s edge cache) ──▶ xivlabs.tech
Enter fullscreen mode Exit fullscreen mode

The sensor runs on an Oracle Cloud Always Free VM. The Worker runs on Cloudflare's free tier. The whole thing costs nothing to run.

The interesting constraint

Cloudflare's free plan allows 100k D1 row writes per day across the whole account. A public port 22 can draw tens of thousands of attempts a day, so one row per attempt would burn the quota and break every other project on the account.

So the sensor does the counting instead. It keeps hourly buckets in memory for 7 days, saves them to stats.json so restarts don't lose history, and caps distinct keys per bucket so memory stays bounded. Once a minute it ships a single signed request with the stats snapshot and the 10 newest events. The Worker upserts one snapshot row, inserts those events, and prunes the feed back to 500 rows.

That's about 21 rows written per minute — roughly 30k a day — no matter how hard the honeypot gets hit. /stats reads one row, /recent reads only what it returns, both behind a 30s edge cache.

Nobody gets in

This is the part people ask about first:

  • No shell. Every SSH login is rejected. There's no channel, no command execution. The HTTP side never reads request bodies and always returns the same static page.
  • Bounded. 256 open connections max (10 per IP), 30–60s session limits, capped string lengths, and an in-memory queue that drops the oldest events when full.
  • Sandboxed. Runs as an unprivileged hive user under systemd with a read-only filesystem, no new privileges, and a syscall filter. systemd-analyze security exposure: 1.5. It gets CAP_NET_BIND_SERVICE only, to bind ports 22 and 80.
  • Isolated. Its own VM. Not a home network, not a box holding anything else.

Privacy

The live map and the public API only show masked networks (203.0.113.x) and countries. Full addresses leave the VM only for the blocklist: anything that hit the honeypot at least 3 times in 7 days gets shipped hourly, kept in one D1 row behind an authenticated /export, and published every Monday to hive-blocklist in iptables, nginx and plain-text formats. Private and reserved ranges are never listed.

The API

Endpoint What
GET /stats 24h/7d totals, unique sources, top usernames, passwords, paths, countries, user agents, map points
GET /recent?limit=50 Latest events (max 100)
GET /export Bearer-auth. Snapshot + blocklist, used by the weekly Action
POST /ingest Sensor only. HMAC-SHA256 signed, 5-minute clock skew window

Public endpoints are cached at the edge for 30s and only send CORS headers to allowed origins.

Try it

The README has full setup instructions if you want to run your own. It takes about 20 minutes on a free Oracle VM.

If you've run a honeypot before, I'd like to hear what you did with the data — the blocklist is the obvious use, but there's a lot more in there.

Top comments (0)