DepWall

Home / Blog

  • policy
  • verdicts
  • supply chain

Org policy can tighten a verdict. It can never unblock one.

Letting a web dashboard influence a security gate is one compromised account away from installing the package the engine just stopped. So the merge is asymmetric: block always applies, allow only ever reaches ASK, and BLOCK is not negotiable by configuration.

The dashboard has been a policy editor with no reader for a while. You could write org rules and nothing on any developer's machine would ever consult them. This is the reader.

depwall guard now fetches its org's policy once per invocation and applies it to every package in the resolved tree. Four environment variables configure it, and all four are required:

DEPWALL_POLICY_URL
DEPWALL_ORG
DEPWALL_ORG_TOKEN
DEPWALL_POLICY_PUBKEY

Leave any one of them unset and nothing is fetched, nothing changes, and the install path costs nothing. There is no partial mode where three of four variables gets you a policy with one guarantee missing — half-configured security is the state in which people believe they are protected.

The rule#

A rule may always tighten a verdict. It may only loosen one that was already ASK. BLOCK is not negotiable by org configuration.

EffectApplies toNever applies to
blockAny tier — escalation is always allowed
askALLOWBLOCK
allowASK, and only ASKBLOCK

Escalation is free in the sense that matters here: the worst case of a wrong block rule is a false BLOCK, which a human immediately sees and can remove. The worst case of a wrong allow rule is nobody seeing anything.

Why the asymmetry is the entire security argument#

If an org rule could turn BLOCK into ALLOW, then compromising one dashboard account installs the package the engine just stopped — across the whole org, quietly, with the gate reporting success.

That is not a hypothetical this project is discovering for the first time. It already refused to let its own published advisory feed feed its own scanner, for exactly this reason. A feed with a login form in front of it is worse than one without, not better: the login form is a new way in, attached to the same downstream authority.

So the invariant lives in one expression, in src/policy/apply.ts, and nowhere else:

// THE invariant, and it lives here ONLY.
const escalates = RANK[next] > RANK[tier];
const relaxesFromAsk = RANK[next] < RANK[tier] && tier === "ASK";
if (escalates || relaxesFromAsk) {
  tier = next;
  applied = rule;
}

"It lives here only" is load-bearing, and it is written in the comment because we learned it the expensive way. The rule had been encoded in two places, and a mutation test passed in the dashboard's copy while the other copy had been deleted. Two encodings of one invariant means two things that can drift and one test that can be satisfied by the wrong one.

Verification, not decoration#

A policy is applied only if it clears all of these, and each has a test that fails without it:

  • It is Ed25519-signed by a pinned key — the one in DEPWALL_POLICY_PUBKEY, not one the server offers.
  • It is for the org we asked for. A correctly-signed policy for a different org is not a policy for you.
  • It is fresh. An expired policy is not applied, which means a replayed old policy cannot restore a rule you removed.

The merge itself is mutation-verified twice: deleting the guard fails a unit test, and stubbing the merge out of guard.ts fails four integration tests. A guard whose removal breaks nothing is decoration.

Three smaller decisions#

A bare * is refused

Wildcards match by prefix — @acme/* matches @acme/anything — and everything else is exact. But a rule whose name is just * matches nothing, because one typo in a dashboard field should not be able to mute an entire ecosystem. A prefix is required.

An unknown effect is ignored, not guessed

The merge is pure, total, and never throws. A malformed rule is skipped rather than failing the check open or closed — a policy document that a newer dashboard wrote and an older CLI does not fully understand must not take the gate down, and must not take the gate off.

The file is duplicated on purpose

The same eighty lines live in the CLI and in the dashboard, pinned to identical canonical bytes by a golden test. A shared package between a CLI and a Cloudflare Pages app would be a third artifact to publish, version and keep in sync for eighty lines of pure function. The golden test is the cheaper guarantee, and it is the one that would have caught the drift described above.

What this does not give you#

It does not give an org a way to run a more permissive gate than the engine. That is the point, and it is worth being blunt that some buyers will read it as a missing feature. If your team genuinely needs a package the engine blocks, the answer is a fix to the detection or a documented exception outside the gate — not a switch in a web app that turns the gate's strongest verdict into its weakest.