DEV Community

Cover image for The check that never failed
Jeffrey Jorgensen
Jeffrey Jorgensen

Posted on Originally published at jeffreyjorgensen.dev

The check that never failed

Fourteen measures that were present, had been reviewed, and were believed. None of them was working. Each entry says what it looked like from outside, and what it took to make it prove itself.

A safeguard has two states that are easy to confuse: present, and working. Almost nothing below was found by an incident. Each was found by asking what would have to be true for the measure to be working, and then attempting it.

None of them were found by reading code either. Reading tells you the control is there. It can't tell you the control is reached, that its refusal path has ever executed, or that the permission it depends on was actually granted.

A check that has never shown a failure has not been checked.

Every entry came out of a system I had to prove rather than one I had read: an identity service issuing signed tokens, a double-entry accounting core across four networks, and the infrastructure under both. Ordered from what you meet on every deploy to what costs the most when it fires.

What each one looked like

  1. The service that started in the wrong mode
  2. The stored data that a config change made unreadable
  3. The risk that was described and stayed open
  4. The mechanism that was built and never ran
  5. The green check that was always going to be green
  6. The control that stopped applying and said nothing
  7. Healthy, ready, and unable to do the job
  8. The request that chose how much it would cost
  9. The service that answered a question it shouldn't have
  10. The strong factor that a weak one could remove
  11. The rotated key that kept working
  12. The audit log its own subject could edit
  13. The probe that reported the opposite of the truth
  14. The audit record deleted along with its subject

01. The service that started in the wrong mode

An absent value is not a typo

What you see. 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 doesn't catch absence, because absence takes the default, and the permissive mode is a popular default.

The damage isn't the mode itself, it's 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's no moment at which anything looks wrong.

What proves 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 unreadable

The ciphertext says how to read it; the settings don't

What you see. 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's the expensive part: whoever is debugging goes looking for a key problem that doesn't 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 proves 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 open

A name is not a fix

What you see. The defect is documented, everybody knows about it, and it's 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 doesn't, 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 proves 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 ran

Written, permitted, unreachable

What you see. The function exists, the permission for it was granted by its own line in the access policy, and it's 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 isn't, 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 proves it: a check that every public mechanism has a caller, run in CI where a new one can't be added without it.

05. The green check that was always going to be green

An indicator is not a diagnosis

What you see. 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 doesn't 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's 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 proves 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 nothing

A silent failure is a security defect, not an inconvenience

What you see. The measure no longer applies, and nobody reported it.

A mandatory input that falls back to a default when it's 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 isn't neutral here: it's the report that the control is fine.

What proves 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 job

Readiness checked the dependency, not the capability

What you see. 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 (running and reachable, but not yet handed 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 proves it: readiness performs the capability and fails when it can't. 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 cost

The sender sets the price unless you do

What you see. 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 proves it: a ceiling on body size, decided as a cost question rather than set as a configuration value. Be exact about what it isn't: it isn't a rate limit, and it doesn't mean the bytes failed to arrive.

09. The service that answered a question it shouldn't have

Answer with a fact, not a decision

What you see. 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 doesn't hold the risk and can't price it.

What proves 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 remove

A construction is as strong as the cheapest way to dismantle it

What you see. 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 isn't additive. It's 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 proves 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 working

Rotation is a change and a withdrawal

What you see. 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's 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's 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 didn't 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 proves 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's half done. And the test rotates, then presents the old key to every path there is.

12. The audit log its own subject could edit

A witness table is defined by permissions, not by intent

What you see. 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 can't 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 proves 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 don't 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 truth

A zero exit code is not evidence

What you see. The permission check passed. The permission was never granted.

Granting yourself a privilege you don't 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 isn't broken. It's answering a question about the command, not about the world.

What proves 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 subject

Cascade is a convenience that erases evidence

What you see. 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's the normal choice everywhere else, and it's discovered on the day somebody asks what was done to an account that no longer exists.

What proves 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.

Every one of them reported success

Every one is a measure that reported success while not applying вАФ because nothing ever asked it to refuse, and a refusal nobody has triggered is a refusal nobody has watched work.

Two of the catches above are source scans, and both are labelled as indicators rather than proof. Everything else was found the same way: presenting a withdrawn key to every verification path there is, running the forbidden statement as the role that must not be able to run it, sending the request that costs too much. The list is kept in the open because an empty line in a document like this reads as вАЬcoveredвАЭ.

Send me what your safeguards claim

A policy, a checklist, or the three controls you would name if asked what protects the money. IвАЩll reply in writing with the ones that can't currently fail, and what it takes to make each one demonstrate its own refusal. No charge, and nothing to attend: a written answer, usually inside two days. The systems this came out of вЖТ


Originally published at jeffreyjorgensen.dev/security. Companion pieces: Reconciliation Doesn't Break in the Arithmetic and Falsehoods Engineers Believe About Moving Money.

Top comments (0)