Skip to content
itsF4LCONPublic

About

Low-interaction SSH/HTTP honeypot in Rust with a live attack map on Cloudflare Workers

Resources

Stars

0 stars

Watchers

0 watching

Forks

Latest commit

 

History

22 Commits

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

hive

A low-interaction honeypot with a live attack map, weekly reports and a public blocklist. A small Rust 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 xivlabs.tech shows them on a world map as they happen.

 bots ──▶ :22 / :80 ──▶ hive-sensor (Rust, VM) ──1 signed request/min──▶ hive Worker (Rust/WASM) ──▶ D1
                        counts every event,        stats snapshot +                          1 snapshot row
                        hourly buckets on disk     10 newest events                          + newest 500 events
                                                                                ▲
                                                   xivlabs.tech ── GET /stats, /recent (cached 30s)

Safety model

  • Nobody ever gets in. Every SSH login is rejected, so there's no shell, channel or command execution. The HTTP side never reads request bodies and always returns the same static page.
  • Bounded: at most 256 open connections (10 per IP), 30–60s session limits, capped string lengths, and an in-memory queue that drops the oldest events when it's 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: runs on its own VM, not on a home network or a machine that holds 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: addresses that attacked the honeypot at least HIVE_BLOCKLIST_MIN times in the last 7 days are sent to the Worker once an hour, kept in one D1 row behind /export, and published every Monday in hive-blocklist. Private and reserved ranges and anything in HIVE_IGNORE_IPS are never listed.

Why the sensor does the counting

The Worker runs on Cloudflare's free plan, where D1 allows 100k rows written per day across the whole account. A public port 22 can draw tens of thousands of attempts a day, so storing one row per attempt would use that up, and break other projects sharing the account.

So the sensor counts every event in memory instead. It keeps hourly buckets 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 sends a single signed request with the stats snapshot and the 10 newest events, and once an hour a second one with the blocklist. The Worker upserts one snapshot row, inserts those events and prunes the feed back to 500 rows, and the blocklist is one more upsert an hour. That's at most about 21 rows written per minute (~30k a day) however hard the honeypot is hit. /stats reads one row and /recent reads only the rows it returns, both behind a 30s edge cache.

Layout

Path What
sensor/ The Rust honeypot: SSH (russh), HTTP (hyper), GeoLite2 lookups, signed shipping
sensor/hive-sensor.service systemd unit
worker/ Cloudflare Worker (Rust): POST /ingest, GET /stats, GET /recent
test/worker.sh Tests for the Worker's auth, validation, IP masking, CORS and export

API

Endpoint
POST /ingest Sensor only. Body is {sent_at, events[≤50], stats?}, signed with X-Hive-Signature: hex(HMAC-SHA256(secret, body)). Requests more than 5 min off the Worker's clock are rejected. The snapshot is size-capped and cleaned before it's stored.
GET /recent?limit=50 Latest events (max 100)
GET /export Needs Authorization: Bearer <HIVE_EXPORT_TOKEN>. The latest snapshot plus the blocklist (ip, ssh, http, last_seen) and when it was last updated. Used by hive-blocklist's weekly Action.
GET /stats The latest snapshot: 24h/7d totals, unique sources, top usernames, passwords, paths, countries and user agents, and map points

The public endpoints are cached at the edge for 30s and only send CORS headers to origins in ALLOWED_ORIGINS.

Setup

1. Worker

cd worker
wrangler d1 create hive-db             # put the id in wrangler.toml
wrangler d1 execute hive-db --remote --file=schema.sql
openssl rand -hex 32                    # the shared secret, keep it for the sensor too
wrangler secret put HIVE_SECRET
openssl rand -hex 32                    # a second token, for hive-blocklist's weekly Action
wrangler secret put HIVE_EXPORT_TOKEN
wrangler deploy                         # serves on hive.xivlabs.tech

