The adversary I didn’t have
A few days ago I set out to make my review system trustworthy enough to actually use, and within the hour I was designing cryptographic authentication for reviewer reports. Each reviewer would sign its verdict; the gate would verify the signature; a forged report — one a reviewer never actually produced — would be caught and thrown out. It was a good design. I was well into the design, working out which reviewers could be authenticated and how, when I stopped and said, out loud, to no one: we’re over-engineering for what I need.
What I was designing was a defence against forgery. Picture an orchestrator — the thing that runs the review, collects the verdicts, and decides whether work passes — gone bad: it wants to sneak a broken change past the gate, so it fabricates a clean review and pins it on a reviewer that never saw the work. Authentication stops that — sign every verdict, and a fabricated one fails the check. It is a genuinely satisfying problem: authenticatable quorums, who-signed-what, which identities can be trusted. The kind of problem that is a pleasure to design. That pleasure, it turned out, was the first thing I should have been suspicious of.
The adversary I didn’t have
Because here is who runs the orchestrator in my setup: me. Solo, on my own machine, on infrastructure I control. The malicious orchestrator I was building a cryptographic defence against — the one fabricating reviews to subvert its own gate — is me, attacking myself, on my own hardware, for no reason. It doesn’t exist. There is no adversary in my threat model. My actual threat is duller and far more real: I make mistakes. I move fast and misconfigure something; a reviewer silently gets pointed at the wrong work; two roles collapse onto one model and I don’t notice. Those are accidents, and they happen. An adversary forging signatures to defeat my own gate is not a thing that happens to a solo operator on trusted infrastructure. I had spent an hour designing defences against an attacker I would have had to hire.
A guarantee that couldn’t hold
And then the part that should have stopped me sooner than it did. Even if I built the whole thing, it couldn’t work for the reviewer I most wanted it to cover. The most valuable reviewer in my panel runs in-process — spawned by the orchestrator, inside the same process, sharing its memory and its filesystem. A signing key is a secret, and a thing the orchestrator spawns cannot keep a secret from the orchestrator: whatever key the reviewer holds, the orchestrator can read, which means it can sign anything it likes in the reviewer’s name. The signature looks like authentication and is forgeable by exactly the party it is meant to defend against. For the reviewers that run as separate processes and answer over a bus, you could make it work; for the in-process one, the cryptography is theatre by construction. I had already written that down, in the design: leaky by construction — cryptographic signing can’t hold a secret the orchestrator can’t read.
Which means I had reasoned the design all the way to the point of proving it couldn’t do the job — and only then, holding a proof that the elegant thing was impossible for the case I cared about, did I think to ask the cheaper question I should have asked at the start: do I even have the threat this defends against?
Match the enforcement to the stakes
The answer was no, and that reframes the whole exercise. Enforcement should be sized to the threat you actually have, not the most sophisticated one you can imagine. Building adversary-grade authentication for accident-grade stakes isn’t extra safety; it’s the trap where building the tool quietly becomes the work instead of using it. So I made the boring decision: drop the authentication tier, and name the limit out loud — this system defends against mistakes, not a malicious orchestrator; if it ever becomes something other people run, or runs somewhere I don’t control, the adversarial version is where I’d start. Until then it isn’t deferred-and-pending, it’s just not needed. What I kept were the cheap checks that catch my real threat: that the reviewer answering is the one the roster expected, that the model a verdict is recorded under is the one the roster assigned it, that the scribe can’t reach the verdict. They cost almost nothing and they catch mistakes — which is what actually goes wrong. The expensive machinery I dropped from the design caught an attacker I don’t have.
And naming the limit is not the lesser option; it is the better one. A named limit tells you exactly where you’re exposed, so you stay alert there. The authentication I almost built would have done the opposite — sat in the architecture looking like a guarantee of integrity while, for the reviewer that mattered, guaranteeing nothing. That is the worse failure: not a gap you can see, but a reassurance you can’t, papering over the gap. I’ve written before about tests that pass for the wrong reason; this is the same shape one level up — in the architecture instead of the test. Better to say plainly “this catches accidents, not adversaries” than to ship a lock with its key resting beside it.
Before you build enforcement, ask two cheap questions: am I defending against an accident or an adversary, and do I have that adversary? If the answer to the second is no, you’re not building safety — you’re building the appearance of it. Match the enforcement to the threat you actually have, and name, out loud, where it stops.