How Armour Actually Works — and Why Ours Is Shaping Up to Be One of the Best. Somebody Attacked Us While I Was Writing This. Here Is the Whole Receipt.
- Patrick Duggan
- 1 day ago
- 12 min read
Stop letting fear stop you from being secure.
That is the whole argument, and the reason it needs making is that the security industry has a commercial interest in you believing the opposite — that defence is a thing you buy at enterprise scale, that it costs hundreds of thousands of dollars, and that attempting it with less is worse than not trying. None of that is true. Armour built right is cheap, and the expensive part was never the armour.
While I was writing this paragraph, somebody attacked us. So rather than argue the point, here is the complete receipt: what they did, what caught it, what it cost, what MITRE calls it, and — because a receipt with only the good parts is marketing — the three places our own instruments were wrong tonight.
What armour actually is
Not a product. Layers, each cheap, each covering a different failure of the one beside it. The Roman version and the medieval version and ours all work the same way: overlapping plates, none individually sufficient, arranged so that getting through one puts you in front of the next.
The four we run, in order of cost:
The edge. A Cloudflare Worker sitting in front of everything, enforcing a blocklist pulled from our own feed. It never touches our origin, so a blocked request costs us nothing at all. This is the cheapest layer and it does the most work.
The deception layer. Thirty canary paths on that same worker — fake .env files, a fake phpMyAdmin, convincing fake responses. They cost nothing to run because they are static strings. Nobody legitimate ever requests them, which makes a hit a zero-false-positive signal, and that property is worth more than any threat feed.
The behavioural detector. Watches request shape rather than reputation — burst geometry, path fan-out, non-browser user agents. This is the layer that catches things nobody has ever seen before, because it does not need to have seen them before.
The corpus. 1.7M indicators, so anything caught anywhere feeds everything else, and every catch is published free so somebody with no budget gets the same protection we do.
Tonight, live: what came at us
01:55:59 UTC. A single host in Warsaw starts asking for files that do not exist.
`[194.180.49.37](https://analytics.dugganusa.com/stix/register?ref=ioc-click&q=194.180.49.37)`, on AS201814 — MEVSPACE, a cheap Polish VPS shop. User agent: `Go-http-client/1.1` — the Go standard library default, on all 200 records I sampled. No browser impersonation. No rotation. No attempt whatsoever to look like a person.
The paths are the interesting part, because they are not a list — they are a generated permutation dictionary:
/.env1 · /.env2 · /.env~ · /.env.dev · /.env.prod · /.env.production · /.env.staging · /.env.local · /.env.txt · /.env.back · /.env.original · /.env_orig · /.env_copy · /.env_copy.txt · /.env.save · /.env.save.1 · /.env.save.2 · /.env.save.txt · /.env.save.dev · /.env.save.prod · /.env.save.stage · /.env.save.backup · /.env.save.production · /.env.save.development
Read the structure: .env × {suffix} × {environment} × {extension}, combinatorially expanded. Somebody wrote a generator, not a wordlist. It ran in irregular clumps — 26 requests in one second, then one, then seven, then fourteen — for about seven minutes, and it was still going when I pulled the data.
What they wanted: the .env file. In a badly-deployed web application that is a single flat file containing database passwords, API keys, cloud credentials and signing secrets. One successful GET is game over. That is why this is the single most common unsolicited request on the internet, and why the canary is pointed at it.
What MITRE calls it
Mapping matters because it turns "some Polish box is being annoying" into something you can reason about and compare across incidents.
Tactic: Reconnaissance (TA0043). Everything here is pre-compromise. Nothing was breached, nothing was exploited, no vulnerability was involved — this is the survey that precedes an attack.
Technique: T1595 Active Scanning, specifically T1595.003 — Wordlist Scanning. That is the textbook definition of what a generated .env dictionary is, and it is a precise fit rather than an approximate one.
Supporting: T1592.002 Gather Victim Host Information: Software — the probe reveals framework and deployment style by what responds.
Objective, had it landed: T1552.001 Unsecured Credentials — Credentials In Files. That is the prize. Everything else is the means.
Infrastructure: T1583.003 Acquire Infrastructure: Virtual Private Server. A rented Warsaw box, disposable, no attribution value.
For the deception side, MITRE Engage is the right framework rather than ATT&CK — decoy content and lures, deliberately reachable, instrumented.
What our armour actually did
The canary fired. 729 honeypot records from Poland, each one carrying the path, ASN, user agent, method and city. The scanner received convincing fake responses and kept going, which is the entire point — a honeypot is there to get got.
The autoblock listed it. Every catch writes to our iocs index at confidence 95. Anything at 80 or above flows automatically to /stix-feed/ips.csv, which is what the edge worker enforces. I verified it tonight: 2,236 rows on the live blocklist, and `[194.180.49.37](https://analytics.dugganusa.com/stix/register?ref=ioc-click&q=194.180.49.37)` is on it. No human decided that.
The feed published it. That listing is in the free feed. Anybody pulling it — no account needed for the basics — got this address blocked on their own edge because it annoyed us first. That is the part I am proudest of and it is the cheapest part of the whole system.
Detection to listing: automatic. Human involvement: zero. I found out about it because somebody asked me a question, not because anything needed me.
The other plate, on a completely different attack: BOATYMCFUCKFACE
Tonight's probe was caught by the deception layer. Here is the layer beside it doing something the canary could never do — and we have the live data for that one too, because we grabbed it while it was happening.
21 August, 22:45 UTC. Four machines in Frankfurt — Google Cloud, europe-west3 — began walking our entire archive with headless Chrome. 462,545 requests in seventy-five minutes. A hundred and seventy-four times normal. They fetched 1,579 of roughly 1,641 posts — 96.2% of the corpus. Median post fetched fifteen times, and not one single post in the whole archive was fetched exactly once, which is how you know a retry loop was riding on top of a sitemap walk.
Nothing about that cluster was on any blocklist. The ASN is Google's. The user agents were real Chrome. Reputation was useless — every reputational signal said these were fine.
At 23:13:09 — twenty-eight minutes after onset, with no human in the loop — the behavioural detector identified all four addresses and listed every one of them in the same second, at confidence 90, tagged scraper. Not a signature. Not a feed lookup. It caught them on shape: the burst geometry of a single network doing something no visitor does. Confidence at or above 80 flowed to the published blocklist, so within the hour those four addresses were in the free feed anyone on earth can pull.
In MITRE terms that is T1594 — Search Victim-Owned Websites, still under Reconnaissance (TA0043), with the same T1583.003 rented-VPS infrastructure pattern. And the honest finding, published at the time: nothing was attacked. No admin path probed, no credential surface touched, no attempt at the API. It was a badly-engineered bulk corpus harvester with zero backoff through a 47% error rate. They wanted the words. We named it BOATYMCFUCKFACE deliberately and kept the name after it turned out to be real, because a frightening name gets you frightened analysis.
Now put the two events side by side, because that is the actual argument of this post.
Warsaw probe | BOATYMCFUCKFACE | |
Wanted | credentials | content |
MITRE | T1595.003 wordlist scanning | T1594 search victim-owned websites |
Volume | 729 records, ~7 min | 462,545 requests, 75 min |
User agent | Go-http-client/1.1, no evasion | real Chrome, Google ASN |
On any blocklist? | no | no |
Caught by | deception layer (canary) | behavioural detector (shape) |
Human involvement | zero | zero, to detection |
Two completely different attacks. Two different plates. Neither would have caught the other one. The canary is blind to a sitemap walk, because a harvester never requests /.env. The shape detector was blind to tonight's probe, for the reason in the next section. That is what overlapping armour means, and it is why you build layers instead of buying a product.
What it cost
Our entire infrastructure runs around $500–600 a month, and that number covers a 17.9-million-document corpus, the search API, the feed, the blog and the edge — not just the armour. The marginal cost of blocking tonight's attacker was zero, because the block happens at Cloudflare's edge on a worker that was already running.
Compare the conventional path. We priced an earlier incident honestly and it came out at roughly 55–60 person-hours across seven or eight roles — tier one triage, tier two log pull, IR lead, threat intel, network engineer, SRE, detection engineering, and a manager to write it up. Call it a person-week and a half, four or five thousand dollars, and several days of calendar time while the ticket queues between people who each need the context re-explained.
And that comparison comes with an asterisk I will not drop: three hours is the marginal cost of responding, not the cost of being able to respond. The detector and the pipeline were months of prior work. Tonight spent that capital. It did not create it.
But months of one person's work is not hundreds of thousands of dollars, and it is not out of reach. That is the actual claim.
"Are they finding us, or getting stronger, or did we break something?"
That was the question my partner asked when I showed him tonight's probe, and it is the right one — all these layers have been running for months, so why does this feel new? Three hypotheses: the bad guys are finding us, the bad guys are getting stronger, or somebody changed something in our own stack and made us louder.
It is testable, so we tested it instead of arguing about it. The honeypot has been running since 5 April 2026 — 142 days. It holds 82,601 records.
Lifetime average: 582 records per day.
Yesterday, a full day, the day that produced tonight's "attack": 639.
That is 1.10× the lifetime average. A ten percent wobble. It is noise.
And it gets better. Look at the third day the honeypot was ever alive — 7 April 2026: 835 records from three addresses, 830 of them French. Day three of this instrument's life was a bigger day than tonight, from twenty-four times fewer hosts. If anything, per-host intensity has gone down.
So, against the three hypotheses:
Are the bad guys finding us? No evidence. A flat arrival rate is what "nobody is targeting you specifically" looks like.
Are they getting stronger? No. April was heavier.
Did somebody change our stack and make us louder? That would produce a step function on a deploy date. There is no step. There is a flat line with normal daily variance.
The correct answer is the fourth one, which is the whole point of this post: nothing changed. We started looking.
Roughly 582 attacks a day have been arriving since April — credential probes, scanners, harvesters, the constant background radiation of being reachable on the internet — and the armour has been quietly eating them the entire time. Tonight felt like an event because somebody happened to glance at the instrument, not because the internet did anything unusual.
We have written this exact lesson before, about ourselves, in harder terms: our archive was failing to serve its own cold pages about half the time for at least a week, and it took a stranger's rude scraper to file the bug report. Nobody was watching the cold path, so the cold path was free to be broken. Tonight is the pleasant inverse — somebody was watching, so 82,601 hostile requests are a line on a chart instead of an incident.
And that is the honest sales pitch for cheap armour. Not that it will save you from a targeted nation-state operation. That it will silently absorb the five hundred and eighty-two things a day that arrive at everyone, forever, whether or not anyone is looking — which is the overwhelming majority of what will ever come at a small organisation.
Three things our instruments got wrong tonight
A receipt with only the wins is an advertisement. All three of these are mine, all three happened in the last two hours, and all three are the same failure.
One. I read a false zero off an error page. I checked whether the attacker was on the blocklist by curling the feed without a key, got a 401 stub, grepped four lines of error text, found no match, and reported that it was not blocked. It was blocked the whole time.
Two. I invented a rotation pool that does not exist. Searching for 194.180.49 returned two more addresses, .171 and .174, and I called them sibling hosts confirming a known pattern. They are from February, from a different detector, at a different confidence, with no paths. They matched a string, not a campaign. There is no rotation here — it is one host.
Three. I inflated the volume. A fuzzy search returned an estimate of 9,782 records for the address. The correct figure, filtered properly, is 729 for all of Poland. An order of magnitude, in the direction that made the story better.
Every one of those is the same mistake: treating a query result as evidence without checking what the query actually matched. We have written that essay repeatedly, including about our own morning brief reporting zero gaps when there were four. Knowing the failure mode does not exempt you from it. Publishing it is the only thing that helps.
The gap this exposed — and we can prove it from our own decision log
Here is a finding worth more than the attack, and we did not have to speculate about it, because we record every autoblocking decision.
oz_decisions is our decision log: 8,672,136 records, and it is live — it was writing at 02:00:46 UTC tonight, mid-probe, publishing a batch of 29 indicators at BDE score 88. Each record carries the reasoning, not just the verdict: a BDE score, a tier, and three separate dimensions — novelty, confidence and significance — plus the action taken and a human-readable reason.
So we asked it the obvious question: what did the Behavioural Detection Engine decide about tonight's Warsaw host?
Nothing. There is no decision record for `[194.180.49.37](https://analytics.dugganusa.com/stix/register?ref=ioc-click&q=194.180.49.37)` at all.
There are records for two neighbouring addresses in the same /24 — from February, at BDE score 80, tier high, via the auto-blocker. So the engine has opinions about that neighbourhood. It has no opinion about the host that spent seven minutes tonight asking us for our credentials.
The reason is the seam. The BDE scores bursts partly on abuse tells — 403s, 404s, 504s, POSTs to the API. Our honeypot answered every one of tonight's requests with a convincing fake 200. No errors, no tells, nothing to score. The deception layer made the attacker invisible to the behavioural detector.
The outcome was still fine — the canary caught it, listed it at confidence 95, and the edge is enforcing it. But two of our four plates overlap in a way that leaves a gap, and if the canary paths had not happened to be in that scanner's dictionary, nothing else in our stack would have flagged it. Boatymcfuckface was caught by the BDE precisely because it generated 504s in abundance. Tonight's probe generated none, by our own design.
That is a real defect in our armour. It was found by writing this post rather than by an alarm, it is on the bench, and it is exactly the kind of thing that only surfaces when you document a win in enough detail to notice the shape of what is missing.
Why any of this matters to you
The Polish scanner is, in my partner's assessment, silly — and he is right. It is not damaging. It is a rented box running a Go binary against a filename dictionary, and it will find nothing here.
But it is the same thing that hits your dentist's website, your parish office, your regional charity, and the small law firm holding other people's divorce papers. The difference between us and them is not budget and it is not talent. It is four cheap layers and a decision to build them.
An edge blocklist is a free Cloudflare worker. A canary path is a static string. Refusing to execute PHP in your uploads directory is four lines of nginx config. Alerting on "credential added" is a checkbox nobody ticks. None of these cost hundreds of thousands of dollars, and together they stop the overwhelming majority of what actually arrives.
The fear is the product. The armour is cheap. Build it.
Capped where we always cap it at 95 percent, because we always do: our armour is good, it is not perfect, and the seam described above is live as you read this.
Sources
All attack data is first-party, from our own edge honeypot: [194.180.49.37](https://analytics.dugganusa.com/stix/register?ref=ioc-click&q=194.180.49.37), AS201814 (MEVSPACE, Warsaw), Go-http-client/1.1, 2026-08-25 01:55:59 UTC onward, 729 Poland-attributed honeypot records. Blocklist verification against our own published feed at min_confidence=80, 2,236 rows. ASN resolution via Team Cymru.
MITRE ATT&CK: TA0043 Reconnaissance, T1595 Active Scanning (T1595.003 Wordlist Scanning), T1592.002, T1552.001 Unsecured Credentials in Files, T1583.003 Virtual Private Server. Deception framing per MITRE Engage.
Incident cost comparison from our own prior write-up, "That Ain't Lake Minnetonka" (22 August 2026).
If you run a small site and you go and add one canary path this week — a fake /.env that logs and returns nothing useful — tell me what shows up. My guess is: something, within days. That is the cheapest security lesson available and almost nobody runs the experiment. Rate this post below.
How do AI models see YOUR brand?
AIPM has audited 250+ domains. 15 seconds. Free while still in beta.
Was this useful? Thirty seconds, no cookies, no tracking, no third parties, your address hashed and never stored. If the box below does not load, the same question lives at https://analytics.dugganusa.com/nps.html?post=how-armour-actually-works-and-why-ours-is-shaping-up-to-be-one-of-the-best-somebody-attacked-us-w




Comments