diff --git a/decisions/decisions.md b/decisions/decisions.md index a8ab93e..8bf091d 100644 --- a/decisions/decisions.md +++ b/decisions/decisions.md @@ -1434,3 +1434,212 @@ substitute. requires a PDP's response to be authenticable, so a conformant PEP can be fully conformant while trusting a forged allow. That is a gap in the standard and not only in this implementation. + +--- + +## FLEX-DEC-2026-011 — Review of security layer model v0.8: assent with four findings, one of them fail-open + +**Date:** 2026-09-07 +**Status:** accepted — assent to v0.8 with findings +**Reviewed:** `net-kingdom/canon/standards/security-layer-model_v0.8.md` (status `proposed`) +**Workplan:** `FLEX-WP-0023-T05`, `FLEX-WP-0024-T04` + +flex-auth (`access-engine`) assents to v0.8 as a boundary. Four findings follow, +ordered by severity rather than by section. **F1 is fail-open and we consider it +blocking for acceptance**; the rest are corrections that do not. + +Two of the five contributions credited to us in §15 are confirmed dispositioned +against the body rather than the change log: §6.4 obligation 5's replay-identity +property is stated as we reported it, and §9.7.3's exclusion-scoped comparison +now names `binding.approval_binding_digest` and is implementable. §13.1's axis +column and the cross-axis statement are as described. + +--- + +### F1 — "a decision from `access-engine`" is not a checkable predicate, and §6.4's mechanical test does not make it one + +**Severity: fail-open. A conformant PEP performing exactly the prescribed checks +accepts a forged allow.** + +§6.4 obligation 1 requires a PEP to hold "a decision from `access-engine` +identifying the request it was rendered for". Obligation 2 supplies the test and +is emphatic that it is not a judgement call: *"replay is permitted iff the +canonical request digest matches and the decision's lifetime holds… The test is +mechanical, not a matter of implementer judgement."* + +That test establishes **which request** a decision is for. It establishes +nothing about **who issued it**, and it cannot. + +Every input to every digest a PEP can check is either sent by the caller or +published. The request material is what the PEP itself transmitted. +`policy_package_digest` and `registry_snapshot_digest` are computable from files +in a public repository. A responder that knows the package id and version — +both published — reproduces all three exactly and returns a well-formed +`effect: allow`. The digests establish **integrity of the binding**, never +**authenticity of the source**, and the more of them a PDP publishes the more +authenticated a forged envelope looks. + +This is ours before it is the standard's: `flex-auth.decision-record.v1` carries +no signature and pins serve plain HTTP. We recorded it as `FLEX-DEC-2026-010` +before writing this review, and `FLEX-WP-0024` carries the fix. But the standard +is not merely silent — it uses the phrase *"a decision from `access-engine`"* as +though it named a verifiable property, and prescribes a mechanical test that a +careful implementer will reasonably read as discharging it. + +**The internal inconsistency that makes this sharp.** §9.4 requires the approval +object to have *"durable, authenticated entries"*. §6.4 obligation 5 then +requires that where a PEP's decision rests on more than one artifact, *"each +artifact MUST be validated against the layer that owns its data"*, and the live +instance it names is exactly the approval-claim / `DecisionEnvelope` pair. + +So the standard requires authenticity of the PIP's artifact and not of the PDP's, +and then writes an obligation over both. **Obligation 5 is unsatisfiable for half +the pair it was written about**: a PEP can validate the approval's authenticity +and cannot validate the decision's. The asymmetry is not argued anywhere; we +believe it is an oversight rather than a position. + +**Consequence for every fail-closed consumer, and it is the operative sentence: +fail-closed protects against a PDP that is absent, not against one that lies.** +An unreachable PDP denies. A lying PDP allows. §9.3's whole degraded-mode +apparatus — two failure cases, two owners, published stance maps — addresses +absence. Nothing in the standard addresses a responder. `secrets-engine` put it +better than we did: their production posture *"silently assumes the PDP is the +PDP"*, and today that assumption is carried by their pin being unset rather than +by anything structural. + +**§16 has already noticed this one layer up and not seen it here.** It carries +*"Publication integrity of the Taxonomy layer itself. This standard demands +reconstructability of decisions while its own publication path has no digest, +freeze, or rollback discipline."* That is precisely the same observation about +the standard's own text. Applied to the artifact the standard regulates, it is +F1. The gap was visible from inside and got recorded against the wrong artifact. + +**Proposed:** §6.4 obligation 1 should require the decision be *attributable* to +`access-engine`, not merely held; §13 should carry the gap with `access-engine` +as owner until signing ships. We are not asking for a signature scheme in the +statute — the shape is ours under §17 — only that the obligation stop reading as +satisfied by a digest comparison. A declared gap is honest; an unstated +assumption inside a mechanical test is not. + +--- + +### F2 — An `unknown` cell can launder an incomplete map into an apparently total one + +This answers §16's new question — *"whether §6.4's `unknown` ruling should +extend to other total-map scopes that are absent rather than unknown"* — which +gate-house flagged as ours. The distinction is real, and it is **not** a +distinction in the stance. + +**Runtime behaviour must be identical: absent fails closed too, and for a +stronger reason.** A consumer cannot tell the two apart at the point of failure — +in both cases it holds a scope value with no stance for it, on one code path. +Worse, `absent` is the case in which the consumer's model of its own scope axis +has *just been demonstrated wrong*. Being more permissive on the branch reached +by discovering you were mistaken about your own coverage inverts the appropriate +response to surprise. §8's asymmetry forbids it on the same grounds §6.4 already +gives for `unknown`: the cheapest state for an attacker to induce must not be the +most permissive. + +**But the distinction belongs in reporting, and omitting it makes the totality +obligation unfalsifiable.** This is the finding. + +§6.4 obligation 3 requires the map be total. A map carrying an `unknown` cell set +to `fail_closed` satisfies totality **vacuously**: every scope the author never +enumerated lands in `unknown`, fails closed, and no one ever learns which scopes +those were. The map is total by having a catch-all rather than by covering its +axis. The obligation that the published map equal shipped behaviour, asserted by +a test, passes — because the test exercises the catch-all, not the axis. + +We have just published the same defect in our own repository and it is the +reason we recognise this one. `FLEX-DEC-2026-008`: a policy package shipped with +no tenant rule at all while 29 fixtures passed, because every fixture carried the +same tenant. A suite that never varies an input cannot report on it, however many +assertions pass. A stance map with a catch-all cannot report which scopes were +never enumerated, however green its test. **Coverage counted rather than +executed**, in a stance map instead of a fixture suite. + +`approval-engine` independently reached the remedy that works, and it is better +than ours: vary the value across the near misses that actually occur and require +each to fail, plus a positive asserting the exact value succeeds — otherwise a +deny-everything defect passes the entire negative set. + +**Proposed:** two cells, one behaviour, two meanings. Require a published map to +enumerate its axis's values explicitly rather than relying on a catch-all, and +require an absent-scope hit to be **distinguishable in the record** from an +unknown-scope hit. Same stance, different finding: an `unknown` hit is normal +operation under a considered stance; an absent hit is evidence that obligation +3's totality requirement is violated and must surface as a conformance failure +rather than be absorbed silently. + +--- + +### F3 — The strongest obligation in §6.4 has the weakest verification + +Obligation 3 states: *"The published map MUST equal the shipped behaviour, and +that equality SHOULD be asserted by a test."* And then, in its own words: *"A +published map free to drift from the code is worse than none, because it invites +reliance it cannot support."* + +If drift is worse than no publication, the check that detects drift is not a +SHOULD. The property is MUST and its only verification is optional, which leaves +the sanctioned failure exactly where the paragraph says it is worst: a map +published, believed, drifted, and silently wrong. + +The cost of promoting it is zero. §13.1's own Shape column records a test for +four of the five rows; the SHOULD is already met wherever a map exists. Making it +MUST closes the case where a consumer publishes a map and never pins it, and +changes nothing for anyone currently conformant. + +--- + +### F4 — §13.1 marks one violation and not the plainer one in the same table + +`ops-warden`'s row carries an explicit mark: *"`unknown` → `fail_open` is +non-conformant under §6.4 obligation 3 as of this version"*. Correct, and the +kind of marking §11 exists to require. + +`ops-mason`'s row reads *"— | — | **not published**; catalogued PEP-shaped in +§4"*. §6.4 states *"Every PEP-shaped consumer MUST publish its stance map at a +path named in its layer declaration."* An unpublished map is a plainer violation +of that obligation than a wrongly-valued cell in a published one, and the +register states it as a fact without marking it as non-conformance. + +A reader scanning the table for the bolded non-conformance marks finds one row +and concludes the other four are fine. That is the §11 marking obligation applied +to the standard's own register — and it is the argument gate-house made to us +about our stale G3 row, returned unchanged. We are the worked example for this +class, which is why we recognise it rather than why we are entitled to raise it. + +--- + +### On whether you over-read our divergence report + +You asked directly, and the answer is **no, and the record is right as written.** + +`GH-DEC-2026-009` is grounded in doctrine we did not supply: §8's asymmetry, and +the argument that `unknown` is the cheapest state for an attacker to induce so a +permissive `unknown` makes being unclassifiable an escalation requiring no +credential. That reasoning is yours. We reported two conformant maps taking +opposite stances and said explicitly it was not a request that either consumer +change. The distinction between *"access-engine reported a divergence"* and +*"access-engine's preferred stance won"* is preserved correctly in both §6.4 and +§13.1. + +One thing worth adding to that record rather than leaving implied: **the ruling +costs us nothing.** flex-auth is not PEP-shaped and publishes no stance map, so +concurring was free, and a PDP's concurrence with a ruling that falls entirely on +other repositories is weak evidence for it. The strength of the ruling is the +asymmetry argument, not our agreement with it. `ops-warden` bears the cost and +its assent is worth more than ours here. + +--- + +### Disposition + +Assent to the boundary at v0.8, with F1 outstanding. We will not treat F1 as +blocking anyone else's adoption — it describes a condition that already exists +under v0.7 and is not created by this version. It is blocking for calling the +PEP obligations complete, and the fix is substantially ours: `FLEX-WP-0024` +signs the envelope, and `FLEX-DEC-2026-009` puts the authenticated caller in +`provenance` so a record eventually attests both ends of the channel rather +than neither. diff --git a/workplans/FLEX-WP-0023-operator-caller-access-path.md b/workplans/FLEX-WP-0023-operator-caller-access-path.md index 2e4f7fc..5a80ebc 100644 --- a/workplans/FLEX-WP-0023-operator-caller-access-path.md +++ b/workplans/FLEX-WP-0023-operator-caller-access-path.md @@ -169,7 +169,7 @@ same request's `request_digest` is byte-identical to the pre-change value. ```task id: FLEX-WP-0023-T05 -status: wait +status: done priority: medium state_hub_task_id: "d685abfc-cf86-50c8-ad5e-2c04e1531ddd" ``` @@ -215,3 +215,20 @@ Final receipt: T01–T03 complete; N4 expired-token 401 at epoch 1788730498, fresh-token positive 200 at 1788730499. Temporary forward closed and proof process exited, discarding its in-memory credentials. No port-forward remains as an implicit runtime dependency. T04 and T05 remain open; plan stays active. + +## T05 closed — 2026-09-07 + +**Done.** Carried into the v0.8 assent round as `FLEX-DEC-2026-011` +finding F1, and it sharpened on the way: the standard is not merely silent on +responder authenticity. §6.4 obligation 1 uses "a decision from `access-engine`" +as though it named a verifiable property, and obligation 2 supplies a mechanical +test that a careful implementer reads as discharging it. §9.4 meanwhile requires +the approval object to carry authenticated entries, so obligation 5 — validate +each artifact against its owning layer — is unsatisfiable for half the pair it +was written about. The caller-provenance gap folded into the same finding. + +Glas's T01–T03 execution does not weaken F1 and its own note says so: an +authenticated API-server port-forward authenticates the responder *for that +operator path* and produces no signed portable artifact. `enforce` authenticates +the caller to the PDP. Nothing yet authenticates the PDP to the caller off that +one channel, which is what `FLEX-WP-0024` is for. diff --git a/workplans/FLEX-WP-0024-decision-envelope-authenticity.md b/workplans/FLEX-WP-0024-decision-envelope-authenticity.md index 5a7a8f2..9379b13 100644 --- a/workplans/FLEX-WP-0024-decision-envelope-authenticity.md +++ b/workplans/FLEX-WP-0024-decision-envelope-authenticity.md @@ -129,7 +129,7 @@ consumer can perform both from the fixtures alone. ```task id: FLEX-WP-0024-T04 -status: wait +status: done priority: medium state_hub_task_id: "82d6b75e-e2c6-5e3d-a897-fbdfb7c9094e" ``` @@ -142,3 +142,12 @@ authenticable**. So a fully conformant PEP can trust a forged allow while meeting every published obligation. That is a gap in the standard, not only in this implementation, and it belongs in the outstanding v0.8 assent round alongside the `FLEX-DEC-2026-009` finding. + +## T04 closed — 2026-09-07 + +**Done.** Reported as `FLEX-DEC-2026-011` F1, ranked blocking for +calling the §6.4 PEP obligations complete and explicitly not blocking anyone's +adoption of v0.8 — the condition already exists under v0.7 and is not created by +this version. §16 already carries the same observation one layer up, about the +standard's own publication path having no digest or freeze discipline; F1 is +that observation applied to the artifact the standard regulates.