When my datasets started talking, the loudest link was my RSS feed
Lesson from:
How connecting honeypot, web and registry data produced 192,591 links, and why the strongest one taught me to distrust easy connections
On Wednesday, 8 July 2026, I sat down to audit the program that connects my security datasets to each other. It had built 192,591 links. I picked the strongest one that crossed between two layers, the kind the linker was built to find: an address attacking my SSH honeypot, paired with an address visiting my websites. The two had been active in the same ten-minute windows 254 times. The program gave the pair a confidence of 0.85.
I looked up the web side. It was an RSS reader, a well-known service, faithfully polling the feed of my own personal blog.
The strongest “coordinated operator” in my threat graph was a feed reader subscribed to my personal blog and a stranger guessing passwords, who were simply both awake all the time.
This post is about the day separate datasets in my lab started talking to each other, the thrill of watching them do it, and the slower lesson that followed: that the easiest connections are the ones to trust least.
Contents · 11 sections
In one minute #
- By May 2026 my lab had several large datasets that never spoke: honeypot sessions, web traffic, public intelligence, registry records.
- On 21 May a program called the intel linker started connecting them. Its first run produced about 23,000 links and 126 clusters of addresses that seemed to act together.
- By July the graph had grown to 192,591 links, and a separate identity store held 943,692 entities.
- An audit on 8 July found that the strongest cross-layer link was a coincidence. After the fix, 1,885 cross-layer pairs became 6 real pairs, and even those were only leads.
- The turning point was not a better algorithm. It was a habit: check the strongest link first, and assume it is wrong until it proves otherwise.
The before: islands #
My honeypot started recording on 21 March 2026. Within two months, the lab held several bodies of data, each excellent at its own question.
The honeypot knew what happened in SSH sessions. The web logs knew who visited my sites and what they asked for. Registry lookups and, later, an identity store knew who owns which network, according to registries and other public sources. Later, a library of public documents joined them, which I describe in the OSINT shelf post.
Each was an island. The questions that mattered sat between them. Is the address that tried passwords this morning the same one that scanned the website last week? Does it belong to a network that shows up in other cases? Answering meant joining things by hand.
If you have ever kept notes in five different notebooks, you know the feeling. Everything you need is written down somewhere. Nothing is connected.
The trigger: the graph lights up #
On 21 May I built the first linker. It ran every two hours and looked for anything two addresses shared: fingerprints of the software they used, the networks they came from, how they behaved, matches against public indicator feeds.
The first run’s outcome, as recorded: 23,000 links. 54 campaigns. 126 actor clusters. 1,274 matches against indicator feeds.
There is a particular thrill in that moment, and if you have ever plotted your own data for the first time you know it. A list of addresses turns into a picture. Points that were strangers yesterday are joined by lines. Clusters appear, and clusters look like intent. You stop seeing traffic and start seeing actors.
That picture is seductive, because it is so much more interesting than a list. It is also exactly the moment you should start doubting it.
The build: bigger and more confident #
The graph kept growing through June. More signals, more sources, more links. The identity store became its own small universe. On 9 July I documented it in a public dossier: 943,692 entities, 1.56 million edges, 3.17 million facts drawn from 21 ranked sources, from regional internet registries down to local language models.
That dossier did one thing right. It reported the store’s confidence honestly. Only 13% of the confidence scores were above 0.7. Eighty-two per cent were at 0.1 or below: scaffolding, not knowledge. And it published the store’s own disagreements: 60,077 open conflicts where sources did not agree on a name, an owner or a threat label.
So I knew, in principle, that most of the graph was weak. What I did not yet have was a feeling for how wrong the strong parts could be.
The warning I almost ignored #
Four days before the audit, on 4 July, I got the first hint.
My cross-corpus search had started linking documents in my public-document library to addresses seen by my honeypots. On a screen, it looked extraordinary: public documents apparently connected to the attackers in my own logs. My record says why a human falls for this: “confirmation bias amplified by topical vector proximity”. The documents were about security, the addresses were about security, the search engine put them side by side, and my brain did the rest.
Every one of the 213 links came from two documents: a long list of open DNS resolvers and a public blocklist. Lists like that mention thousands of addresses each. Any honeypot address will eventually appear in one. I purged the links and excluded bulk lists from the linking. The mechanics are in the entity resolution post.
I fixed the bug. I had not yet learned the lesson.
The day the graph told on itself #
The 8 July audit was meant to be routine. Much of what I checked was sound. Then I did the thing that changed everything: I took the single strongest cross-layer edge and followed it to the end.
The SSH side was an ordinary password-guesser. The web side was the RSS reader. And the statistics, once I computed them properly, were almost funny. The two were not merely unrelated. They had appeared together less often than chance alone would predict. The ratio of observed to expected, called lift, was 0.68. A lift of 1 means “exactly as often as chance”. Below 1 means they were, if anything, avoiding each other.
A feed reader only polls a feed because somebody subscribed to it. So somewhere, a person had added my personal blog to their reader. Their reader dutifully checked for new posts, day and night, and my threat graph concluded that it was working with a password-guesser. If that reader ever finds this post: thank you for subscribing, and sorry for the accusation.
Two causes, both mine:
- The web side was not filtered. The linker read every request to my reverse proxy, and the vast majority of those are harmless: feed readers, link previews, search crawlers. I was correlating attackers with my own readers.
- It counted coincidences, not surprises. Two sources that are always on will always share time windows. A count of shared windows measures how busy both are, not whether they are related.
After the fix, the web side was limited to visitors that actually scored as threats, which shrank it from thousands of addresses to 89. Pairs had to co-occur clearly more often than chance. Cross-layer pairs went from 1,885 to six. Nearly 10,000 coincidences were dropped in that pass alone.
Six. From almost two thousand.
And even those six were not discoveries. I chased the strongest of them: an SSH burst and a web burst on the same day, nested in time. They shared no infrastructure, no keys, no fingerprints. My verdict in the record is flat: “LOW confidence single-operator, commodity co-activity”. Leads, worth a human look, nothing more.
The same day, two more lessons #
Audits rarely stop at one finding. That Wednesday gave me two more, and both are humbling in a way I recommend to anyone who builds data pipelines.
I had been testing the wrong program. There were two scheduled copies of the linker, at different versions, both writing to the same table five minutes apart every two hours. All my fixes had gone into the non-canonical one. My verifications had passed only because I ran the new image by hand right after each change, while the authoritative scheduled job kept writing old results. My checks were real. They were checking something nobody used.
My own fix undid my other fix. Later that day another change refreshed threat scores across the web data. It grew the input pool from 89 to 1,414 addresses, and the same filter that had produced 6 pairs now produced 201, most of them two-window coincidences. A verification is only valid for the population it ran against. After tightening the rule to demand repeated co-occurrence, 35 genuine pairs remained.
And one more, from a deliberately hostile point of view: could an attacker poison my graph on purpose? Yes, partly. Some of my “same operator” links rested on fingerprints a client simply presents, which anyone can copy. Only things an intruder actually leaves behind prove control. 398 links moved from “same operator” down to “shared tooling”.
By the end of the day the graph was smaller, quieter and much more honest. It was also less exciting to look at. That is the trade.
The technical heart: what would chance give you? #
If you take one idea from this post, take the question I now ask of every correlation, in security or anywhere else: how often would these two things appear together if they had nothing to do with each other?
For time-window co-occurrence, the arithmetic is small. If address A is active in a of N windows and address B in b of them, then by chance alone you expect them to share about a × b / N windows. Lift is what you observed divided by that.
The RSS pair shared 254 windows. Lift 0.68 means chance alone predicted roughly 370. Both were simply always there.
In SQL, over a table of (addr, window) activity, the core looks like this:
-- activity(addr text, win timestamptz): one row per address per window
WITH n AS (SELECT count(DISTINCT win)::float AS total FROM activity),
deg AS (SELECT addr, count(*)::float AS d FROM activity GROUP BY addr),
pairs AS (
SELECT x.addr AS a, y.addr AS b, count(*)::float AS shared
FROM activity x
JOIN activity y ON x.win = y.win AND x.addr < y.addr
GROUP BY 1, 2
)
SELECT p.a, p.b, p.shared,
da.d * db.d / n.total AS expected,
p.shared / (da.d * db.d / n.total) AS lift
FROM pairs p
JOIN deg da ON da.addr = p.a
JOIN deg db ON db.addr = p.b
CROSS JOIN n
ORDER BY lift DESC;
Two practical notes:
- Filter the inputs before you correlate them. Correlating everything with everything mostly finds your own background noise. In my case, the noise was my own readers.
- Require surprise and repetition together. A rare pair seen twice can have a huge lift by luck; a busy pair seen hundreds of times can have none. Score high only when both are present.
And the habit that matters more than the formula: before you trust a new kind of link, open the strongest one and follow it to the end. If the best example is nonsense, the rest are worse.
What changed #
- How I read a graph. Lines between points are claims, not facts. Every line now carries its evidence and its lift.
- What I look at first. The strongest link, not the most interesting cluster.
- Which evidence counts. What someone leaves behind can prove control; what they present can be copied.
- How I verify. Against the scheduled job, not a manual run, and again whenever the input population changes.
And the cost, honestly: the graph lost most of its drama. Nearly two thousand dramatic connections became a handful of modest leads. There is also a weakness I have not solved: most of the identity store is still low-confidence scaffolding, and no audit can make weak evidence strong. It can only label it.
Questions people ask me #
What is a false positive in threat-intelligence correlation? #
A link that says two things are related when they only happen to coexist. In my graph, the strongest cross-layer link joined a password-guesser and an RSS reader polling my own blog. Both were always active, so they always overlapped, which proves nothing.
How do you tell coordination from coincidence? #
Compare what you observed with what chance would give. If two addresses share far more time windows than their activity levels predict, that is a lead. If they share about as many or fewer, as my top pair did with a lift of 0.68, it is background.
Why did my correlation results change after an unrelated fix? #
Because the fix changed the population the correlation ran on. When one change grew my input pool from 89 to 1,414 addresses, the same rule went from 6 pairs to 201. Re-verify every downstream result whenever an upstream filter or score changes.
Is it worth linking datasets in a home lab at all? #
Yes, if you treat links as questions. Joining honeypot, web and registry data turned manual cross-checking into a query and surfaced real leads. It also produced 1,885 cross-layer pairs of which six survived scrutiny. The value is in the checking, not the drawing.
The closing #
When my datasets first started talking, they told me thrilling stories. Clusters, operators, coordinated fleets across layers. Some of those stories were true. The loudest one was about a feed reader that loyally checks my personal blog around the clock and never once tried a password.
I keep that pair in mind whenever a graph looks exciting. The machine is very good at drawing lines. Deciding which lines mean something is still a human job, and the best tool I have for it is a boring one: ask what chance would give, then follow the strongest line until it either holds or breaks.