2. VM (Oracle Cloud Always Free)

  1. Create an Always Free VM running Ubuntu 24.04 or 22.04: an Ampere A1 (1 OCPU / 6 GB) if you have ARM allowance left, or an AMD VM.Standard.E2.1.Micro (1 GB is plenty; use the x86 binary).
  2. Create a Network Security Group with ingress TCP 22 and 80 from 0.0.0.0/0 and TCP 2200 from your own IP, and attach it to the VM's VNIC. An NSG only affects this VM, while a subnet security list would also open the ports on every other VM in the subnet.
  3. Move your real SSH to port 2200 before anything else. Oracle's Ubuntu images also firewall with iptables, so open the ports there too:
    # Insert the rules just before the image's catch-all REJECT (its position varies between images).
    reject=$(sudo iptables -L INPUT --line-numbers -n | awk '/REJECT/ {print $1; exit}')
    for p in 80 2200; do sudo iptables -I INPUT "$reject" -p tcp --dport $p -m state --state NEW -j ACCEPT; done
    sudo iptables -L INPUT -n --line-numbers    # 22 (already allowed by the image), 2200 and 80 must be above REJECT
    sudo netfilter-persistent save
    sudo sed -i 's/^#\?Port .*/Port 2200/' /etc/ssh/sshd_config
    # 24.04 starts sshd through ssh.socket, 22.04 through ssh.service. Never start the socket on 22.04:
    # it would listen on port 22 and take it from the honeypot.
    if systemctl is-enabled --quiet ssh.socket 2>/dev/null; then
      sudo systemctl daemon-reload && sudo systemctl restart ssh.socket
    else
      sudo systemctl restart ssh
    fi
    Check that ssh -p 2200 ubuntu@<vm-ip> works from a second terminal before you close the first. Then check that nothing is left on 22:
    sudo ss -tlnp | grep -E ':(22|2200)\b'    # expect sshd on 2200 only
  4. sudo apt install unattended-upgrades so the VM patches itself.

3. Sensor

Download the binary for your VM from the latest release (aarch64 for Ampere, x86_64 for the AMD Micro), or build it on the VM with cargo build --release in sensor/. Then:

sudo useradd --system --no-create-home --shell /usr/sbin/nologin hive
sudo install -m 755 hive-sensor-* /usr/local/bin/hive-sensor
sudo install -d -m 750 -o root -g hive /etc/hive
sudo tee /etc/hive/env >/dev/null <<'ENV'
HIVE_INGEST_URL=https://hive.xivlabs.tech/ingest
HIVE_SECRET=<the secret from step 1>
HIVE_GEOIP_DB=/etc/hive/GeoLite2-City.mmdb
ENV
sudo chmod 640 /etc/hive/env && sudo chgrp hive /etc/hive/env

For locations on the map, the sensor needs a city database in GeoIP2 format. The easiest is DB-IP City Lite, which is free, needs no account, and can be downloaded on the VM (it's updated monthly, so change the month in the URL):

curl -L https://download.db-ip.com/free/dbip-city-lite-2026-09.mmdb.gz | gunzip > city.mmdb
sudo install -m 644 -o root -g hive city.mmdb /etc/hive/GeoLite2-City.mmdb && sudo systemctl restart hive-sensor

MaxMind's GeoLite2-City works too (free account required). The file is memory-mapped, so it barely counts toward the sensor's RAM. Always replace it with install as above and then restart; never edit it in place while the sensor runs. Without a database the sensor still works, just without map points.

sudo cp hive-sensor.service /etc/systemd/system/
sudo systemctl daemon-reload && sudo systemctl enable --now hive-sensor
journalctl -u hive-sensor -f

From your own machine, ssh root@<vm-ip> should show up in the journal right away, and on the map within about 2 minutes (one flush plus the 30s cache).

Configuration

Variable Default
HIVE_INGEST_URL required Worker ingest URL
HIVE_SECRET required Shared HMAC secret
HIVE_SSH_ADDR 0.0.0.0:22
HIVE_HTTP_ADDR 0.0.0.0:80
HIVE_STATE_DIR /var/lib/hive Holds the SSH host key and stats.json
HIVE_GEOIP_DB unset Path to GeoLite2-City .mmdb
HIVE_BLOCKLIST_MIN 3 Attempts in 7 days before an address goes on the blocklist
HIVE_IGNORE_IPS unset Comma-separated IPs or CIDRs that are never listed, e.g. your own address

Development

# Worker
cd worker && cp .dev.vars.example .dev.vars   # set HIVE_SECRET and HIVE_EXPORT_TOKEN
wrangler d1 execute hive-db --local --file=schema.sql && wrangler dev
HIVE_SECRET=... HIVE_EXPORT_TOKEN=... ../test/worker.sh

# Sensor, on unprivileged ports
cd sensor && cargo test
HIVE_INGEST_URL=http://localhost:8787/ingest HIVE_SECRET=... \
HIVE_SSH_ADDR=127.0.0.1:2222 HIVE_HTTP_ADDR=127.0.0.1:8080 HIVE_STATE_DIR=/tmp/hive cargo run

IP geolocation by DB-IP (CC BY 4.0), or GeoLite2 data created by MaxMind, available from https://www.maxmind.com.

About

Low-interaction SSH/HTTP honeypot in Rust with a live attack map on Cloudflare Workers

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages