Two days, 2026-08-23 into 2026-08-24, of log-analysis practice worked directly against the honeypot's real attacker traffic instead of practice logs. The arc I had laid out for myself went from basic filtering up to multi-indicator correlation. The commands are the cheap part. What I want back in six months is that three separate times in this file, the thing that looked like the answer wasn't one, and each time the fix was the same: go and look at what the number is actually counting.
Filter, extract, rank — then verify the rank
Almost every log question collapses into three moves chained with the pipe: filter, extract, count or sort. grep matches lines, grep -o keeps only the matched part, grep -v inverts, and sort | uniq -c | sort -rn answers anything shaped like "what is the most common X". That last idiom carries most of the weight.
The caution came out of real data, not theory. The top entry in a "most active session" ranking was my own personal test session, not an attacker. The metric was correct and completely misleading. The rule I adopted: verify the source address behind a ranking before trusting it.
The complement to ranking is reconstruction. Every event in the honeypot's JSON event log carries a session field — a short hex id unique to one connection. Filtering on one id turns a flat pile of lines into a single connection's story in chronological order, with no sorting needed, because the log is already written in event order. Ranking is the zoom-out over everyone; the session filter is the zoom-in on one thread, read to the end.
A fixed offset is a bug waiting for the text to change
My first "activity by hour" command cut a fixed character range (cut -c14-15) out of grep -o output. That was wrong because grep -o prints the matched field name along with the value, so the offset landed inside the year instead of the hour. The fix was to stop counting characters and match the shape of the timestamp instead — a two-digit hour anchored on the literal T. Generalized: match shape, not position. Any assumption about a fixed offset breaks the moment the surrounding text changes length, and it will.
Command vocabulary is not identity
I tried separating humans from bots by the commands they ran, treating things like cat, nano and ls -la as human markers. Bots walked straight into the "likely human" bucket. Botnet scripts run cat /proc/cpuinfo and cd /tmp too; those words mean nothing on their own.
What actually held up is intent-shaped behaviour:
- Persistence attempts — planting
authorized_keys,chattr-style locking. Nobody poking around legitimately does that, so it is an unambiguous attacker signal. - Payload downloads —
wgetorcurlpulling and executing a binary. - Credential harvesting at scale, versus one or two attempts and then giving up.
- Exfiltration-shaped reads versus curiosity-shaped reads. Reading a honeytoken and moving on reads differently from reading it and piping it out, or referring back to its contents in later commands.
- Leaving things working (
apt update) versus trying to break or take them.
One live case where this mattered: two sessions that looked exploratory rather than malicious appeared, tentatively, to trace back to a public discussion thread that had disclosed the honeypot's address — likely curious readers trying a password that had been posted, rather than attackers demonstrating tradecraft. I could not confirm that, and I did not use them as a control group until they had passed the persistence and payload checklist. Tentative stayed tentative; the checklist decided.
Two independent indicators, or it is a hunch
The reusable pattern for proving two indicators point at one campaign is comm -12, the intersection of two sorted lists. Build two sorted, de-duplicated lists of source addresses — one per indicator — and intersect them.
Applied to a real finding: several SSH sessions from different addresses each issued a direct TCP connection request to the same external host on port 25 immediately after login. Port 25 is SMTP, so that is a spam-relay pivot. Two independent things tied those sessions together: the shared relay target, and a shared SSH client fingerprint, meaning the same client tooling regardless of source address. The intersection returned a four-address cluster sharing both signals. That is one coordinated campaign rather than four coincidences, and it stayed active across multiple days, with new addresses joining under the same fingerprint and the same target.
One indicator is a coincidence with a story attached. Two provably-shared, independent indicators is a finding.
The detail that beat the volume
A new heavy-hitting source showed up with 810 hits overnight. The behaviour was dull: exactly one command per session, always the same fixed-flag uname, pure OS fingerprinting, then nothing further. The find was not there.
It was in the usernames. Attempts included sol, solana, eth, ethdocker, blockchain, and firedancer. That last one is the tell — a real, fairly obscure Solana validator client name that a generic scanner would not guess by coincidence. This was a deliberately built target list aimed at cryptocurrency node operators, not random dictionary spraying.
Volume says something is happening. A specific, hard-to-fake detail says what it is. Given both, trust the detail.
The messy parts worth keeping
awk is the step up from line-oriented grep once the data is column-shaped: -F for a custom separator, $N for one specific field, $NF for whichever field is last when the field count varies.
Ranking attacker countries through whois lookups exposed some genuine real-world messiness: ARIN, RIPE and APNIC label and capitalise the same fields differently. The chain that fixed it was while read over the lookups, grep -im1 to take only the first match and avoid double-counting a record, awk '{print $NF}' to pull the value without depending on the label, and a tr pass to normalise case. Every piece is in there because of a specific mess it fixed, not out of habit — and the country ranking's own result never made it into the file, which is the shape of note I keep producing: the setup, not the answer.
Both IoT botnet lineages turned up telnet-side, confirmed by signature commands, so they belong in the record. Mirai shows up as a chain of shell keywords ending in a busybox call with a random applet name, which is its device-compatibility fingerprint check. The Gafgyt/Bashlite family shows up as a hex-encoded echo carrying a hardcoded marker string — a crude check some botnets use to see whether a device is already infected, occasionally to avoid or evict a rival on the same device. Two distinct lineages, despite the similar IoT targeting.
What to keep
The commands are cheap; the discipline is not. Filter, extract, rank, then check what the ranking actually counted. Match shape, not position. Verify the address behind a "most active" anything. Two independent shared indicators before calling it a cluster. Intent-shaped behaviour, not vocabulary. And one hard-to-fake detail outweighing a large volume of noise.
I ended the file calling it my strongest piece of work so far — real correlation, a finding I can defend, a method I can reuse. That is my own read of my own session, and it stays my read until something tests it.