From a compromised WordPress to an operational ontology
A site was compromised, and closing it took three days. The three weeks of argument afterwards produced something the defence could not: a reason for the next incident to leave the system different from how it found it. This is the derivation, with the wrong turns left in.
It started with password-reset emails arriving for accounts nobody had asked to reset. That is the kind of signal that is either nothing or everything, and there is no way to know which without looking.
It was everything. A WordPress site had been compromised weeks earlier. Two dozen administrator accounts had been created in a contiguous block, web shells were on disk, and sessions belonging to the intruder were still alive.
The tenant is anonymised throughout, and so is every coordinate of the defence. What follows is the shape of what happened, which is the part that transfers.
What the attack actually looked like
The noisy part was credential stuffing: 16,615 requests in 24 hours across three tenants, from 74 hosts, all rented cloud infrastructure. Loud, concentrated, easy to see.
The noise was the design. The real entry ran underneath it, through an endpoint that packs many sub-requests into a single HTTP request — which means any per-request rate limit counts it once. And the login form had been renamed by a plugin, which quietly moved it outside the block that basic auth was protecting. The protected login page had never seen a single request. It did not need to.
That part is a normal, if unpleasant, incident. Accounts purged, shells removed,
sessions destroyed, the endpoints closed at the web-server layer, verified from
outside. Today the attack traffic has not dropped at all — thousands of requests a
day still arrive — and every one of them dies before reaching the application.
The metric that matters is not how many arrive. It is the worst response code
observed. That number is now a 401.
The discovery was not the attack
While closing it, four unrelated things surfaced. Each had the same shape:
- A registry’s access policy declared eleven namespaces and served four. Nothing failed, because the running machines all had the images cached. It would have failed the first time anything needed a fresh pull.
- A component declared credential rotation as one of its responsibilities. Its update path applied manifests and restarted the pod — it never re-created the secret. No rotation could ever reach the cluster. It had been that way for months.
- Two alerts were declared in a component’s supervision block. The log collector on the machine running those services had been unable to reach the log store for days. The log store held zero series. The alerts described a pipeline that delivered nothing.
- A PHP pool declared it could run twenty workers. The container’s memory limit fitted about six. Six workers already sat at 411Mi of a 512Mi ceiling. The container had been killed five times.
Four mechanisms, one pattern: the declaration was correct and meant nothing, because nothing contrasted it against the running system.
Every one of these passed every review. A declaration is unfalsifiable by inspection. You can read it and agree with it, and be wrong for months.
The question that had no answer
Three days later the operator asked the obvious question: is this still happening? Is it mitigated? What state are the services in?
Answering took a full working session — cluster queries, SQL against three databases, log arithmetic, external probes. And here is the part worth sitting with: every fact recovered that day had already been established three days earlier. Nothing new was learned. The work was pure reconstruction.
Worse, the reconstruction produced a false positive. An administrator account created during the incident response was flagged as a possible attacker artifact. It was a deliberate operator decision — an account created for service management after removing the one the attackers were targeting. The decision existed. It was written nowhere that could be consulted.
Incident-response indicators and intrusion indicators look identical. Without a record, every future reviewer re-derives the same wrong conclusion. Including a machine one.
Why nothing already in place could hold it
The project was not short of structure. It had architecture decision records with executable predicates attached. It had a typed backlog. It had an action log.
None of them fits:
- A decision record holds the predicate, but it is architectural and nearly immutable. Twenty-three of them in the project’s life, while a service accumulates commitments weekly. “We enabled two-factor auth for two of three administrators” is not an architectural decision.
- A backlog item holds intent, and it closes. A commitment never closes; it recurs. One backlog item in this project states, in plain prose, that a claim “a workstation cannot check” — because the schema had no way to express the check, so it narrated it.
- A log entry holds the moment, not the claim. It cannot be re-evaluated later.
And the place where these commitments actually lived? A list of strings in a component definition. Four names. No state, no evidence, no date, no predicate. One of those names was the rotation whose mechanism could not deliver it.
The first design was wrong
The obvious move is to give those strings a type: promote them to a record with a claim, an executable check, and a verification date, right there in the component.
The operator rejected it, on three grounds that were all correct:
- Component definitions are owned upstream, by the domain. Adding a field there propagates a schema to every workspace derived from it, whether or not they use it.
- A service has contingencies no schema can anticipate. If registering an unforeseen contingency requires modifying the domain contract, contingencies will not get registered — the bureaucratic cost exceeds the benefit and people go back to writing a comment.
- A record inside a regenerable file shares its fate. A rewrite from the catalog erases it silently. A record that can evaporate on update is worse than no record, because you believe it is there.
The correction was already in the codebase. Cabling between components had faced the same problem and solved it by putting the information in a lateral file, discovered by naming convention, keyed to the component, validated against a schema — and adding nothing at all to the shared contract. Solved once by lateralising, not by extending.
Cells and tissues
That is where the frame changed, and it came from the operator, not from the design. The vocabulary that follows — cell, tissue, nucleus, membrane, lineage, apoptosis — is not defined here on purpose. It is defined in the registry those links point at, and the reason for the discipline is the subject of this post: the moment a blog post defines a term the schema also enforces, the site says one thing and the mechanism another, and the drift is now public.
Two placements had to be corrected, and both corrections came from pushing the analogy honestly rather than decoratively:
Memory is not in the cell. It belongs to the tissue. A cell carrying its own history cannot be exported, because that history means nothing where it lands. Statelessness is what makes a cell portable — one workspace can hand a cell to another, which then differentiates it in its own context.
Role is not in the cell. Sensing, validating and regulating are axes of expression the tissue assigns. The same cell is a sensor in one tissue and a policy in another. Which means it also cannot be welded to a moment: the same cell must be usable in a pre-commit hook, in CI, in an agent session, and in a live service check. It answers with what it has, and what it does not have has a name.
Not knowing is a first-class answer
A verdict is pass, fail, or unknown. A cell that needs cluster access,
invoked from a pre-commit, does not fail — it returns unknown, and each circuit
decides what that means for it. Without that, every cell has to be excluded per
context, and the exclusion welds it back to a place.
This is the part that stopped being a proposal while this post was being written. It is now a ratified protocol decision, and it arrived with numbers that make the case better than the argument did.
The tool already had the third value on one of its two executors, and had never finished applying it. Of 59 constraints reported violated on this tree, at least 27 were the harness never reaching its oracle — 46% noise, in red, which is precisely why the number had been run, read, and ignored. Extending the third value to the other executor turned up three checks that had never once passed, one of them announcing “manifest capabilities incomplete” from a run in which the module it needed had never loaded. A claim about its subject, from a run that never looked at the subject.
Two of those three were found by the mechanism itself, the day it reached them, before anyone went looking. That is the difference between a mechanism that reports and one that is asserted to work.
And the third state alone turned out not to be enough — which is the fourth correction in this story, and the one made against the model rather than against the design.
A check declares what it needs in order to answer, so absence of a capability
yields unknown instead of a false red. Now apply that to a gate that must
confirm a privacy policy is published before any personal data is routed
anywhere. No network → unknown → permissive → the flow proceeds to collect
personal data having verified nothing. Absence of proof becomes proof of
compliance, and the mechanism built to stop a check from lying introduces the
lie itself.
Severity says how bad it is for a claim to be false. Nothing said how bad it is for it to be unanswered. So a claim now also declares what its own silence means — permit, or refuse — and that posture travels with the claim rather than with whichever circuit invoked it. A fail-closed check that arrives somewhere else fail-open is not the same check.
Protection is not evolution
Here is the question that reframed the whole thing, and it was asked as an objection: a harness without ontology only solves one problem. We protect ourselves — but do we evolve?
No. A harness without ontology is innate immunity: fast, effective, and it learns nothing.
There is a working example in the same cluster — an automated detector that spots a filesystem going read-only and dispatches a repair job. It works. It can run a hundred repairs and the system’s model of itself is identical to before the first one. It protects, and it teaches nothing.
Adaptive immunity is slower, generates new specificity, remembers, and leaves the organism different after the encounter. The difference is a return arc:
perception → action protection
perception → signal → tension → decision evolution
The project already had that arc — for sessions. Closing a working session captures state, evaluates whether a decision deserves recording, and registers an emergence signal. What was missing is its twin on the operation side. A service can fail fifty times and nothing reaches the model.
The evidence was sitting there: one pattern — the mechanism declares and does not deliver — appeared four times in seven days and had no ontological existence at all.
With one hard line: the harness proposes, the human ratifies. Detecting a repeated pattern is mechanical. Naming it is a decision. A harness that declares its own tensions is not evolution, it is drift.
What this asks of a protocol
The frame is not new vocabulary bolted on, and the test of that is not whether the words sound apt. It is whether they land on boundaries the architecture had already drawn for unrelated reasons.
They did, and the strongest instance is not lexical. The cell/tissue split — what travels, and what is meaningless outside the context that holds it — turned out to be the same line the protocol had already ratified between a domain and its instances: declarations converge at the domain, execution and memory diverge per instance. Nobody was aiming at that. The model was derived from an incident by someone not looking at that decision.
A term landing on a term can be dismissed as vocabulary shopping. A model landing on a boundary that was already there cannot, and that is the honest answer to the reader who suspects the biology was chosen first and the mechanics fitted after.
That convergence has since been decided in its own right: what crosses between tissues is protocol — a lineage envelope naming its parent by content digest, so a mutated parent makes descendants stale rather than silently wrong — and the tissue itself is explicitly not standardised. Memory, cadence and role stay local, because they mean nothing in a receiving tissue. Deciding that something stays local is a decision, not a gap, and it is written down as one so the next author does not close it helpfully.
There was also a boundary term already in use as a field in the gating mechanism and absent from the lexicon. The mechanism had it; the vocabulary did not. It has since been defined, on the warrant it already carried — which is the smallest possible instance of this entire post, and the only one that took ten minutes to fix.
One term from this derivation is still marked provisional in the registry: making a difference, the criterion that a cell is alive only if its verdict can change at least one decision in at least one circuit. It has no ratified warrant yet. It is linked here in the state it is actually in, which is the whole point.
It happened again, to the people writing it
While these notes were being written, the protocol’s own query surface was asked for one of these terms from a different repository. It answered “No glossary found” — with total confidence, about a term that exists.
The vocabulary is assembled per project root. That root declared no glossary. So
the tool reported absence as fact, where the honest answer was unknown: the
exact case for the third value, made by the surface that lacked it, days after
the case was written down, to the people who wrote it.
The derivation is the product
Three days of incident response produced a working defence. The three weeks of argument afterwards produced something the defence could not: a reason for the next incident to leave the system different from how it found it.
Everything above happened in the order it is written, corrections included. That is not modesty. In a protocol whose central claim is that a statement about a system should be contradictable by a machine and not only by a reader, showing the statements that got contradicted is the only honest way to present it.
The question worth taking away is not about our cluster. It is: what did your infrastructure learn from its last incident? If the answer is “it was repaired”, that is protection. It is not nothing. But it is not evolution either, and the difference is whether anything in your system is different now because it happened.