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.
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
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
hiveuser under systemd with a read-only filesystem, no new privileges, and a syscall filter.systemd-analyze securityexposure: 1.5. It getsCAP_NET_BIND_SERVICEonly, 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
- Live map: xivlabs.tech
- Source: github.com/itsF4LCON/hive
- Blocklist: github.com/itsF4LCON/hive-blocklist
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)