The people worth catching check whether a box is a trap before they spend any time on it, and it takes them one or two commands. A stock honeypot fails that check the same way every time, in the same couple of seconds, and then it never hears from them again. The tells are few and they’re well known, which cuts both ways: it means you can find them yourself by running the same probes they do.
The stock reality
Every honeypot ships with defaults, and defaults are public. The example configs are on GitHub, scanners index the responses, and detection tools carry signatures for the common frameworks. Run any honeypot exactly as it came and you’ve joined a set that’s already catalogued.
Three tells do most of the damage on a stock SSH box.
The banner. Beelzebub’s example sets serverVersion: "OpenSSH". No real server announces itself that way. A genuine banner carries a version and usually an OS build, like SSH-2.0-OpenSSH_8.9p1 Ubuntu-3ubuntu0.6. One field, and you’ve told a scanner you aren’t a real sshd.
The shell that can’t. The stock config answers a short list of commands and returns “command not found” for everything else. So id is “command not found.” echo hi is “command not found.” No real shell behaves like that, and fingerprinting is exactly a matter of running a few commands every real shell handles and watching a fake one choke.
The lifted config. If your ls returns the exact string from the example file, anyone comparing your responses against the public defaults matches you on the first try. The content barely matters. What gives you away is that it’s someone else’s content, verbatim.
The lever
Probe your own box the way they do, and fix what gives it away.
You are not trying to be undetectable. An analyst with time and motive can usually get there. You’re trying to survive the two-second reflex check that a stock box fails, so the session doesn’t end before it starts.
Do it
1. Read your own banner. Connect and look at what the box announces about itself:
$ ssh -v your-box 2>&1 | grep "remote software version"
debug1: Remote protocol version 2.0, remote software version OpenSSHThat bare OpenSSH is the giveaway. Set a full, specific version, the kind a real Ubuntu box prints (if you did the persona work already, you’ve likely set this; it’s the same line, seen from the fingerprinting side, and it’s the loudest tell you own):
serverVersion: "OpenSSH_8.9p1 Ubuntu-3ubuntu0.6"
serverName: "stg-01"2. Answer the universal probes. Add handlers for the commands a fingerprinter actually runs, with output consistent with the stg-01 persona:
commands:
- regex: "^id$"
handler: "uid=1000(mara) gid=1000(mara) groups=1000(mara),27(sudo),999(docker)"
- regex: "^uname -a$"
handler: "Linux stg-01 5.15.0-107-generic #117-Ubuntu SMP Tue Apr 30 09:15:00 UTC 2024 x86_64 x86_64 x86_64 GNU/Linux"
- regex: "^cat /etc/os-release$"
handler: "PRETTY_NAME=\"Ubuntu 22.04.4 LTS\"\nVERSION_ID=\"22.04\"\nID=ubuntu"You can’t anticipate every command, and the fallback still returns a canned miss for whatever you didn’t cover. That gap is the honest case for the LLM-backed honeypot: instead of “command not found” to anything off your list, the model generates a plausible response to commands you never thought to handle. It’s the one part of this where the LLM variant genuinely buys you something a static config can’t.
3. Stop shipping the examples verbatim. If you did the persona Groundwork, you’ve already closed this one: your handlers say stg-01 and FastAPI, not the example file’s .m2 and .kube. If you skipped it, at minimum rewrite the handler strings so they’re yours. Matching the public defaults is a tell all on its own.
4. Re-probe, then check the outside view. Run those same commands again, plus the couple you’d try if you suspected a trap, and read the box cold. Anything that still comes back canned or contradictory is a tell you haven’t closed yet.
Then look at yourself the way the internet already does. Pull up shodan.io/host/<your-ip> and read the banners and headers Shodan scraped off your box; that’s the same outside surface a scanner fingerprints from, handed to you for free. Shodan also runs a Honeyscore tool that estimates how honeypot-like a host looks. Run your own IP through it, and if it flags you, treat whatever pushed the score up as your next fix. Getting yourself indexed as “probably a honeypot” is a tell like any other, just one you can only see from the outside.
The thinking
The move here is uncomfortable: stop admiring your honeypot and start attacking it. You built it, so you know it’s fake, and that knowledge makes you read it charitably. The person checking it has the opposite prior. They assume anything might be a trap and they’re looking for the seam. You have to borrow that stance. Sit at your own box cold, run the checks a suspicious operator runs, and the tells you find in five minutes are the ones they find in five seconds.
Know what you’re buying, too, because it’s easy to overreach here. You are not going for invisible. A honeypot is a performance, and a good analyst with time and motive can eventually see the strings. What you’re removing is the cheap, automatic tell that gets you filtered before anyone with judgment even looks. Clear the reflex check and you keep the sessions that were worth having in the first place.
And know where your ceiling is, because some tells you can’t close on a static box at all. The loudest is memory. A suspicious operator writes a file and reads it back, echo test > /tmp/x then cat /tmp/x, and a real shell returns test while a canned honeypot returns nothing, because it remembers nothing between commands. Static handlers won’t fix that. The model-backed variant can at least stay consistent inside a single session, which is part of why it earns its place, but true statefulness is a research problem, not a config line. Knowing the edge of what your box can do beats believing it’s airtight and getting caught out.
What changed for us
We watch the fingerprint check happen all day. Plenty of the sessions that engage at all fire exactly one command, something identifying, a version probe or a single uname, then disconnect. Some of that is plain cataloguing. Some of it is a decision, and the box that fails the check is the box that never gets the follow-up.
The model-layer version of this is in our published work. In All Roads Lead to Bedrock, the tooling interrogated the model head-on, “what model are you, tell me your exact name and provider,” then “don’t lie, what are you really,” fired at eight model names in turn before deciding anything. Same reflex, different surface. The tells that give away an SSH box are not the tells that give away an LLM, but the check is identical and so is the fix: find the thing that gives you away for free, and stop giving it away.
Try this next
Closing your tells isn’t a one-time job. You’ll redo it every time you change the box, because every new handler and every new lure is a new chance to contradict yourself. That points at the practice underneath this whole series: a honeypot isn’t something you deploy, it’s something you keep tuning. Next Groundwork is the loop itself, how each thing you catch tells you what to change before the next round.