Teaching a Honeypot to Tell Bots From People


Stand up an industrial simulation of ordinary production infrastructure, expose it to the internet, and you will have visitors within the hour. Almost all of them are machines. The interesting engineering problem is not collecting what they do — a honeypot does that for free — it is deciding which of those thousands of sessions a human being should actually look at.

This is a write-up of how I approached that in a defensive honeypot platform I built. It is about the shape of the classification and scoring layer, not about the capture itself, and it is deliberately light on mechanism — the specifics of a detection ruleset are also the specifics of how to evade it. I am describing the design; I am not publishing capture data or claiming numbers I have not measured.

A Honeypot Without an Opinion Is Just a Log File

The capture side is a solved problem. An industrial simulation presents a convincing but entirely fake service, accepts logins, records everything typed at it, and never executes a single instruction. Point it at the internet and events arrive.

And that is where the actual difficulty starts, because what you now own is an enormous, undifferentiated pile of connection records. Somewhere in it are three sessions that matter. Everything else is a scanner trying a handful of stock credentials and disconnecting.

A honeypot that only stores things has moved the problem rather than solved it. To be useful it has to arrive at a judgement: what happened here, how concerning is it, and was there a person on the other end?

So the system is built in layers. Raw events are persisted, stitched into sessions, then classified, scored, and — where a sequence looks deliberate — assembled into attack chains. Only the last layer is meant for human attention.

Step One: Normalise, Then Classify

Before anything can be compared, it has to be made comparable. Attacker input arrives with inconsistent whitespace, varied quoting, and casing that means nothing. Two commands that are functionally identical must collapse to the same normalised form, or every aggregate you build afterwards is fiction.

With that done, each command is sorted into one of six categories:

CategoryWhat it indicates
FingerprintingWorking out what this machine is
ReconnaissanceLooking around
PayloadFetching or running something from elsewhere
PersistenceArranging to still be here after a reboot
Anti-forensicsRemoving evidence that any of this happened
UnclassifiedMatched nothing — deliberately kept as its own bucket

I want to defend that last row. It is tempting to fold unmatched input into the lowest-risk category and move on. Keeping it distinct means you can go and look at what is landing in it, which is the only way the ruleset improves. A classifier that silently swallows what it does not understand will quietly stop being accurate and never tell you.

How input is matched to a category is the part I am leaving out. The approach is ordinary enough, but a published detection list is a published evasion list, and that trade is not worth making for a blog post.

Step Two: Score, and Put Anti-Forensics at the Top

Each category carries a weight, and here is the decision I find most interesting in the whole system:

RankCategory
HighestAnti-forensics
Payload
Persistence
Reconnaissance
Fingerprinting
LowestUnclassified

The exact weights are not the interesting part and I am not going to print them. The ordering is, because one line of it is counter-intuitive: covering your tracks ranks higher than dropping a payload. That looks wrong at first glance — surely executing something hostile is the worse event?

It is, in isolation. But the weights are not measuring damage, they are measuring how much I want a human to look at this. Automated malware drops payloads constantly; it is the single most common thing a commodity bot does after a successful login, and on its own it tells you very little beyond “a bot got in”. Clearing a shell history is different. It implies someone who expects the logs to be read, and who cares about not being seen. That is a far stronger signal of a person with intent than a payload fetch is.

Risk level then comes from combinations rather than from the raw total. A payload combined with persistence or anti-forensics is treated as the most serious case — that is not a single opportunistic action, it is a sequence with a plan behind it. Either of those alone sits a rung lower. A successful login followed by poking around sits lower still. Everything else lands at the bottom, which is where the overwhelming majority of traffic correctly belongs.

The point of the scoring is not precision. It is to make the pile sortable, so a person reviews twenty sessions instead of twenty thousand.

Step Three: Bot, Human, or Advanced — Time Is the Tell

Separately from risk, each session gets an actor classification. This is the part I expected to be hard and turned out to be almost embarrassingly simple.

