01
The service that started in the wrong modeAn absent value is not a typo
Symptom
Production runs as development, and nothing in the logs or the health checks looks wrong.
A configuration field typed as a fixed set of values catches a misspelling. It catches an empty string. It does not catch absence, because absence takes the default — and the permissive mode is a popular default.
The damage is not the mode itself, it is what hangs behind it: a production system that quietly enables its development affordances is indistinguishable, in its logs and in its health checks, from one that started correctly. There is no moment at which anything looks wrong.
What catches it
the environment name has no default, and the service refuses to start without it. The test asserts the refusal, not the parsing — the parsing was never the part that failed.
02
The stored data that a config change made unreadableThe ciphertext says how to read it; the settings don't
Symptom
After a configuration change, data written earlier stops decrypting, and the error blames the key.
If the decryption scheme is selected by configuration, then changing configuration retroactively reinterprets data written under the old one.
The failure surfaces as “wrong key”. That is the expensive part: whoever is debugging goes looking for a key problem that does not exist, while the data is intact and merely being read by the wrong rules. Every minute of that hunt is spent in the wrong repository.
What catches it
the scheme is named by a prefix on the ciphertext itself, and no prefix means legacy. Configuration cannot reinterpret history it did not write.
03
The risk that was described and stayed openA name is not a fix
Symptom
The defect is documented, everybody knows about it, and it is still there.
A test can carry a risk in its name and assert nothing about it. A comment can state a hole precisely and leave it exactly where it is.
This is the failure mode of teams that document well. A described gap feels handled in a way an undescribed one does not, and it acquires a strange kind of immunity: everybody has read it, so nobody is surprised by it, so nobody is alarmed. It can sit in plain sight for weeks with no one hiding anything.
What catches it
an open risk gets an assertion that fails until it closes, or a line in a list the document refuses to omit. An empty list reads as “covered”, which is the one thing it was never safe to assume.
04
The mechanism that was built and never ranWritten, permitted, unreachable
Symptom
The function exists, the permission for it was granted by its own line in the access policy — and it is called from nowhere.
A separate, deliberate line in an access policy is evidence that at least two people reasoned about this mechanism. It still never ran.
Dead code is ordinarily harmless. This kind is not, because everyone downstream has already recorded the operation it belongs to as done. The mechanism's existence is doing the work its execution was supposed to do.
What catches it
a check that every public mechanism has a caller, run in CI where a new one cannot be added without it.
05
The green check that was always going to be greenAn indicator is not a diagnosis
Symptom
The check is green, and it would have been green either way.
A source scan that looks for variable names finds the names it knows. It does not find a value arriving under a different name, a foreign response body logged whole in one call, or a variable somebody renamed last week.
Its green means “not found this way”. That is a much smaller claim than the one people read into it, and the gap between the two is where the rule quietly stops being enforced while continuing to be reported.
What catches it
publish the limitation beside the check, in the same table as the claim it supports. An indicator presented as a diagnosis retires the question it was meant to keep open.
06
The control that stopped applying and said nothingA silent failure is a security defect, not an inconvenience
Symptom
The measure no longer applies, and nobody reported it.
A mandatory input that falls back to a default when it is missing. An exception swallowed on a path that only executes in production. A filter that passes quietly when its pattern fails to match.
Each is a control that stopped working while continuing to look present, and each will be discovered by exactly the event it existed to prevent. Silence is not neutral here: it is the report that the control is fine.
What catches it
the absence of a required input is a refusal, not a default — and the refusal has its own test, because a refusal nobody has triggered is a refusal nobody has watched work.
07
Healthy, ready, and unable to do the jobReadiness checked the dependency, not the capability
Symptom
Both probes green, every authenticated request failing.
A readiness probe that asks whether the key service answers is checking the wrong thing. Between “the vault answers” and “we can sign” sit a sealed vault — one that is running and reachable but has not yet been given its keys after a restart — an expired role credential, and a value encrypted under a scheme that no longer exists. All three leave the dependency perfectly reachable and the capability gone.
What catches it
readiness performs the capability and fails when it cannot. Pair it with the opposite rule, which matters more: the same sealed state must break issuing and must not break verifying, or a routine operational pause turns into a full stop for everything downstream.
08
The request that chose how much it would costThe sender sets the price unless you do
Symptom
One request stalls the whole process, health endpoint included.
A request body with no ceiling reaches a parser, and the parser's cost is chosen by whoever sent the bytes.
Measured on my own code: 640 KB of a structured binary format spent 29.5 seconds of CPU inside a single-threaded event loop. Everything else waited — including the health endpoint, which is how a parsing problem presents as an outage.
What catches it
a ceiling on body size, decided as a cost question rather than set as a configuration value. Be exact about what it is not: it is not a rate limit, and it does not mean the bytes failed to arrive.
09
The service that answered a question it shouldn't haveAnswer with a fact, not a decision
Symptom
One service decides what another service is allowed to do.
“The session exists” and “this is allowed” are different sentences, and the identity layer is only entitled to the first.
When it answers the second, its notion of sufficiency is compiled into every caller. The day one caller needs a stricter bar — a larger transfer, a new jurisdiction, a customer who asked for it — the change has to be made in the wrong codebase, by a team that does not hold the risk and cannot price it.
What catches it
return facts — which methods proved the identity, when, at what strength — and let the caller decide. It reads as less helpful. It is what keeps the decision next to the money.
10
The strong factor that a weak one could removeA construction is as strong as the cheapest way to dismantle it
Symptom
The account is protected by a strong second factor, and that factor can be removed by presenting a weaker one.
Adding a strong second factor raises the bar exactly until the recovery path lowers it again.
If a strong factor can be removed by presenting a weaker one, then the account is protected by the weaker one, whatever the enrolment screen implies. The arithmetic is not additive. It is a minimum, taken over every path that can remove a factor — and recovery paths are usually designed by whoever is optimising for support volume.
What catches it
removing a factor requires that same factor, not merely a strength level that something weaker also satisfies. The distinction is between *which method proved it* and *how strong the proof was* — two claims that collapse into one at the first convenient moment.
11
The rotated key that kept workingRotation is a change and a withdrawal
Symptom
The rotation is done, both sides consider it complete, and the old key is still accepted.
Rotation is two operations, and the second is the one that gets lost.
Taking the old key out of circulation has to happen everywhere verification reads, and there is usually more than one such place: a published key set for outside callers, a direct lookup for internal routes. Filter one and the other carries on accepting — which is worse than not rotating at all, because now everybody believes it is done and the compromised material is no longer being watched.
The same shape appears one layer down. A key service reports a rotation and the rotation happened; what did not happen was raising the minimum version accepted for decryption, so the retired key goes on unwrapping old material. Both sides read the success of an operation as the arrival of a property.
What catches it
withdrawal lives in the store that every verification reads, not inside each verification. The command exits non-zero until both halves are done — a command that exits zero at the halfway point announces “done” in precisely the place where it is half done. And the test rotates, then presents the old key to every path there is.
12
The audit log its own subject could editA witness table is defined by permissions, not by intent
Symptom
The log is complete, and the role whose actions it records can rewrite it.
A table becomes evidence when the role whose actions it records cannot change it. A comment in the model reading “audit log — do not modify” is an intention, and intentions are not controls.
The argument extends to shape: a log whose schema one person can change alone is not a log, for the same reason that a log one person can empty is not one.
What catches it
a separate owning role; insert and select for the recorded role and nothing else. And somebody has actually attempted update, delete, truncate and alter from that role and watched each one refuse — because this property lives in the live database and nowhere else. On a test database the role is different and the grants do not exist at all, so the suite can show that the log records and can never show that the log resists. The half it covers is not the interesting half.
13
The probe that reported the opposite of the truthA zero exit code is not evidence
Symptom
The permission check passed. The permission was never granted.
Granting yourself a privilege you do not hold can succeed without error and without effect.
A probe that reads only the return code reports success — and reports it in the one place where being wrong costs most, because this is the check that exists to catch exactly this. The probe is not broken. It is answering a question about the command, not about the world.
What catches it
probe by consequence, never by exit status. Attempt the forbidden operation and require it to fail.
14
The audit record deleted along with its subjectCascade is a convenience that erases evidence
Symptom
The user was deleted, and with them the record of what was done to them.
A cascading foreign key is the obvious default, and it quietly makes the evidence weaker than the thing it witnesses: a row nobody is permitted to delete directly disappears the moment its subject does.
Nobody decides this. It arrives with the schema, it survives review because it is the normal choice everywhere else, and it is discovered on the day somebody asks what was done to an account that no longer exists.
What catches it
the reference nulls instead of cascading — verified by behaviour against a live database rather than by reading the schema, because the schema is what looked correct in the first place.