The honeypot’s been up a week. The log has thousands of lines in it. You haven’t opened it. That’s how most honeypot projects actually die. Not on the deploy, on the follow-through. Catching traffic is easy; the internet does that part for you. Reading it is the work, and most of what you have to read is junk.
The stock reality
Fire-and-forget is the standard way this fails, and an easy trap to fall into. Put a honeypot on the open internet and it’s logging within minutes: SSH brute-forcers, HTTP scanners, the steady background hiss of the whole internet finding your open port. The file grows into a wall of JSON that doesn’t invite a second look, so it doesn’t get one.
Beelzebub writes one JSON object per line: every connection, credential, and command as a structured event. Here’s a single line, reformatted so you can read it:
{
"event": {
"DateTime": "2026-07-03T09:14:22Z",
"RemoteAddr": "203.0.113.44:51022",
"Protocol": "SSH",
"User": "root",
"Password": "123456",
"Command": "uname -a",
"Description": "SSH interactive"
},
"status": "Interaction",
"msg": "New Event",
"time": "2026-07-03T09:14:22Z"
}Everything lives under event, with PascalCase field names like Command, RemoteAddr, and Password. That’s the whole schema you need to start cutting.
The lever
Open the log, throw out everything that did nothing, and read what’s left.
jq is the tool: the command-line JSON processor, free, and probably already on your box. It turns the wall of events into answers. The trick isn’t reading every line; it’s not reading the ~70% that never ran a command, so your attention is still there for the sessions that did.
Do it
Point these at your log, the logsPath you set in beelzebub.yaml. (Running in Docker? docker logs <container> gives you the same JSON stream; pipe it to the same filters.)
1. Get the volume, then the noise floor. How many events total, and how many connections actually ran a command:
# total events
wc -l beelzebub.log
# distinct connections seen
jq -r '.event.RemoteAddr' beelzebub.log | sort -u | wc -l
# connections that ran at least one command
jq -r 'select(.event.Command != "") | .event.RemoteAddr' \
beelzebub.log | sort -u | wc -lThe gap between those last two numbers is your noise floor: connections that touched the box and left without doing a thing. On a live sensor that’s most of them. Everything worth your time is in that third number.
2. See what the engaged sessions actually did. The most-run commands tell you what people show up looking for:
jq -r 'select(.event.Command != "") | .event.Command' beelzebub.log \
| sort | uniq -c | sort -rn | head -20Expect the top of that list to be recon boilerplate: uname -a, cat /proc/cpuinfo, w, the same scripted sweep everyone runs. That repetition is the noise floor showing up as commands. You’re hunting for the line that isn’t on it.
3. Check your bait. If you planted a canary last Groundwork, ask straight out who reached for it:
jq -r 'select(.event.Command | test("\\.env"))
| [.event.RemoteAddr, .event.Command] | @tsv' beelzebub.log4. Pull one session and read it end to end. Once a source looks worth it, because it ran something off-script or went for the bait, reconstruct everything it did, in order:
ip="203.0.113.44"
jq -r --arg ip "$ip" \
'select(.event.RemoteAddr | startswith($ip))
| [.event.DateTime, .event.Command] | @tsv' \
beelzebub.log | sortThat last one is the whole point. One visitor, every command, on a single clock. Read top to bottom, it tells you which surfaces they tested, whether they found the bait, and where they lost interest. The scanner that ran uname and bailed reads nothing like the operator who opened your .env, went quiet for a beat, and came back with a pointed follow-up. This is where a honeypot stops being a counter and turns into something you can actually read.
The thinking
Dashboards train a bad instinct: that volume is the score. Events per day, attacks blocked, line goes up. On a honeypot that’s backwards. A box that logs a million scanner hits and one real session produced exactly one useful thing, and the million just buried it.
So the job is triage, and triage is mostly deletion. You’re not trying to understand every session. You’re trying to throw out the ones that don’t matter fast enough that you’ve still got attention left for the one that does. The automated sweep, the same five commands fired at every box on the internet, is the overburden. Clear it away, and what’s left is your data.
Two habits do most of the work. First, keep intent and compromise separate in your head. Everything in the log is something someone attempted, and none of it is proof anything worked, because the box is a decoy with nothing real to break. That’s why the honest verbs are “attempted,” “ran,” “reached for,” and never “breached” or “captured.” Second, chase yield, not volume. Don’t ask how much you caught; ask what you caught that you couldn’t have predicted. The predictable sweep is noise by definition, and the surprise is the entire reason the trap is out there.
So what actually marks a session as worth the read? Reaction is the most reliable tell. A scanner runs its list no matter what your box says; it will cat /etc/passwd on a box that already told it the directory is empty, because it isn’t listening. The sessions worth your time respond to your box: they read an answer, change course on it, chase the specific thing you left dangling, slow down where a script wouldn’t. Cadence says the same thing from another angle. Fixed sub-second gaps between commands are a program; irregular timing, a pause in the wrong place, a typo and a correction, that’s a person, or something built to move like one. You’re not really filtering for rare commands. You’re filtering for a visitor who’s paying attention to the trap.
This travels to any honeypot you’ll ever run, whether that’s Beelzebub JSON, Cowrie’s logs, or T-Pot in Elasticsearch. Same opening question every time: which sessions did more than the automated baseline? Find the floor, get under it, read the rest by hand.
What changed for us
The shape is stable. Across sensors, across months, it barely moves. Just under 70% of sessions are zero-command: connect and gone. Of the ones that do interact, the command sequences collapse into a handful of near-identical recon scripts, that same uname/cat /proc/cpuinfo/w sweep in a slightly different order. When we categorize the interacting set, the genuinely novel sessions, the ones doing something we haven’t seen before, come in well under 1%, on the order of one in a couple hundred. That handful is what all the triage is for.
Put that distribution next to the plan of “read every session in order” and watch it fall apart: you’d spend almost all your time in the 70% that did nothing and the long tail of near-duplicates, and quit before you ever reached the sub-percent that justified the whole exercise. The skill that matters is discarding with confidence, so that when something genuinely odd shows up, it’s the only thing still standing in front of you.
You don’t need our pipeline for any of this. jq, a couple of filters, and the willingness to actually open the file, which, going by how most deployments go, is the step people skip.
Try this next
Once you’re reading sessions by hand, you start noticing something that stings a little: some visitors run one command, get an answer that tells them exactly what you are, and leave. They made the trap. The next Groundwork is about reading your own box the way they do, spotting the tells that get a honeypot fingerprinted and skipped, so the sessions you worked to catch don’t die on the first uname.