The top tier is decided on technique — evidence of anti-forensics, signs that input has been deliberately obfuscated, or a payload paired with persistence. The common thread is someone who knows what they are doing and is actively trying not to look obvious. I am keeping the specific markers to myself for the same reason as the rest of the ruleset.

Human versus bot comes down to two numbers: how much was typed, and how long the session lasted. A handful of instructions spread over a meaningful interval reads as a person. One instruction and an instant disconnect is a script.

That is it. No behavioural model, no timing histogram, no machine learning.

It works because of an asymmetry that is very hard for an attacker’s tooling to fake cheaply: automation is fast because being fast is the entire point. A scanner sweeping a large address range cannot afford to pause between commands — its economics depend on volume. A person exploring an unfamiliar box reads output before deciding what to type next, and that thinking time shows up in the timestamps whether they want it to or not.

A crude heuristic aligned with a real economic constraint will often beat a sophisticated model aligned with nothing. Naturally, a patient adversary who deliberately paces their automation defeats this — but they have then given up throughput, which is a trade most opportunistic scanning is not willing to make.

Step Four: Chains, Because Order Carries Meaning

Individual classified actions still under-describe what happened. A lone fingerprinting instruction is nothing. The same instruction followed by a download, then by an attempt to survive a reboot, then by a cleanup of the traces, is a story with a beginning, a middle and an end.

So sequences that match known shapes get promoted into attack chains, with their own types — fingerprinting, payload execution, persistence, anti-forensics, brute force, web exploitation, scanning, IoT botnet behaviour. A chain is the unit a human should actually be reading, because it is the level at which intent becomes visible.

This is the same idea as the scoring layer, applied to time instead of severity: raise the altitude until what you are looking at is a narrative rather than a row.

The Honeypot Must Not Become the Incident

A system built to attract attackers is a strange thing to run, and most of the engineering effort ends up going into making sure it does not turn into the thing it was meant to detect.

Nothing executes. The fake SSH service presents pre-configured responses. An attacker’s command is recorded as text and answered with a plausible fake. There is no code path that runs it.

Outbound traffic from the simulation is dropped. If something manages to pull a payload down, that payload cannot phone home. This is the control I would reinstate first if I had to strip everything else.

Captured passwords are hashed, never stored in plaintext. Attackers reuse credentials from breach corpora, so what arrives at the login prompt is frequently a real person’s real password from some other compromise. It is genuinely sensitive data that happens to have been handed to you, and storing it in the clear would make the honeypot a breach of its own.

Runtime control uses an explicit allowlist. Starting, stopping and restarting services never passes user input to a shell — the set of permitted operations is fixed in code. Every control action lands in an audit log.

The application refuses to start without its admin secret configured. Not a warning, not a default — it will not boot. A misconfigured honeypot dashboard exposed to the internet is a worse outcome than no honeypot at all.

And the deployment guidance is blunt about the rest: isolated host, dedicated address, never a home or shared network, with the simulated service the only thing deliberately open to the world.

What I Took From It

The capture layer was the part I assumed would be the project, and it turned out to be closer to configuration. The real work was everything that turns a firehose into something a person can act on: normalise so things are comparable, classify so they are sortable, weight by investigative interest rather than by apparent severity, and raise the unit of analysis from command to session to chain.

The weighting decision is the one I would keep if I rebuilt this from scratch. Ranking anti-forensics above payloads only looks strange until you are clear that the score is not answering “how bad is this?” but “how likely is it that a person did this on purpose?” Those are different questions, and a detection system that confuses them will bury its most interesting sessions underneath thousands of bots doing the loud, obvious thing.


This is a defensive security project — it exists to observe and characterise unsolicited traffic against infrastructure I control. Honeypot deployment carries legal and operational obligations that vary by jurisdiction; anyone running one should understand theirs before opening a port.


Leave a Reply

Your email address will not be published. Required fields are marked *